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/InvoiceCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<UCard class="group/list">
<div class="flex flex-col gap-2.5">
<div class="flex flex-row justify-between">
<UIcon name="i-lucide-banknote-arrow-up" class="size-14 text-primary" />

<UButton
variant="outline"
color="neutral"
size="md"
icon="i-lucide-pencil"
class="size-10 justify-center opacity-0 group-hover/list:opacity-100 transition duration-200"
@click="modalUpdateInvoice.open({ invoiceId: invoice.id })"
/>
</div>

<h3 class="text-xl md:text-xl/6 font-semibold">
{{ new Intl.NumberFormat().format(invoice.total) }} ₽
</h3>

<p class="text-base/5">
{{ invoice.title }}
</p>

<p class="text-sm/4 text-muted">
{{ invoice.description }}
</p>

<div class="flex flex-row flex-wrap gap-2">
<UBadge
:label="getInfoByType(invoice.type)"
color="neutral"
size="md"
variant="soft"
/>

<UBadge
:label="getInfoByStatus(invoice.status)"
:color="invoice.status === 'unpaid' ? 'error' : 'success'"
size="md"
variant="soft"
/>
</div>
</div>
</UCard>
</template>

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

defineProps<{
invoice: Invoice
}>()

function getInfoByType(type: Invoice['type']) {
switch (type) {
case 'replenishment':
return 'Пополнение'
case 'royalties':
return 'Роялти'
case 'other':
return 'Другое'
}
}
Comment on lines +56 to +65
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 | 🟡 Minor

Add default case to prevent undefined returns.

The switch statement lacks a default case. If an unexpected type value is passed, the function returns undefined, which could cause rendering issues.

Apply this diff:

 function getInfoByType(type: Invoice['type']) {
   switch (type) {
     case 'replenishment':
       return 'Пополнение'
     case 'royalties':
       return 'Роялти'
     case 'other':
       return 'Другое'
+    default:
+      return 'Неизвестно'
   }
 }
📝 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
function getInfoByType(type: Invoice['type']) {
switch (type) {
case 'replenishment':
return 'Пополнение'
case 'royalties':
return 'Роялти'
case 'other':
return 'Другое'
}
}
function getInfoByType(type: Invoice['type']) {
switch (type) {
case 'replenishment':
return 'Пополнение'
case 'royalties':
return 'Роялти'
case 'other':
return 'Другое'
default:
return 'Неизвестно'
}
}
🤖 Prompt for AI Agents
In apps/web-app/app/components/InvoiceCard.vue around lines 56 to 65, the
getInfoByType switch lacks a default branch so it can return undefined for
unknown types; add a default case that returns a safe fallback string (e.g.,
'Неизвестно' or an empty string) so the function always returns a string and
adjust typing if needed to reflect a guaranteed string return.


function getInfoByStatus(status: Invoice['status']) {
switch (status) {
case 'unpaid':
return 'Не оплачен'
case 'paid':
return 'Оплачен'
}
}
Comment on lines +67 to +74
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 | 🟡 Minor

Add default case to prevent undefined returns.

The switch statement lacks a default case. If an unexpected status value is passed, the function returns undefined, which could cause rendering issues.

Apply this diff:

 function getInfoByStatus(status: Invoice['status']) {
   switch (status) {
     case 'unpaid':
       return 'Не оплачен'
     case 'paid':
       return 'Оплачен'
+    default:
+      return 'Неизвестен'
   }
 }
📝 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
function getInfoByStatus(status: Invoice['status']) {
switch (status) {
case 'unpaid':
return 'Не оплачен'
case 'paid':
return 'Оплачен'
}
}
function getInfoByStatus(status: Invoice['status']) {
switch (status) {
case 'unpaid':
return 'Не оплачен'
case 'paid':
return 'Оплачен'
default:
return 'Неизвестен'
}
}
🤖 Prompt for AI Agents
In apps/web-app/app/components/InvoiceCard.vue around lines 67 to 74, the
getInfoByStatus switch lacks a default branch so it can return undefined for
unexpected statuses; add a default case that returns a sensible fallback string
(e.g., an empty string or a localized "Unknown" like 'Неизвестно') so the
function always returns a string and prevents rendering issues.


const overlay = useOverlay()
const modalUpdateInvoice = overlay.create(ModalUpdateInvoice)
</script>
2 changes: 1 addition & 1 deletion apps/web-app/app/components/PartnerAgreementCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
{{ kitchen.address }}
</h3>

<h3 class="text-sm/5">
<h3 class="text-sm/4">
{{ kitchen.city }}
</h3>
</div>
Expand Down
23 changes: 23 additions & 0 deletions apps/web-app/app/components/PartnerBalanceCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<UCard>
<div class="flex flex-col gap-2.5">
<div class="flex flex-row items-start gap-2.5">
<UIcon name="i-lucide-banknote" class="size-14 text-primary" />
</div>

<h3 class="text-xl md:text-xl/6 font-semibold">
Баланс {{ new Intl.NumberFormat().format(balance) }} ₽
</h3>

