Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions apps/web-app/app/components/PartnerAgreementCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<UCard
v-if="agreement"
variant="subtle"
class="h-full"
>
<div class="flex flex-col gap-3">
<div class="flex flex-row items-start gap-3.5">
<UIcon name="i-lucide-scroll-text" class="shrink-0 size-16 text-secondary" />

<UProgress
v-model="agreementProgress"
size="lg"
color="secondary"
status
/>
</div>

<h3 class="text-xl md:text-xl/6 font-semibold">
Договор №{{ agreement.internalId }}
</h3>
Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use internationalization for hard-coded Russian text.

Hard-coded Russian text should be moved to i18n locales for better maintainability and potential future localization.

-      <h3 class="text-xl md:text-xl/6 font-semibold">
-        Договор №{{ agreement.internalId }}
-      </h3>
+      <h3 class="text-xl md:text-xl/6 font-semibold">
+        {{ $t('partner.agreement.title', { id: agreement.internalId }) }}
+      </h3>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h3 class="text-xl md:text-xl/6 font-semibold">
Договор №{{ agreement.internalId }}
</h3>
<h3 class="text-xl md:text-xl/6 font-semibold">
{{ $t('partner.agreement.title', { id: agreement.internalId }) }}
</h3>
🤖 Prompt for AI Agents
In apps/web-app/app/components/PartnerAgreementCard.vue around lines 19 to 21,
the hard-coded Russian text "Договор №" should be replaced with a call to the
internationalization (i18n) system. Move this text to the appropriate i18n
locale file and update the template to use the i18n key instead, ensuring the
text can be localized and maintained properly.


<div>
<p v-if="agreement.concludedAt">
Заключен: {{ format(new Date(agreement.concludedAt), 'd MMMM yyyy', { locale: ru }) }}
</p>
<p v-if="agreement.willEndAt">
Действует до: {{ format(new Date(agreement.willEndAt), 'd MMMM yyyy', { locale: ru }) }}
</p>
<p>Роялти: {{ agreement.royalty }}%</p>
<p>Мин. роялти: {{ agreement.minRoyaltyPerMonth }} ₽ / месяц</p>

<p v-if="agreement.marketingFee">
Маркетинговый сбор: {{ agreement.marketingFee }}%
</p>
<p v-if="agreement.minMarketingFeePerMonth">
Мин. маркетинговый сбор: {{ agreement.minMarketingFeePerMonth }} ₽ / месяц
</p>

<p>Паушальный взнос: {{ agreement.lumpSumPayment }} ₽</p>
</div>
Comment on lines +24 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use internationalization for hard-coded Russian labels.

All the Russian labels should be extracted to i18n locales for consistency and maintainability.

-        <p v-if="agreement.concludedAt">
-          Заключен: {{ format(new Date(agreement.concludedAt), 'd MMMM yyyy', { locale: ru }) }}
-        </p>
-        <p v-if="agreement.willEndAt">
-          Действует до: {{ format(new Date(agreement.willEndAt), 'd MMMM yyyy', { locale: ru }) }}
-        </p>
-        <p>Роялти: {{ agreement.royalty }}%</p>
-        <p>Мин. роялти: {{ agreement.minRoyaltyPerMonth }} ₽ / месяц</p>
+        <p v-if="agreement.concludedAt">
+          {{ $t('partner.agreement.concluded') }}: {{ format(new Date(agreement.concludedAt), 'd MMMM yyyy', { locale: ru }) }}
+        </p>
+        <p v-if="agreement.willEndAt">
+          {{ $t('partner.agreement.validUntil') }}: {{ format(new Date(agreement.willEndAt), 'd MMMM yyyy', { locale: ru }) }}
+        </p>
+        <p>{{ $t('partner.agreement.royalty') }}: {{ agreement.royalty }}%</p>
+        <p>{{ $t('partner.agreement.minRoyalty') }}: {{ agreement.minRoyaltyPerMonth }} ₽ / {{ $t('common.month') }}</p>
🤖 Prompt for AI Agents
In apps/web-app/app/components/PartnerAgreementCard.vue between lines 24 and 41,
the Russian text labels are hard-coded directly in the template. To fix this,
replace all these hard-coded Russian strings with references to i18n locale keys
using the appropriate translation function or directive. Extract each label into
the i18n locale files to ensure consistency and maintainability across the app.


<p class="text-muted">
{{ agreement.comment }}
</p>
</div>
</UCard>
<CreateCard
v-else
label="Добавить договор"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use internationalization for create card label.

The hard-coded Russian text should use i18n for consistency.

