Skip to content

Commit 88e5fb8

Browse files
committed
feat: agreements page
1 parent 31d9e1e commit 88e5fb8

12 files changed

Lines changed: 376 additions & 13 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<template>
2+
<div class="flex flex-row gap-1.5 items-center">
3+
<UPopover
4+
v-for="file in files"
5+
:key="file.id"
6+
mode="hover"
7+
:content="{
8+
align: 'center',
9+
side: 'bottom',
10+
sideOffset: 8,
11+
}"
12+
>
13+
<ULink
14+
:to="file.url"
15+
external
16+
target="_blank"
17+
>
18+
<UIcon
19+
:name="getFileData(file).icon"
20+
:class="getFileData(file).class"
21+
class="size-5 hover:scale-110 duration-200"
22+
/>
23+
</ULink>
24+
25+
<template #content>
26+
<div class="h-auto w-56 p-4 flex flex-col gap-2">
27+
<UIcon
28+
:name="getFileData(file).icon"
29+
class="size-10 text-muted/25"
30+
/>
31+
32+
<div class="flex flex-col gap-2.5">
33+
<h4 class="text-base/5">
34+
{{ file.name }}
35+
</h4>
36+
37+
<UButton
38+
size="sm"
39+
variant="outline"
40+
color="neutral"
41+
:to="file.url"
42+
icon="i-lucide-external-link"
43+
external
44+
target="_blank"
45+
class="w-fit"
46+
>
47+
Открыть
48+
</UButton>
49+
</div>
50+
</div>
51+
</template>
52+
</UPopover>
53+
</div>
54+
</template>
55+
56+
<script setup lang="ts">
57+
import type { File } from '@roll-stack/database'
58+
import { ULink } from '#components'
59+
60+
defineProps<{ files: File[] }>()
61+
62+
function getFileData(file: File) {
63+
if (file.name.startsWith('Договор к')) {
64+
return {
65+
type: 'main',
66+
icon: 'i-lucide-book-text',
67+
class: 'text-secondary',
68+
}
69+
}
70+
if (file.name.startsWith('Акт о при')) {
71+
return {
72+
type: 'act',
73+
icon: 'i-lucide-file-text',
74+
class: '',
75+
}
76+
}
77+
78+
return {
79+
type: 'unknown',
80+
icon: 'i-lucide-file',
81+
class: '',
82+
}
83+
}
84+
</script>