<p class="text-base/5">
Может уходить в минус, если партнер не будет оплачивать выставленные ему счета.
</p>
</div>
</UCard>
</template>

<script setup lang="ts">
defineProps<{
balance: number
}>()
</script>
2 changes: 1 addition & 1 deletion apps/web-app/app/components/PartnerCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
variant="soft"
size="md"
class="rounded-lg justify-center font-semibold"
:label="`Баланс ${new Intl.NumberFormat().format(partner.balance)} руб`"
:label="`Баланс ${new Intl.NumberFormat().format(partner.balance)} `"
/>

<h3 class="text-sm/4 font-bold">
Expand Down
111 changes: 111 additions & 0 deletions apps/web-app/app/components/form/CreateInvoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<template>
<UForm
:validate="createValidator(createPartnerInvoiceSchema)"
:state="state"
class="flex flex-col gap-3"
@submit="onSubmit"
>
<UFormField
:label="$t('common.title')"
name="title"
required
>
<UInput
v-model="state.title"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField
:label="$t('common.description')"
name="description"
>
<UInput
v-model="state.description"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField label="Тип" name="type">
<USelect
v-model="state.type"
:items="[
{ label: 'Оплата роялти', value: 'royalties' },
{ label: 'Пополнение', value: 'replenishment' },
{ label: 'Другое', value: 'other' },
]"
:placeholder="$t('common.select')"
size="xl"
class="w-full"
/>
</UFormField>

<UFormField
label="Сумма, руб"
name="total"
required
>
<UInputNumber
v-model="state.total"
orientation="vertical"
:step="0.1"
size="xl"
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 { CreatePartnerInvoice } from '#shared/services/partner'
import type { FormSubmitEvent } from '@nuxt/ui'
import { createPartnerInvoiceSchema } from '#shared/services/partner'

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

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

const partnerStore = usePartnerStore()

const state = ref<Partial<CreatePartnerInvoice>>({
title: undefined,
description: undefined,
total: 0,
type: 'royalties',
status: 'unpaid',
})

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

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

await partnerStore.update()

actionToast.success(toastId, t('toast.invoice-created'))
emit('success')
} catch (error) {
console.error(error)
actionToast.error(toastId)
}
}
</script>
132 changes: 132 additions & 0 deletions apps/web-app/app/components/form/UpdateInvoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<UForm
:validate="createValidator(updatePartnerInvoiceSchema)"
:state="state"
class="flex flex-col gap-3"
@submit="onSubmit"
>
<UFormField
label="Статус"
name="status"
required
>
<USelect
v-model="state.status"
:items="[
{ label: 'Не оплачен', value: 'unpaid' },
{ label: 'Полностью оплачен', value: 'paid' },
]"
:placeholder="$t('common.select')"
size="xl"
class="w-full"
/>
</UFormField>

<UFormField
:label="$t('common.title')"
name="title"
required
>
<UInput
v-model="state.title"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField
:label="$t('common.description')"
name="description"
>
<UInput
v-model="state.description"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

<UFormField label="Тип" name="type">
<USelect
v-model="state.type"
:items="[
{ label: 'Оплата роялти', value: 'royalties' },
{ label: 'Пополнение', value: 'replenishment' },
]"
:placeholder="$t('common.select')"
size="xl"
class="w-full"
/>
</UFormField>
Comment on lines +48 to +59
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 | 🟠 Major

Missing 'other' invoice type option.

The UpdateInvoice form only includes 'royalties' and 'replenishment' type options, but the CreateInvoice form (lines 34-38 in apps/web-app/app/components/form/CreateInvoice.vue) includes a third option: 'other'. This discrepancy prevents users from updating invoices that were created with type: 'other'.

Add the missing option:

       <USelect
         v-model="state.type"
         :items="[
           { label: 'Оплата роялти', value: 'royalties' },
           { label: 'Пополнение', value: 'replenishment' },
+          { label: 'Другое', value: 'other' },
         ]"
         :placeholder="$t('common.select')"
         size="xl"
         class="w-full"
       />
📝 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
<UFormField label="Тип" name="type">
<USelect
v-model="state.type"
:items="[
{ label: 'Оплата роялти', value: 'royalties' },
{ label: 'Пополнение', value: 'replenishment' },
]"
:placeholder="$t('common.select')"
size="xl"
class="w-full"
/>
</UFormField>
<UFormField label="Тип" name="type">
<USelect
v-model="state.type"
:items="[
{ label: 'Оплата роялти', value: 'royalties' },
{ label: 'Пополнение', value: 'replenishment' },
{ label: 'Другое', value: 'other' },
]"
:placeholder="$t('common.select')"
size="xl"
class="w-full"
/>
</UFormField>
🤖 Prompt for AI Agents
In apps/web-app/app/components/form/UpdateInvoice.vue around lines 48 to 59, the
USelect items only include 'royalties' and 'replenishment', causing invoices
with type 'other' to be unselectable; add the missing option { label: 'Другое'
(or the same localized label used in CreateInvoice.vue), value: 'other' } to the
items array so the UpdateInvoice form matches CreateInvoice and can update
invoices created with type 'other'.