-    label="Добавить договор"
+    :label="$t('partner.agreement.add')"
🤖 Prompt for AI Agents
In apps/web-app/app/components/PartnerAgreementCard.vue at line 50, replace the
hard-coded Russian label "Добавить договор" with a call to the
internationalization (i18n) function or directive used in the project. This
involves using the appropriate i18n syntax to reference a translation key
instead of the literal string, ensuring the label is localized consistently
across the app.

icon="i-lucide-scroll-text"
@click="modalCreatePartnerAgreement.open({ partnerId, legalEntityId })"
/>
</template>

<script setup lang="ts">
import type { PartnerAgreement } from '@roll-stack/database'
import { ModalCreatePartnerAgreement } from '#components'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale/ru'

const { agreement } = defineProps<{ partnerId: string, legalEntityId: string, agreement: PartnerAgreement | null | undefined }>()

const overlay = useOverlay()
const modalCreatePartnerAgreement = overlay.create(ModalCreatePartnerAgreement)

const agreementProgress = computed(() => {
if (!agreement?.willEndAt || !agreement?.concludedAt) {
return 0
}

const now = new Date()
const concludedAt = new Date(agreement.concludedAt)
const willEndAt = new Date(agreement.willEndAt)

return Math.floor(100 - ((now.getTime() - concludedAt.getTime()) / (willEndAt.getTime() - concludedAt.getTime())) * 100)
})
Comment on lines +67 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix progress calculation logic and add bounds checking.

