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
16 changes: 9 additions & 7 deletions apps/web-app/app/components/PartnerCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,33 +49,35 @@
{{ partner?.legal }}
</p>

<p class="text-sm/4 text-muted line-clamp-3">
<p class="text-sm/4 text-muted line-clamp-4">
{{ partner.city }}
</p>
</div>
</ActiveCard>
</template>

<script setup lang="ts">
import type { Partner, PartnerAgreement, PartnerLegalEntity } from '@roll-stack/database'
import type { Partner } from '@roll-stack/database'
import type { PartnerLegalEntityWithData } from '~/stores/partner'

const { partner } = defineProps<{
partner: Partner & {
legalEntity: PartnerLegalEntity | null
activeAgreement: PartnerAgreement | null
legalEntity: PartnerLegalEntityWithData | null
}
}>()

const { imagesMode } = useApp()

const minimalAgreement = computed(() => partner.legalEntity?.agreements.toSorted((a, b) => new Date(a.willEndAt ?? '').getTime() - new Date(b.willEndAt ?? '').getTime())[0])
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

Potential runtime error with agreement sorting logic.

The minimalAgreement computation has several potential issues:

  1. Null date handling: new Date(a.willEndAt ?? '') will create an invalid date if willEndAt is null/undefined
  2. Empty agreements array: No handling if agreements is empty
  3. Business logic: Using the earliest ending agreement might not always be the most relevant for progress display

Apply this diff to add proper error handling:

-const minimalAgreement = computed(() => partner.legalEntity?.agreements.toSorted((a, b) => new Date(a.willEndAt ?? '').getTime() - new Date(b.willEndAt ?? '').getTime())[0])
+const minimalAgreement = computed(() => {
+  const agreements = partner.legalEntity?.agreements
+  if (!agreements?.length) return null
+  
+  return agreements
+    .filter(agreement => agreement.willEndAt) // Filter out agreements without end dates
+    .toSorted((a, b) => new Date(a.willEndAt!).getTime() - new Date(b.willEndAt!).getTime())[0] || null
+})
📝 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 minimalAgreement = computed(() => partner.legalEntity?.agreements.toSorted((a, b) => new Date(a.willEndAt ?? '').getTime() - new Date(b.willEndAt ?? '').getTime())[0])
const minimalAgreement = computed(() => {
const agreements = partner.legalEntity?.agreements
if (!agreements?.length) return null
return (
agreements
.filter(agreement => agreement.willEndAt) // Filter out agreements without end dates
.toSorted(
(a, b) =>
new Date(a.willEndAt!).getTime() - new Date(b.willEndAt!).getTime()
)[0] || null
)
})
🤖 Prompt for AI Agents
In apps/web-app/app/components/PartnerCard.vue at line 71, the minimalAgreement
computed property can cause runtime errors due to invalid date parsing when
willEndAt is null or undefined, and it does not handle empty agreements arrays.
To fix this, first check if partner.legalEntity and its agreements array exist
and are non-empty. Then filter out agreements with invalid or missing willEndAt
dates before sorting. Finally, adjust the sorting logic to handle only valid
dates and ensure the selected agreement aligns with the intended business logic
for progress display.


