-
Notifications
You must be signed in to change notification settings - Fork 0
feat: legal and agreement create actions #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p class="text-muted"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ agreement.comment }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </UCard> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <CreateCard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| v-else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label="Добавить договор" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix progress calculation logic and add bounds checking. The progress calculation has several issues:
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> |
| 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="Дата заключения" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| name="concludedAt" | ||
| required | ||
| > | ||
| <UInput | ||
| :value="selectedConcludedAt ? df.format(selectedConcludedAt.toDate(getLocalTimeZone())) : ''" | ||
| placeholder="Выберите дату" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()
+ }
})
🤖 Prompt for AI Agents |
||
|
|
||
| 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> | ||
There was a problem hiding this comment.
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.
📝 Committable suggestion
🤖 Prompt for AI Agents