The progress calculation has several issues:

  1. The logic appears inverted - it calculates remaining time percentage but shows it as progress
  2. No bounds checking - can return negative values or values over 100
  3. No handling for edge cases where dates might be invalid
 const agreementProgress = computed(() => {
   if (!agreement?.willEndAt || !agreement?.concludedAt) {
     return 0
   }

   const now = new Date()
   const concludedAt = new Date(agreement.concludedAt)
   const willEndAt = new Date(agreement.willEndAt)

-  return Math.floor(100 - ((now.getTime() - concludedAt.getTime()) / (willEndAt.getTime() - concludedAt.getTime())) * 100)
+  // Validate dates
+  if (concludedAt > willEndAt) {
+    return 0
+  }
+
+  const totalDuration = willEndAt.getTime() - concludedAt.getTime()
+  const elapsed = now.getTime() - concludedAt.getTime()
+  
+  // Calculate elapsed percentage (0-100)
+  const progress = Math.floor((elapsed / totalDuration) * 100)
+  
+  // Clamp between 0 and 100
+  return Math.max(0, Math.min(100, progress))
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const agreementProgress = computed(() => {
if (!agreement?.willEndAt || !agreement?.concludedAt) {
return 0
}
const now = new Date()
const concludedAt = new Date(agreement.concludedAt)
const willEndAt = new Date(agreement.willEndAt)
return Math.floor(100 - ((now.getTime() - concludedAt.getTime()) / (willEndAt.getTime() - concludedAt.getTime())) * 100)
})
const agreementProgress = computed(() => {
if (!agreement?.willEndAt || !agreement?.concludedAt) {
return 0
}
const now = new Date()
const concludedAt = new Date(agreement.concludedAt)
const willEndAt = new Date(agreement.willEndAt)
// Validate dates
if (concludedAt > willEndAt) {
return 0
}
const totalDuration = willEndAt.getTime() - concludedAt.getTime()
const elapsed = now.getTime() - concludedAt.getTime()
// Calculate elapsed percentage (0-100)
const progress = Math.floor((elapsed / totalDuration) * 100)
// Clamp between 0 and 100
return Math.max(0, Math.min(100, progress))
})
🤖 Prompt for AI Agents
In apps/web-app/app/components/PartnerAgreementCard.vue around lines 67 to 77,
the progress calculation logic is incorrect because it calculates remaining time
as progress, lacks bounds checking, and does not handle invalid dates. Fix this
by calculating elapsed time percentage as progress, clamp the result between 0
and 100 to avoid negative or over 100 values, and add checks to ensure the date
values are valid before performing calculations.

</script>
7 changes: 6 additions & 1 deletion apps/web-app/app/components/PartnerCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
:class="{ 'opacity-75 grayscale group-hover:grayscale-0 group-hover:opacity-100': imagesMode === 'grayscale' }"
>

<div class="absolute top-2 left-0 right-0 w-full">
<div
class="absolute top-2 left-0 right-0 w-full opacity-0 group-hover:opacity-100 duration-200"
:class="[
agreementProgress <= 15 && 'opacity-100',
]"
>
<div class="mx-2 px-2 py-1 bg-default/97 rounded-lg flex flex-row items-center gap-1.5">
<UIcon name="i-lucide-scroll-text" class="shrink-0 size-5 text-secondary" />

Expand Down
36 changes: 36 additions & 0 deletions apps/web-app/app/components/PartnerLegalEntityCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<template>
<UCard v-if="entity" class="h-full">
<div class="flex flex-col gap-2.5">
<UIcon name="i-lucide-scale" class="size-16 text-muted/25" />

<h3 class="text-xl md:text-xl/6 font-semibold">
{{ entity.name }}
</h3>

<div>
<p>ИНН {{ entity.inn }}</p>
<p>ОГРНИП {{ entity.ogrnip }}</p>
</div>

<p class="text-muted">
{{ entity.comment }}
</p>
</div>
</UCard>
<CreateCard
v-else
label="Добавить юридическое лицо"
icon="i-lucide-scale"
@click="modalCreatePartnerLegalEntity.open({ partnerId })"
/>
</template>

<script setup lang="ts">
import type { PartnerLegalEntity } from '@roll-stack/database'
import { ModalCreatePartnerLegalEntity } from '#components'

defineProps<{ partnerId: string, entity: PartnerLegalEntity | null | undefined }>()

const overlay = useOverlay()
const modalCreatePartnerLegalEntity = overlay.create(ModalCreatePartnerLegalEntity)
</script>
168 changes: 168 additions & 0 deletions apps/web-app/app/components/form/CreatePartnerAgreement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<template>
<UForm
:validate="createValidator(createPartnerAgreementSchema)"
:state="state"
class="flex flex-col gap-3"
@submit="onSubmit"
>
<UPopover>
<UFormField
label="Дата заключения"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use internationalization consistently for all labels.

Some form labels are hard-coded in Russian while others use i18n. This should be consistent throughout the form.

-        label="Дата заключения"
+        :label="$t('partner.agreement.form.concludedAt')"

-        label="Дата окончания"
+        :label="$t('partner.agreement.form.willEndAt')"

-    <UFormField label="Номер договора (внутренний)" name="internalId">
+    <UFormField :label="$t('partner.agreement.form.internalId')" name="internalId">

-    <UFormField label="Роялти, %" name="royalty">
+    <UFormField :label="$t('partner.agreement.form.royalty')" name="royalty">

-    <UFormField label="Мин. роялти, руб" name="minRoyaltyPerMonth">
+    <UFormField :label="$t('partner.agreement.form.minRoyalty')" name="minRoyaltyPerMonth">

-    <UFormField label="Паушальный взнос, руб" name="lumpSumPayment">
+    <UFormField :label="$t('partner.agreement.form.lumpSumPayment')" name="lumpSumPayment">

Also applies to: 30-30, 48-48, 56-56, 66-66, 75-75

🤖 Prompt for AI Agents
In apps/web-app/app/components/form/CreatePartnerAgreement.vue at lines 10, 30,
48, 56, 66, and 75, the form labels are hard-coded in Russian instead of using
the internationalization (i18n) system. Replace these hard-coded label strings
with the appropriate i18n translation keys by using the $t function or
equivalent i18n method to ensure consistent localization across the form.

name="concludedAt"
required
>
<UInput
:value="selectedConcludedAt ? df.format(selectedConcludedAt.toDate(getLocalTimeZone())) : ''"
placeholder="Выберите дату"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use internationalization for placeholder text.

Placeholder text should also use i18n for consistency.

-          placeholder="Выберите дату"
+          :placeholder="$t('common.selectDate')"

-        placeholder="Для внутреннего использования"
+        :placeholder="$t('partner.agreement.form.commentPlaceholder')"

Also applies to: 36-36, 88-88

🤖 Prompt for AI Agents
In apps/web-app/app/components/form/CreatePartnerAgreement.vue at lines 16, 36,
and 88, the placeholder text is hardcoded in Russian. Replace these hardcoded
placeholder strings with calls to the internationalization (i18n) function to
ensure the text is localized consistently across the app. Use the appropriate
i18n key for each placeholder and update the template bindings accordingly.

size="xl"
class="w-full items-center justify-center cursor-pointer"
:ui="{ trailing: 'pe-1.5' }"
/>
</UFormField>

<template #content>
<UCalendar v-model="selectedConcludedAt" class="p-2" />
</template>
</UPopover>

<UPopover>
<UFormField
label="Дата окончания"
name="willEndAt"
required
>
<UInput
:value="selectedWillEndAt ? df.format(selectedWillEndAt.toDate(getLocalTimeZone())) : ''"
placeholder="Выберите дату"
size="xl"
class="w-full items-center justify-center cursor-pointer"
:ui="{ trailing: 'pe-1.5' }"
/>
</UFormField>

<template #content>
<UCalendar v-model="selectedWillEndAt" class="p-2" />
</template>
</UPopover>

<UFormField label="Номер договора (внутренний)" name="internalId">
<UInput
v-model="state.internalId"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField label="Роялти, %" name="royalty">
<UInputNumber
v-model="state.royalty"
orientation="vertical"
:step="0.1"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField label="Мин. роялти, руб" name="minRoyaltyPerMonth">
<UInputNumber
v-model="state.minRoyaltyPerMonth"
orientation="vertical"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField label="Паушальный взнос, руб" name="lumpSumPayment">
<UInputNumber
v-model="state.lumpSumPayment"
orientation="vertical"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField :label="$t('common.comment')" name="comment">
<UInput
v-model="state.comment"
size="xl"
placeholder="Для внутреннего использования"
class="w-full items-center justify-center"
/>
</UFormField>

<UButton
type="submit"
variant="solid"
color="secondary"
size="xl"
block
class="mt-3"
:label="$t('common.create')"
/>
</UForm>
</template>

<script setup lang="ts">
import type { CreatePartnerAgreement } from '#shared/services/partner'
import type { CalendarDate } from '@internationalized/date'
import type { FormSubmitEvent } from '@nuxt/ui'
import { createPartnerAgreementSchema } from '#shared/services/partner'
import { DateFormatter, getLocalTimeZone } from '@internationalized/date'

const { partnerId, legalEntityId } = defineProps<{ partnerId: string, legalEntityId: string }>()
const emit = defineEmits(['success', 'submitted'])

const { t } = useI18n()
const actionToast = useActionToast()

const partnerStore = usePartnerStore()

const state = ref<Partial<CreatePartnerAgreement>>({
concludedAt: undefined,
willEndAt: undefined,
internalId: undefined,
royalty: undefined,
minRoyaltyPerMonth: undefined,
lumpSumPayment: undefined,
comment: undefined,
legalEntityId,
})

const df = new DateFormatter('ru-RU', {
dateStyle: 'long',
})

const selectedConcludedAt = shallowRef<CalendarDate | undefined>()
const selectedWillEndAt = shallowRef<CalendarDate | undefined>()

watch(selectedConcludedAt, () => {
state.value.concludedAt = new Date(
`${selectedConcludedAt.value?.toString()} 12:00:00`,
).toISOString()
})
watch(selectedWillEndAt, () => {
state.value.willEndAt = new Date(
`${selectedWillEndAt.value?.toString()} 12:00:00`,
).toISOString()
})
Comment on lines +138 to +147
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve date conversion logic for reliability.

The current date conversion logic manually constructs date strings which can be error-prone and fragile. Consider using proper date conversion methods.

 watch(selectedConcludedAt, () => {
-  state.value.concludedAt = new Date(
-    `${selectedConcludedAt.value?.toString()} 12:00:00`,
-  ).toISOString()
+  if (selectedConcludedAt.value) {
+    state.value.concludedAt = selectedConcludedAt.value.toDate(getLocalTimeZone()).toISOString()
+  }
 })
 watch(selectedWillEndAt, () => {
-  state.value.willEndAt = new Date(
-    `${selectedWillEndAt.value?.toString()} 12:00:00`,
-  ).toISOString()
+  if (selectedWillEndAt.value) {
+    state.value.willEndAt = selectedWillEndAt.value.toDate(getLocalTimeZone()).toISOString()
+  }
 })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web-app/app/components/form/CreatePartnerAgreement.vue around lines 138
to 147, the date conversion manually constructs date strings by appending
"12:00:00" which is fragile and error-prone. Instead, update the watchers to
convert the selected date values to Date objects directly using reliable methods
like setting the time explicitly or using date libraries if available, then
convert to ISO string. This will ensure consistent and accurate date handling.


async function onSubmit(event: FormSubmitEvent<CreatePartnerAgreement>) {
const toastId = actionToast.start()
emit('submitted')

try {
await $fetch(`/api/partner/id/${partnerId}/agreement`, {
method: 'POST',
body: event.data,
})

await partnerStore.update()

actionToast.success(toastId, t('toast.partner-agreement-created'))
emit('success')
} catch (error) {
console.error(error)
actionToast.error(toastId)
}
}
</script>
Loading