<UFormField
label="Сумма, руб"
name="total"
required
>
<UInputNumber
v-model="state.total"
orientation="vertical"
:step="0.1"
size="xl"
class="w-full items-center justify-center"
/>
</UFormField>

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

<script setup lang="ts">
import type { UpdatePartnerInvoice } from '#shared/services/partner'
import type { FormSubmitEvent } from '@nuxt/ui'
import { updatePartnerInvoiceSchema } from '#shared/services/partner'

const { invoiceId } = defineProps<{
invoiceId: string
}>()

const emit = defineEmits(['success', 'submitted'])

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

const partnerStore = usePartnerStore()
const invoices = computed(() => partnerStore.partners.flatMap((partner) => partner.invoices))
const invoice = computed(() => invoices.value.find((invoice) => invoice.id === invoiceId))

const state = ref<Partial<UpdatePartnerInvoice>>({
title: invoice.value?.title,
description: invoice.value?.description ?? undefined,
total: invoice.value?.total,
type: invoice.value?.type,
status: invoice.value?.status,
})

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

try {
await $fetch(`/api/partner/invoice/id/${invoiceId}`, {
method: 'PATCH',
body: event.data,
})

await partnerStore.update()

actionToast.success(toastId, t('toast.invoice-updated'))
emit('success')
} catch (error) {
console.error(error)
actionToast.error(toastId)
}
}
</script>
19 changes: 19 additions & 0 deletions apps/web-app/app/components/modal/CreateInvoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<UModal title="Создание счета">
<template #body>
<FormCreateInvoice
:partner-id="partnerId ?? ''"
@submitted="overlay.closeAll"
@success="overlay.closeAll"
/>
</template>
</UModal>
</template>

<script setup lang="ts">
defineProps<{
partnerId?: string
}>()

const overlay = useOverlay()
</script>
Comment on lines +1 to +19
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 | 🔴 Critical

🧩 Analysis chain

Verify the empty string fallback for partnerId.

Line 5 uses partnerId ?? '' to provide an empty string when partnerId is undefined. Ensure that the FormCreateInvoice component and the downstream API endpoint at /api/partner/id/${partnerId}/invoice correctly handle empty strings, or consider passing undefined instead.


🏁 Script executed:

#!/bin/bash
# Check how FormCreateInvoice and the API handle empty partnerId

# Check FormCreateInvoice component for partnerId handling
ast-grep --pattern $'const { partnerId } = defineProps<{ partnerId$$$}>()'

# Check API route parameter usage
rg -n "partnerId" apps/web-app/server/api/partner/id/

Length of output: 857



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full API route to see what happens when partnerId is empty
cat apps/web-app/server/api/partner/id/\[partnerId\]/invoice.post.ts

echo "---"

# Find FormCreateInvoice component and check how it uses partnerId
find apps/web-app -name "*FormCreateInvoice*" -o -name "*form-create-invoice*" | head -20

Length of output: 1036



---

🏁 Script executed:

```shell
#!/bin/bash
# Search more broadly for FormCreateInvoice
rg -n "FormCreateInvoice" apps/web-app --type vue --type ts -l

echo "---"

# Try to find it by searching for component definition
rg -n "defineProps.*partnerId" apps/web-app --type vue

Length of output: 59



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for FormCreateInvoice without type filtering
rg -n "FormCreateInvoice" apps/web-app -l

echo "---"

# Find .vue files with FormCreate in name
fd "FormCreateInvoice" apps/web-app

echo "---"

# Search for any $fetch or API calls involving invoice endpoint
rg -n "invoice" apps/web-app/app/components --context 3 -i

Length of output: 13333


Pass undefined instead of empty string, or add validation guards.

The API endpoint explicitly rejects empty strings: if (!partnerId) returns a 400 error. The current code passes partnerId ?? '' to FormCreateInvoice, which constructs the URL /api/partner/id/${partnerId}/invoice. When partnerId is empty, this fails validation.

Recommended fix: Remove the ?? '' fallback to pass undefined directly. Then, either (1) add a guard in FormCreateInvoice to prevent submission if partnerId is missing, or (2) conditionally show the modal only when partnerId is defined.

🤖 Prompt for AI Agents
In apps/web-app/app/components/modal/CreateInvoice.vue lines 1-19, the template
is passing partnerId ?? '' which sends an empty string to FormCreateInvoice and
causes the API to reject it; remove the `?? ''` fallback so the prop is left
undefined when absent (i.e., pass `:partner-id="partnerId"`), and then either
add a validation guard inside FormCreateInvoice to prevent submission/URL
construction when partnerId is missing, or only render/show this UModal/Form
when partnerId is defined (e.g., conditional rendering) to ensure no request is
made with an empty partnerId.

Loading