const agreementProgress = computed(() => {
if (!partner?.activeAgreement?.willEndAt || !partner?.activeAgreement?.concludedAt) {
if (!minimalAgreement.value?.willEndAt || !minimalAgreement.value?.concludedAt) {
return 0
}

const now = new Date()
const concludedAt = new Date(partner.activeAgreement.concludedAt)
const willEndAt = new Date(partner.activeAgreement.willEndAt)
const concludedAt = new Date(minimalAgreement.value.concludedAt)
const willEndAt = new Date(minimalAgreement.value.willEndAt)

const res = Math.floor(100 - ((now.getTime() - concludedAt.getTime()) / (willEndAt.getTime() - concludedAt.getTime())) * 100)

Expand Down
5 changes: 3 additions & 2 deletions apps/web-app/app/components/form/UpdatePartnerAgreement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<UInputNumber
v-model="state.marketingFee"
orientation="vertical"
:step="0.1"
size="xl"
class="w-full items-center justify-center"
/>
Expand Down Expand Up @@ -87,8 +88,8 @@ const { t } = useI18n()
const actionToast = useActionToast()

const partnerStore = usePartnerStore()
const partner = computed(() => partnerStore.partners.find((partner) => partner.activeAgreementId === agreementId))
const agreement = computed(() => partner.value?.activeAgreement)
const partner = computed(() => partnerStore.partners.find((partner) => partner.legalEntity?.agreements.some((agreement) => agreement.id === agreementId)))
const agreement = computed(() => partner.value?.legalEntity?.agreements.find((agreement) => agreement.id === agreementId))
Comment on lines +91 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify agreement lookup logic and consider performance optimization.

The new agreement lookup logic correctly implements the transition from single active agreement to multiple agreements. However, there are a few considerations:

  1. Performance: The partner lookup uses nested find() and some() operations which could be inefficient with many partners/agreements
  2. Error handling: No explicit handling if the agreement is not found

Consider this more efficient approach:

-const partner = computed(() => partnerStore.partners.find((partner) => partner.legalEntity?.agreements.some((agreement) => agreement.id === agreementId)))
-const agreement = computed(() => partner.value?.legalEntity?.agreements.find((agreement) => agreement.id === agreementId))
+const partnerAndAgreement = computed(() => {
+  for (const partner of partnerStore.partners) {
+    const agreement = partner.legalEntity?.agreements.find(a => a.id === agreementId)
+    if (agreement) {
+      return { partner, agreement }
+    }
+  }
+  return { partner: null, agreement: null }
+})
+const partner = computed(() => partnerAndAgreement.value.partner)
+const agreement = computed(() => partnerAndAgreement.value.agreement)

Please verify that all agreement lookups across the codebase handle the case where agreements might be undefined or empty arrays.


🏁 Script executed:

#!/bin/bash
# Description: Check for potential null/undefined access patterns in agreement lookups
# Expected: Find all places where agreements array is accessed to ensure proper null checking

rg -A 3 -B 1 "\.agreements\." --type vue --type ts
rg -A 3 -B 1 "agreements\[" --type vue --type ts
rg -A 3 -B 1 "agreements\.find" --type vue --type ts
rg -A 3 -B 1 "agreements\.some" --type vue --type ts

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Description: Search for all usages of `agreements` in .vue and .ts files 
# to verify proper null/undefined checks around agreement lookups.

# Search for property access (e.g., partner.legalEntity.agreements)
rg -n "\.agreements" -g "*.vue" -g "*.ts" -A2 -B1

# Search for direct array indexing (e.g., agreements[0])
rg -n "agreements\[" -g "*.vue" -g "*.ts" -A2 -B1

Length of output: 2147


Ensure safe and efficient agreement lookups

Most components guard against missing legalEntity, but we still see un‐handled cases and nested searches that can be optimized.

• apps/web-app/app/components/form/UpdatePartnerAgreement.vue (91–92)
– Two separate find()/some() calls scan the list twice.
– No fallback if no match is found.
• apps/web-app/app/pages/partner/[id].vue (49)
– Uses partner.value?.legalEntity?.agreements.length without default → may yield undefined.
• apps/web-app/app/pages/partner/[id]/index.vue (50–52) & …/agreement.vue (5–7)
v-for="agreement in partner?.legalEntity?.agreements" on undefined can throw.
• apps/web-app/app/components/PartnerCard.vue (71)
partner.legalEntity?.agreements.toSorted(…) calls .toSorted on undefined if agreements is missing.

Suggested fixes:

  1. Consolidate lookup in UpdatePartnerAgreement.vue into a single pass and handle “not found”:
- const partner = computed(() => 
-   partnerStore.partners.find(p => p.legalEntity?.agreements.some(a => a.id === agreementId))
- )
- const agreement = computed(() => 
-   partner.value?.legalEntity?.agreements.find(a => a.id === agreementId)
- )
+ const partnerAndAgreement = computed(() => {
+   for (const p of partnerStore.partners) {
+     const match = p.legalEntity?.agreements?.find(a => a.id === agreementId)
+     if (match) return { partner: p, agreement: match }
+   }
+   return { partner: null, agreement: null }
+ })
+ const partner   = computed(() => partnerAndAgreement.value.partner)
+ const agreement = computed(() => partnerAndAgreement.value.agreement)
  1. Everywhere you iterate or access .agreements, default to an empty array:
- partner?.legalEntity?.agreements
+ partner?.legalEntity?.agreements ?? []

This ensures you never call .length, .find, .some, .toSorted, or v-for on undefined.

📝 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 partner = computed(() => partnerStore.partners.find((partner) => partner.legalEntity?.agreements.some((agreement) => agreement.id === agreementId)))
const agreement = computed(() => partner.value?.legalEntity?.agreements.find((agreement) => agreement.id === agreementId))
const partnerAndAgreement = computed(() => {
for (const p of partnerStore.partners) {
const match = p.legalEntity?.agreements?.find(a => a.id === agreementId)
if (match) return { partner: p, agreement: match }
}
return { partner: null, agreement: null }
})
const partner = computed(() => partnerAndAgreement.value.partner)
const agreement = computed(() => partnerAndAgreement.value.agreement)
🤖 Prompt for AI Agents
In apps/web-app/app/components/form/UpdatePartnerAgreement.vue lines 91-92,
replace the two separate find/some calls with a single iteration that locates
the partner and agreement together in one pass, and add a fallback for when no
match is found to avoid undefined errors. Also, throughout the app wherever
agreements are accessed or iterated, ensure to default to an empty array if
agreements is undefined, so methods like length, find, some, toSorted, or v-for
do not throw errors.


const state = ref<Partial<UpdatePartnerAgreement>>({
royalty: agreement.value?.royalty,
Expand Down
15 changes: 2 additions & 13 deletions apps/web-app/app/pages/partner/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,12 @@
</template>

<script setup lang="ts">
import { format } from 'date-fns'
import { ru } from 'date-fns/locale/ru'

const { t } = useI18n()
const { params } = useRoute('partner-id')

const partnerStore = usePartnerStore()
const partner = computed(() => partnerStore.partners.find((partner) => partner.id === params.id))

const activeAgreementTo = computed(() => {
if (!partner.value?.activeAgreement?.willEndAt) {
return 'отсутствует'
}

return `до ${format(new Date(partner.value?.activeAgreement?.willEndAt), 'd MMMM yyyy', { locale: ru })}`
})

const submenuItems = computed(() => [
{
label: t('common.partner'),
Expand All @@ -54,10 +43,10 @@ const submenuItems = computed(() => [
badge: partner.value?.kitchens.length,
},
{
label: 'Договор',
label: 'Договора',
to: `/partner/${partner.value?.id}/agreement`,
icon: 'i-lucide-scroll-text',
badge: activeAgreementTo.value,
badge: partner.value?.legalEntity?.agreements.length,
},
Comment on lines +46 to 50
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

Good refactoring to plural form and count display.

The changes appropriately reflect the shift from single to multiple agreements:

  • Label changed to plural "Договора"
  • Badge now shows agreement count instead of end date

However, there's a potential null safety issue with partner.value?.legalEntity?.agreements.length. If agreements is undefined, this will throw a runtime error.

-    badge: partner.value?.legalEntity?.agreements.length,
+    badge: partner.value?.legalEntity?.agreements?.length ?? 0,
📝 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
label: 'Договора',
to: `/partner/${partner.value?.id}/agreement`,
icon: 'i-lucide-scroll-text',
badge: activeAgreementTo.value,
badge: partner.value?.legalEntity?.agreements.length,
},
label: 'Договора',
to: `/partner/${partner.value?.id}/agreement`,
icon: 'i-lucide-scroll-text',
badge: partner.value?.legalEntity?.agreements?.length ?? 0,
},
🤖 Prompt for AI Agents
In apps/web-app/app/pages/partner/[id].vue around lines 46 to 50, the badge
property accesses partner.value?.legalEntity?.agreements.length without checking
if agreements is defined, which can cause a runtime error if agreements is
undefined. Fix this by safely accessing the length using optional chaining or a
fallback, for example, replace partner.value?.legalEntity?.agreements.length
with partner.value?.legalEntity?.agreements?.length ?? 0 to ensure it does not
throw when agreements is undefined.

{
label: 'Юр. лицо',
Expand Down
8 changes: 6 additions & 2 deletions apps/web-app/app/pages/partner/[id]/agreement.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<template>
<Content>
<div class="grid grid-cols-1 gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div v-if="partner?.activeAgreement" class="lg:col-span-2">
<PartnerAgreementCard :agreement="partner.activeAgreement" />
<div
v-for="agreement in partner?.legalEntity?.agreements"
:key="agreement.id"
class="lg:col-span-2"
>
<PartnerAgreementCard :agreement="agreement" />
</div>

<div>
Expand Down
12 changes: 8 additions & 4 deletions apps/web-app/app/pages/partner/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{{ partner?.priceLevel }} уровень цен
</p>

<p class="text-base">
<p class="text-base/5">
{{ partner?.city }}
</p>
</div>
Expand All @@ -36,7 +36,7 @@
Престиж
</h3>
</div>
<p class="text-muted leading-5">
<p class="text-base/5">
Престиж не является статичным - он может как укрепляться, так и утрачиваться в зависимости от действий Партнера, его достижений и общественного восприятия.
</p>
</div>
Expand All @@ -46,8 +46,12 @@
<PartnerLegalEntityCard :partner-id="partner?.id ?? ''" :entity="partner?.legalEntity" />
</div>

<div v-if="partner?.activeAgreement" class="lg:col-span-2">
<PartnerAgreementCard :agreement="partner.activeAgreement" />
<div
v-for="agreement in partner?.legalEntity?.agreements"
:key="agreement.id"
class="lg:col-span-2"
>
<PartnerAgreementCard :agreement="agreement" />
</div>
</div>
</Content>
Expand Down
7 changes: 5 additions & 2 deletions apps/web-app/app/stores/partner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Kitchen, Partner, PartnerAgreement, PartnerLegalEntity } from '@roll-stack/database'

export type PartnerLegalEntityWithData = PartnerLegalEntity & {
agreements: PartnerAgreement[]
}

type PartnerWithData = Partner & {
kitchens: Kitchen[]
legalEntity: PartnerLegalEntity | null
activeAgreement: PartnerAgreement | null
legalEntity: PartnerLegalEntityWithData | null
}

export const usePartnerStore = defineStore('partner', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ export default defineEventHandler(async (event) => {
})
}

// Update
await repository.partner.update(partnerId, {
activeAgreementId: agreement.id,
})

return {
ok: true,
result: agreement,
Expand Down
7 changes: 5 additions & 2 deletions packages/database/src/repository/partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ export class Partner {
where: (partners, { eq }) => eq(partners.isActive, true),
with: {
kitchens: true,
legalEntity: true,
activeAgreement: true,
legalEntity: {
with: {
agreements: true,
},
},
},
})
}
Expand Down
5 changes: 0 additions & 5 deletions packages/database/src/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export const partners = pgTable('partners', {
city: varchar('city'),
legal: varchar('legal'),
legalEntityId: cuid2('legal_entity_id').references(() => partnerLegalEntities.id),
activeAgreementId: cuid2('active_agreement_id').references(() => partnerAgreements.id),
})

export const partnerLegalEntities = pgTable('partner_legal_entities', {
Expand Down Expand Up @@ -573,10 +572,6 @@ export const partnerRelations = relations(partners, ({ many, one }) => ({
fields: [partners.legalEntityId],
references: [partnerLegalEntities.id],
}),
activeAgreement: one(partnerAgreements, {
fields: [partners.activeAgreementId],
references: [partnerAgreements.id],
}),
}))

export const partnerLegalEntityRelations = relations(partnerLegalEntities, ({ many }) => ({
Expand Down