apps/web-app/app/components/Navigation.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ const menuItems = computed(() => [
8181
active: route.path.startsWith('/partner'),
8282
badge: partnerStore.partners.length,
8383
},
84+
{
85+
label: 'Реестр договоров',
86+
to: '/agreement',
87+
icon: 'i-lucide-scroll',
88+
active: route.path.startsWith('/agreement'),
89+
},
8490
{
8591
label: 'Отзывы клиентов',
8692
icon: 'i-lucide-star',

apps/web-app/app/components/chart/KitchenChecks.client.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,6 @@ const total = computed(() => {
160160
return avg > 0 && count > 0 ? Math.floor(avg / count) : 0
161161
})
162162
163-
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
164-
165163
function formatDate(date: Date): string {
166164
return ({
167165
daily: format(date, 'd MMMM', { locale: ru }),

apps/web-app/app/components/chart/KitchenRevenue.client.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,6 @@ const lineDashArray = (_: DataRecord, i: number) => [i === 0 ? undefined : 3]
157157
158158
const total = computed(() => data.value.reduce((acc: number, { total }) => acc + total, 0))
159159
160-
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
161-
162160
function formatDate(date: Date): string {
163161
return ({
164162
daily: format(date, 'd MMMM', { locale: ru }),

apps/web-app/app/components/chart/NetworkChecks.client.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,6 @@ const total = computed(() => {
143143
return totalChecks > 0 ? Math.floor(totalRevenue / totalChecks) : 0
144144
})
145145
146-
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
147-
148146
function formatDate(date: Date): string {
149147
return ({
150148
daily: format(date, 'd MMMM', { locale: ru }),

apps/web-app/app/components/chart/NetworkRevenue.client.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,6 @@ const lineDashArray = (_: DataRecord, i: number) => [i === 0 ? undefined : 3]
154154
155155
const total = computed(() => data.value.reduce((acc: number, { total }) => acc + total, 0))
156156
157-
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
158-
159157
function formatDate(date: Date): string {
160158
return ({
161159
daily: format(date, 'd MMMM', { locale: ru }),
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<template>
2+
<Header title="Договора" />
3+
4+
<Content>
5+
<div class="flex flex-wrap items-center justify-between gap-1.5">
6+
<div class="flex flex-row gap-2.5">
7+
<UInput
8+
v-model="filterValue"
9+
placeholder="По номеру"
10+
class="max-w-sm"
11+
icon="i-lucide-search"
12+
/>
13+
</div>
14+
15+
<div class="flex flex-wrap items-center gap-1.5">
16+
<UDropdownMenu
17+
:items="
18+
table?.tableApi
19+
?.getAllColumns()
20+
.filter((column) => column.getCanHide())
21+
.map((column) => ({
22+
label: upperFirst(column.id),
23+
type: 'checkbox' as const,
24+
checked: column.getIsVisible(),
25+
onUpdateChecked(checked: boolean) {
26+
table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
27+
},
28+
onSelect(e?: Event) {
29+
e?.preventDefault()
30+
},
31+
}))
32+
"
33+
:content="{ align: 'end' }"
34+
>
35+
<UButton
36+
:label="$t('common.columns')"
37+
color="neutral"
38+
variant="outline"
39+
trailing-icon="i-lucide-settings-2"
40+
/>
41+
</UDropdownMenu>
42+
</div>
43+
</div>
44+
45+
<UTable
46+
ref="table"
47+
v-model:column-visibility="columnVisibility"
48+
v-model:row-selection="rowSelection"
49+
v-model:pagination="pagination"
50+
v-model:sorting="sorting"
51+
:data="data"
52+
:columns="columns"
53+
:pagination-options="{
54+
getPaginationRowModel: getPaginationRowModel(),
55+
}"
56+
class="shrink-0"
57+
:ui="{
58+
base: 'table-fixed border-separate border-spacing-0',
59+
thead: '[&>tr]:bg-default [&>tr]:after:content-none',
60+
tbody: '[&>tr]:last:[&>td]:border-b-0',
61+
th: 'py-1 bg-elevated/50 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
62+
td: 'border-b border-default [&:has([data-action=true]))]:pr-0',
63+
}"
64+
>
65+
<template #id-cell="{ row }">
66+
{{ row.getValue('id') }}
67+
</template>
68+
<template #internalId-cell="{ row }">
69+
<div class="flex flex-row gap-2 items-center">
70+
<ULink :to="`/agreement/${row.getValue('id')}`" class="text-base font-medium text-highlighted">
71+
{{ row.getValue('internalId') }}
72+
</ULink>
73+
<UIcon
74+
v-if="row.getValue('isActive')"
75+
name="i-lucide-check"
76+
class="size-4 text-secondary"
77+
/>
78+
</div>
79+
</template>
80+
<template #files-cell="{ row }">
81+
<div>
82+
<AgreementFilesBlock :files="row.getValue('files') as File[]" />
83+
</div>
84+
</template>
85+
<template #royalty-cell="{ row }">
86+
{{ row.getValue('royalty') }}
87+
</template>
88+
<template #minRoyaltyPerMonth-cell="{ row }">
89+
{{ formatNumber(row.getValue('minRoyaltyPerMonth')) }}
90+
</template>
91+
<template #marketingFee-cell="{ row }">
92+
{{ row.getValue('marketingFee') }}
93+
</template>
94+
<template #minMarketingFeePerMonth-cell="{ row }">
95+
{{ formatNumber(row.getValue('minMarketingFeePerMonth')) }}
96+
</template>
97+
<template #comment-cell="{ row }">
98+
<div class="text-sm/4 whitespace-pre-wrap max-w-56">
99+
{{ row.getValue('comment') }}
100+
</div>
101+
</template>
102+
<template #action-cell="{ row }">
103+
<div class="flex items-end" data-action="true">
104+
<UDropdownMenu
105+
:items="getDropdownActions(row.original as PartnerAgreement)"
106+
:content="{ align: 'end' }"
107+
class="ml-auto"
108+
>
109+
<UButton
110+
icon="i-lucide-ellipsis-vertical"
111+
color="neutral"
112+
variant="outline"
113+
/>
114+
</UDropdownMenu>
115+
</div>
116+
</template>
117+
</UTable>
118+
119+
<div class="flex items-center justify-between gap-3 border-t border-default pt-4 mt-auto">
120+
<div v-if="table?.tableApi?.getFilteredSelectedRowModel().rows.length" class="text-sm text-muted">
121+
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} {{ t('common.table.rows-selected', table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0) }}
122+
{{ $t('common.table.rows-from') }} {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }}
123+
</div>
124+
<div v-else class="text-sm text-muted">
125+
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} {{ t('common.table.rows', table?.tableApi?.getFilteredRowModel().rows.length || 0) }}
126+
</div>
127+
128+
<div class="flex items-center gap-1.5">
129+
<UPagination
130+
:default-page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
131+
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
132+
:total="table?.tableApi?.getFilteredRowModel().rows.length"
133+
@update:page="(p: number) => table?.tableApi?.setPageIndex(p - 1)"
134+
/>
135+
</div>
136+
</div>
137+
</Content>
138+
</template>
139+
140+
<script setup lang="ts">
141+
import type { DropdownMenuItem, TableColumn } from '@nuxt/ui'
142+
import type { File, PartnerAgreement } from '@roll-stack/database'
143+
import type { PartnerAgreementWithData } from '~/stores/partner'
144+
import { getPaginationRowModel } from '@tanstack/table-core'
145+
import { upperFirst } from 'scule'
146+
147+
const UButton = resolveComponent('UButton')
148+
149+
const { t } = useI18n()
150+
151+
const filterValue = ref('')
152+
153+
const partnerStore = usePartnerStore()
154+
155+
const data = computed<PartnerAgreementWithData[]>(() => {
156+
const finalRows = partnerStore.agreements.filter((k) => k.internalId.toLowerCase().includes(filterValue.value.toLowerCase()))
157+
158+
return finalRows
159+
})
160+
161+
const columnVisibility = ref({
162+
id: false,
163+
isActive: false,
164+
})
165+
const rowSelection = ref()
166+
const pagination = ref({
167+
pageIndex: 0,
168+
pageSize: 100,
169+
})
170+
const sorting = ref([
171+
{
172+
id: 'internalId',
173+
desc: true,
174+
},
175+
])
176+
177+
const columns: Ref<TableColumn<PartnerAgreementWithData>[]> = ref([{
178+
accessorKey: 'id',
179+
header: 'Id',
180+
}, {
181+
accessorKey: 'internalId',
182+
enableSorting: true,
183+
header: ({ column }) => {
184+
const isSorted = column.getIsSorted()
185+
const icon = isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow'
186+
187+
return h(UButton, {
188+
color: 'neutral',
189+
variant: 'ghost',
190+
label: '№ договора',
191+
icon: isSorted ? icon : 'i-lucide-arrow-up-down',
192+
class: '-mx-2.5',
193+
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
194+
})
195+
},
196+
}, {
197+
accessorKey: 'files',
198+
header: 'Файлы / сканы',
199+
}, {
200+
accessorKey: 'royalty',
201+
header: 'Роялти, %',
202+
}, {
203+
accessorKey: 'minRoyaltyPerMonth',
204+
header: 'Мин. роялти',
205+
}, {
206+
accessorKey: 'marketingFee',
207+
header: 'Маркетинговый взнос, %',
208+
}, {
209+
accessorKey: 'minMarketingFeePerMonth',
210+
header: 'Мин. маркетинговый взнос',
211+
}, {
212+
accessorKey: 'comment',
213+
header: 'Комментарий',
214+
}, {
215+
accessorKey: 'isActive',
216+
header: 'Активен',
217+
}, {
218+
id: 'action',
219+
enableSorting: false,
220+
enableHiding: false,
221+
}])
222+
223+
function getDropdownActions(_: PartnerAgreement): DropdownMenuItem[][] {
224+
return [
225+
[
226+
{
227+
type: 'label',
228+
label: t('common.actions'),
229+
},
230+
// {
231+
// label: t('common.open-page'),
232+
// type: 'link',
233+
// to: `/product/${product.id}`,
234+
// icon: 'i-lucide-cooking-pot',
235+
// },
236+
],
237+
]
238+
}
239+
240+
const table = useTemplateRef('table')
241+
242+
useHead({
243+
title: 'Договора',
244+
})
245+
</script>

0 commit comments

Comments
 (0)