@hed-hog/operations 0.0.300 → 0.0.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/operations.controller.d.ts +713 -31
  2. package/dist/operations.controller.d.ts.map +1 -1
  3. package/dist/operations.controller.js +157 -0
  4. package/dist/operations.controller.js.map +1 -1
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +5 -1
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.proposal.subscriber.d.ts +11 -0
  9. package/dist/operations.proposal.subscriber.d.ts.map +1 -0
  10. package/dist/operations.proposal.subscriber.js +80 -0
  11. package/dist/operations.proposal.subscriber.js.map +1 -0
  12. package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
  13. package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
  14. package/dist/operations.proposal.subscriber.spec.js +88 -0
  15. package/dist/operations.proposal.subscriber.spec.js.map +1 -0
  16. package/dist/operations.service.d.ts +490 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +2442 -119
  19. package/dist/operations.service.js.map +1 -1
  20. package/dist/operations.service.spec.d.ts +2 -0
  21. package/dist/operations.service.spec.d.ts.map +1 -0
  22. package/dist/operations.service.spec.js +159 -0
  23. package/dist/operations.service.spec.js.map +1 -0
  24. package/hedhog/data/menu.yaml +34 -0
  25. package/hedhog/data/role_route.yaml +39 -0
  26. package/hedhog/data/route.yaml +130 -0
  27. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  28. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  29. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  30. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  31. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  32. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  33. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  34. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  35. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  36. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  37. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  38. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  39. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  40. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  41. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  42. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  43. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  44. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  45. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  46. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  48. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  49. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  51. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  52. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  53. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  54. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  55. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  57. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  58. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  59. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  60. package/hedhog/frontend/messages/en.json +473 -12
  61. package/hedhog/frontend/messages/pt.json +528 -66
  62. package/hedhog/table/operations_collaborator.yaml +20 -0
  63. package/hedhog/table/operations_contract.yaml +22 -1
  64. package/hedhog/table/operations_contract_document.yaml +33 -16
  65. package/hedhog/table/operations_contract_template.yaml +58 -0
  66. package/hedhog/table/operations_department.yaml +24 -0
  67. package/package.json +6 -4
  68. package/src/operations.controller.ts +122 -0
  69. package/src/operations.module.ts +6 -2
  70. package/src/operations.proposal.subscriber.spec.ts +121 -0
  71. package/src/operations.proposal.subscriber.ts +86 -0
  72. package/src/operations.service.spec.ts +210 -0
  73. package/src/operations.service.ts +3934 -212
@@ -1,262 +1,941 @@
1
- 'use client';
2
-
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from '@/components/ui/table';
13
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
14
- import { Download, Eye, FileText, Pencil, Upload } from 'lucide-react';
15
- import Link from 'next/link';
16
- import { useMemo, useRef, useState } from 'react';
17
- import { useTranslations } from 'next-intl';
18
- import { OperationsHeader } from '../_components/operations-header';
19
- import { StatusBadge } from '../_components/status-badge';
20
- import { fetchOperations, mutateOperations } from '../_lib/api';
21
- import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
22
- import type { OperationsContract, OperationsContractDetails } from '../_lib/types';
23
- import {
24
- formatCurrency,
25
- formatDate,
26
- formatEnumLabel,
27
- getStatusBadgeClass,
28
- } from '../_lib/utils/format';
29
-
30
- function downloadBase64File(fileName: string, mimeType: string, base64: string) {
31
- const href = `data:${mimeType};base64,${base64}`;
32
- const link = document.createElement('a');
33
- link.href = href;
34
- link.download = fileName;
35
- link.click();
36
- }
37
-
38
- async function fileToBase64(file: File) {
39
- return new Promise<string>((resolve, reject) => {
40
- const reader = new FileReader();
41
- reader.onload = () => {
42
- const result = String(reader.result ?? '');
43
- const [, base64 = ''] = result.split(',');
44
- resolve(base64);
45
- };
46
- reader.onerror = reject;
47
- reader.readAsDataURL(file);
48
- });
49
- }
50
-
51
- export default function OperationsContractsPage() {
52
- const t = useTranslations('operations.ContractsPage');
53
- const commonT = useTranslations('operations.Common');
54
- const { request, showToastHandler, currentLocaleCode } = useApp();
55
- const access = useOperationsAccess();
56
- const [search, setSearch] = useState('');
57
- const [statusFilter, setStatusFilter] = useState('all');
58
- const [categoryFilter, setCategoryFilter] = useState('all');
59
- const [originFilter, setOriginFilter] = useState('all');
60
- const uploadTargetRef = useRef<number | null>(null);
61
- const fileInputRef = useRef<HTMLInputElement>(null);
62
-
63
- const { data: contracts = [], refetch } = useQuery<OperationsContract[]>({
64
- queryKey: ['operations-contracts-list', currentLocaleCode],
65
- queryFn: () => fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
66
- });
67
-
68
- const filteredRows = useMemo(
69
- () =>
70
- contracts.filter((item) => {
71
- const matchesSearch = !search.trim()
72
- ? true
73
- : [
74
- item.name,
75
- item.code,
76
- item.mainRelatedPartyName,
77
- item.clientName,
78
- item.contractType,
79
- ]
80
- .filter(Boolean)
81
- .some((value) =>
82
- String(value).toLowerCase().includes(search.trim().toLowerCase())
83
- );
84
- const matchesStatus = statusFilter === 'all' ? true : item.status === statusFilter;
85
- const matchesCategory =
86
- categoryFilter === 'all' ? true : item.contractCategory === categoryFilter;
87
- const matchesOrigin =
88
- originFilter === 'all' ? true : item.originType === originFilter;
89
- return matchesSearch && matchesStatus && matchesCategory && matchesOrigin;
90
- }),
91
- [contracts, search, statusFilter, categoryFilter, originFilter]
92
- );
93
-
94
- const downloadPdf = async (contractId: number) => {
95
- const detail = await fetchOperations<OperationsContractDetails>(
96
- request,
97
- `/operations/contracts/${contractId}`
98
- );
99
- const document = detail.documents.find((item) => item.isCurrent && item.fileContentBase64);
100
- if (!document?.fileContentBase64) {
101
- showToastHandler?.('error', t('messages.noPdf'));
102
- return;
103
- }
104
- downloadBase64File(document.fileName, document.mimeType, document.fileContentBase64);
105
- };
106
-
107
- const duplicateContract = async (contractId: number) => {
108
- window.location.href = `/operations/contracts/new?duplicate=${contractId}`;
109
- };
110
-
111
- const toggleArchived = async (contract: OperationsContract) => {
112
- try {
113
- await mutateOperations(request, `/operations/contracts/${contract.id}`, 'PATCH', {
114
- status: contract.status === 'archived' ? 'active' : 'archived',
115
- isActive: contract.status === 'archived',
116
- });
117
- showToastHandler?.('success', t('messages.statusSuccess'));
118
- await refetch();
119
- } catch {
120
- showToastHandler?.('error', t('messages.statusError'));
121
- }
122
- };
123
-
124
- return (
125
- <Page>
126
- <OperationsHeader
127
- title={t('title')}
128
- description={t('description')}
129
- current={t('breadcrumb')}
130
- actions={
131
- access.isDirector ? (
132
- <Button size="sm" asChild>
133
- <Link href="/operations/contracts/new">{commonT('actions.create')}</Link>
134
- </Button>
135
- ) : undefined
136
- }
137
- />
138
-
139
- <SearchBar
140
- searchQuery={search}
141
- onSearchChange={setSearch}
142
- onSearch={() => undefined}
143
- placeholder={t('searchPlaceholder')}
144
- controls={[
145
- {
146
- id: 'category',
147
- type: 'select',
148
- value: categoryFilter,
149
- onChange: setCategoryFilter,
150
- placeholder: commonT('labels.contractCategory'),
151
- options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['employee','contractor','client','supplier','vendor','partner','internal','other'].map((value) => ({ value, label: formatEnumLabel(value) }))],
152
- },
153
- {
154
- id: 'origin',
155
- type: 'select',
156
- value: originFilter,
157
- onChange: setOriginFilter,
158
- placeholder: t('filters.originType'),
159
- options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['manual','employee_hiring','client_project'].map((value) => ({ value, label: formatEnumLabel(value) }))],
160
- },
161
- {
162
- id: 'status',
163
- type: 'select',
164
- value: statusFilter,
165
- onChange: setStatusFilter,
166
- placeholder: commonT('labels.status'),
167
- options: [{ value: 'all', label: commonT('filters.allStatuses') }, ...['draft','under_review','active','renewal','expired','closed','archived'].map((value) => ({ value, label: formatEnumLabel(value) }))],
168
- },
169
- ]}
170
- />
171
-
172
- <input
173
- ref={fileInputRef}
174
- type="file"
175
- accept="application/pdf"
176
- className="hidden"
177
- onChange={async (event) => {
178
- const file = event.target.files?.[0];
179
- const contractId = uploadTargetRef.current;
180
- if (!file || !contractId) return;
181
- try {
182
- const fileContentBase64 = await fileToBase64(file);
183
- await mutateOperations(request, `/operations/contracts/${contractId}`, 'PATCH', {
184
- replaceUploadedPdfDocument: {
185
- fileName: file.name,
186
- mimeType: file.type || 'application/pdf',
187
- fileContentBase64,
188
- },
189
- });
190
- showToastHandler?.('success', t('messages.uploadSuccess'));
191
- await refetch();
192
- } catch {
193
- showToastHandler?.('error', t('messages.uploadError'));
194
- } finally {
195
- uploadTargetRef.current = null;
196
- event.target.value = '';
197
- }
198
- }}
199
- />
200
-
201
- {filteredRows.length ? (
202
- <div className="overflow-x-auto rounded-md border">
203
- <Table>
204
- <TableHeader>
205
- <TableRow>
206
- <TableHead>{t('columns.title')}</TableHead>
207
- <TableHead>{t('columns.code')}</TableHead>
208
- <TableHead>{commonT('labels.contractCategory')}</TableHead>
209
- <TableHead>{t('columns.type')}</TableHead>
210
- <TableHead>{t('columns.origin')}</TableHead>
211
- <TableHead>{t('columns.party')}</TableHead>
212
- <TableHead>{commonT('labels.startDate')}</TableHead>
213
- <TableHead>{commonT('labels.endDate')}</TableHead>
214
- <TableHead>{t('columns.signatureStatus')}</TableHead>
215
- <TableHead>{t('columns.active')}</TableHead>
216
- <TableHead>{t('columns.financials')}</TableHead>
217
- <TableHead>{commonT('labels.status')}</TableHead>
218
- <TableHead className="w-[320px] text-right">{commonT('labels.actions')}</TableHead>
219
- </TableRow>
220
- </TableHeader>
221
- <TableBody>
222
- {filteredRows.map((contract) => (
223
- <TableRow key={contract.id}>
224
- <TableCell>{contract.name}</TableCell>
225
- <TableCell>{contract.code}</TableCell>
226
- <TableCell>{formatEnumLabel(contract.contractCategory)}</TableCell>
227
- <TableCell>{formatEnumLabel(contract.contractType)}</TableCell>
228
- <TableCell>{formatEnumLabel(contract.originType)}</TableCell>
229
- <TableCell>{contract.mainRelatedPartyName || commonT('labels.notAvailable')}</TableCell>
230
- <TableCell>{formatDate(contract.startDate)}</TableCell>
231
- <TableCell>{formatDate(contract.endDate)}</TableCell>
232
- <TableCell><StatusBadge label={formatEnumLabel(contract.signatureStatus)} className={getStatusBadgeClass(contract.signatureStatus)} /></TableCell>
233
- <TableCell>{contract.isActive ? t('labels.active') : t('labels.inactive')}</TableCell>
234
- <TableCell>{formatCurrency((contract.valueAmount ?? 0) + (contract.revenueAmount ?? 0))}</TableCell>
235
- <TableCell><StatusBadge label={formatEnumLabel(contract.status)} className={getStatusBadgeClass(contract.status)} /></TableCell>
236
- <TableCell>
237
- <div className="flex justify-end gap-2">
238
- <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}`}><Eye className="size-4" /></Link></Button>
239
- {access.isDirector ? <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}/edit`}><Pencil className="size-4" /></Link></Button> : null}
240
- <Button variant="outline" size="icon" onClick={() => void downloadPdf(contract.id)}><Download className="size-4" /></Button>
241
- {access.isDirector ? <Button variant="outline" size="icon" onClick={() => { uploadTargetRef.current = contract.id; fileInputRef.current?.click(); }}><Upload className="size-4" /></Button> : null}
242
- {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void duplicateContract(contract.id)}>{t('actions.duplicate')}</Button> : null}
243
- {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void toggleArchived(contract)}>{contract.status === 'archived' ? commonT('actions.activate') : t('actions.archive')}</Button> : null}
244
- </div>
245
- </TableCell>
246
- </TableRow>
247
- ))}
248
- </TableBody>
249
- </Table>
250
- </div>
251
- ) : (
252
- <EmptyState
253
- icon={<FileText className="size-12" />}
254
- title={commonT('states.emptyTitle')}
255
- description={t('emptyDescription')}
256
- actionLabel={access.isDirector ? commonT('actions.create') : commonT('actions.refresh')}
257
- onAction={access.isDirector ? () => { window.location.href = '/operations/contracts/new'; } : () => void refetch()}
258
- />
259
- )}
260
- </Page>
261
- );
262
- }
1
+ 'use client';
2
+
3
+ import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from '@/components/ui/alert-dialog';
14
+ import { Button } from '@/components/ui/button';
15
+ import { Card, CardContent } from '@/components/ui/card';
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuSeparator,
21
+ DropdownMenuTrigger,
22
+ } from '@/components/ui/dropdown-menu';
23
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
24
+ import {
25
+ Sheet,
26
+ SheetContent,
27
+ SheetDescription,
28
+ SheetHeader,
29
+ SheetTitle,
30
+ } from '@/components/ui/sheet';
31
+ import {
32
+ Table,
33
+ TableBody,
34
+ TableCell,
35
+ TableHead,
36
+ TableHeader,
37
+ TableRow,
38
+ } from '@/components/ui/table';
39
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
40
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
41
+ import {
42
+ Download,
43
+ FileStack,
44
+ FileText,
45
+ LayoutGrid,
46
+ List,
47
+ MoreHorizontal,
48
+ Pencil,
49
+ Sparkles,
50
+ Trash2,
51
+ Upload,
52
+ } from 'lucide-react';
53
+ import { useTranslations } from 'next-intl';
54
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
55
+ import { useMemo, useRef, useState } from 'react';
56
+ import { ContractWizardSheet } from '../_components/contract-wizard-sheet';
57
+ import { OperationsHeader } from '../_components/operations-header';
58
+ import { StatusBadge } from '../_components/status-badge';
59
+ import { fetchOperations, mutateOperations } from '../_lib/api';
60
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
61
+ import type {
62
+ OperationsContract,
63
+ OperationsContractDetails,
64
+ } from '../_lib/types';
65
+ import {
66
+ formatCurrency,
67
+ formatDate,
68
+ formatEnumLabel,
69
+ getStatusBadgeClass,
70
+ } from '../_lib/utils/format';
71
+
72
+ function downloadBase64File(
73
+ fileName: string,
74
+ mimeType: string,
75
+ base64: string
76
+ ) {
77
+ const href = `data:${mimeType};base64,${base64}`;
78
+ const link = document.createElement('a');
79
+ link.href = href;
80
+ link.download = fileName;
81
+ link.click();
82
+ }
83
+
84
+ const CONTRACT_VIEW_STORAGE_KEY = 'operations-contracts-view-mode';
85
+
86
+ type ContractViewMode = 'table' | 'cards';
87
+
88
+ function openStoredFile(fileId?: number | null) {
89
+ if (!fileId) return;
90
+ const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
91
+ window.open(
92
+ `${baseUrl}/file/open/${fileId}`,
93
+ '_blank',
94
+ 'noopener,noreferrer'
95
+ );
96
+ }
97
+
98
+ async function fileToBase64(file: File) {
99
+ return new Promise<string>((resolve, reject) => {
100
+ const reader = new FileReader();
101
+ reader.onload = () => {
102
+ const result = String(reader.result ?? '');
103
+ const [, base64 = ''] = result.split(',');
104
+ resolve(base64);
105
+ };
106
+ reader.onerror = reject;
107
+ reader.readAsDataURL(file);
108
+ });
109
+ }
110
+
111
+ export default function OperationsContractsPage() {
112
+ const t = useTranslations('operations.ContractsPage');
113
+ const commonT = useTranslations('operations.Common');
114
+ const formT = useTranslations('operations.ContractFormPage');
115
+ const { request, showToastHandler, currentLocaleCode } = useApp();
116
+ const access = useOperationsAccess();
117
+ const router = useRouter();
118
+ const pathname = usePathname();
119
+ const searchParams = useSearchParams();
120
+ const [search, setSearch] = useState('');
121
+ const [statusFilter, setStatusFilter] = useState('all');
122
+ const [categoryFilter, setCategoryFilter] = useState('all');
123
+ const [originFilter, setOriginFilter] = useState('all');
124
+ const uploadTargetRef = useRef<number | null>(null);
125
+ const fileInputRef = useRef<HTMLInputElement>(null);
126
+ const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false);
127
+ const [contractToDelete, setContractToDelete] =
128
+ useState<OperationsContract | null>(null);
129
+ const [isDeletingContract, setIsDeletingContract] = useState(false);
130
+ const [viewMode, setViewMode] = useState<ContractViewMode>(() => {
131
+ if (typeof window === 'undefined') {
132
+ return 'table';
133
+ }
134
+
135
+ const savedViewMode = window.localStorage.getItem(
136
+ CONTRACT_VIEW_STORAGE_KEY
137
+ );
138
+
139
+ return savedViewMode === 'cards' ? 'cards' : 'table';
140
+ });
141
+
142
+ const getContractOptionLabel = (group: string, value?: string | null) => {
143
+ if (!value) {
144
+ return '-';
145
+ }
146
+
147
+ const key = `options.${group}.${value}`;
148
+ return formT.has(key) ? formT(key) : formatEnumLabel(value);
149
+ };
150
+
151
+ const editParam = searchParams.get('edit');
152
+ const duplicateParam = searchParams.get('duplicate');
153
+ const templateParam = searchParams.get('template');
154
+ const editingContractId =
155
+ editParam && /^\d+$/.test(editParam) ? Number(editParam) : null;
156
+ const duplicatingContractId =
157
+ duplicateParam && /^\d+$/.test(duplicateParam)
158
+ ? Number(duplicateParam)
159
+ : null;
160
+ const creatingFromTemplateId =
161
+ templateParam && /^\d+$/.test(templateParam) ? Number(templateParam) : null;
162
+
163
+ const updateSheetQuery = (options?: {
164
+ editId?: number | null;
165
+ duplicateId?: number | null;
166
+ templateId?: number | null;
167
+ }) => {
168
+ const params = new URLSearchParams(searchParams.toString());
169
+
170
+ params.delete('edit');
171
+ params.delete('duplicate');
172
+ params.delete('template');
173
+
174
+ if (options?.editId && options.editId > 0) {
175
+ params.set('edit', String(options.editId));
176
+ }
177
+
178
+ if (options?.duplicateId && options.duplicateId > 0) {
179
+ params.set('duplicate', String(options.duplicateId));
180
+ }
181
+
182
+ if (options?.templateId && options.templateId > 0) {
183
+ params.set('template', String(options.templateId));
184
+ }
185
+
186
+ const nextUrl = params.size ? `${pathname}?${params.toString()}` : pathname;
187
+ router.replace(nextUrl, { scroll: false });
188
+ };
189
+
190
+ const openCreateSheet = (templateId?: number | null) => {
191
+ if (templateId && templateId > 0) {
192
+ updateSheetQuery({ templateId });
193
+ return;
194
+ }
195
+
196
+ setIsCreateSheetOpen(true);
197
+ updateSheetQuery();
198
+ };
199
+
200
+ const openEditSheet = (contractId: number) => {
201
+ setIsCreateSheetOpen(false);
202
+ updateSheetQuery({ editId: contractId });
203
+ };
204
+
205
+ const openDuplicateSheet = (contractId: number) => {
206
+ setIsCreateSheetOpen(false);
207
+ updateSheetQuery({ duplicateId: contractId });
208
+ };
209
+
210
+ const closeFormSheet = () => {
211
+ setIsCreateSheetOpen(false);
212
+
213
+ if (
214
+ editingContractId !== null ||
215
+ duplicatingContractId !== null ||
216
+ creatingFromTemplateId !== null
217
+ ) {
218
+ updateSheetQuery();
219
+ }
220
+ };
221
+
222
+ const { data: contracts = [], refetch } = useQuery<OperationsContract[]>({
223
+ queryKey: ['operations-contracts-list', currentLocaleCode],
224
+ queryFn: () =>
225
+ fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
226
+ });
227
+
228
+ const filteredRows = useMemo(
229
+ () =>
230
+ contracts.filter((item) => {
231
+ const matchesSearch = !search.trim()
232
+ ? true
233
+ : [
234
+ item.name,
235
+ item.code,
236
+ item.mainRelatedPartyName,
237
+ item.clientName,
238
+ item.contractType,
239
+ ]
240
+ .filter(Boolean)
241
+ .some((value) =>
242
+ String(value)
243
+ .toLowerCase()
244
+ .includes(search.trim().toLowerCase())
245
+ );
246
+ const matchesStatus =
247
+ statusFilter === 'all' ? true : item.status === statusFilter;
248
+ const matchesCategory =
249
+ categoryFilter === 'all'
250
+ ? true
251
+ : item.contractCategory === categoryFilter;
252
+ const matchesOrigin =
253
+ originFilter === 'all' ? true : item.originType === originFilter;
254
+ return (
255
+ matchesSearch && matchesStatus && matchesCategory && matchesOrigin
256
+ );
257
+ }),
258
+ [contracts, search, statusFilter, categoryFilter, originFilter]
259
+ );
260
+
261
+ const sheetTitle = editingContractId
262
+ ? commonT('actions.edit')
263
+ : duplicatingContractId
264
+ ? t('actions.duplicate')
265
+ : isCreateSheetOpen || creatingFromTemplateId
266
+ ? commonT('actions.create')
267
+ : t('title');
268
+
269
+ const statsCards = useMemo(
270
+ () => [
271
+ {
272
+ key: 'total',
273
+ title: t('cards.total'),
274
+ value: contracts.length,
275
+ icon: FileText,
276
+ },
277
+ {
278
+ key: 'active',
279
+ title: t('cards.active'),
280
+ value: contracts.filter((item) => item.status === 'active').length,
281
+ icon: Sparkles,
282
+ },
283
+ {
284
+ key: 'underReview',
285
+ title: t('cards.underReview'),
286
+ value: contracts.filter((item) => item.status === 'under_review')
287
+ .length,
288
+ icon: Pencil,
289
+ },
290
+ {
291
+ key: 'withTemplate',
292
+ title: t('cards.withTemplate'),
293
+ value: contracts.filter((item) => Boolean(item.contractTemplateId))
294
+ .length,
295
+ icon: FileStack,
296
+ },
297
+ ],
298
+ [contracts, t]
299
+ );
300
+
301
+ const handleViewModeChange = (value: string) => {
302
+ if (value !== 'table' && value !== 'cards') {
303
+ return;
304
+ }
305
+
306
+ setViewMode(value);
307
+
308
+ if (typeof window !== 'undefined') {
309
+ window.localStorage.setItem(CONTRACT_VIEW_STORAGE_KEY, value);
310
+ }
311
+ };
312
+
313
+ const downloadPdf = async (contractId: number) => {
314
+ const detail = await fetchOperations<OperationsContractDetails>(
315
+ request,
316
+ `/operations/contracts/${contractId}`
317
+ );
318
+ const document = detail.documents.find(
319
+ (item) =>
320
+ item.isCurrent &&
321
+ (item.documentType === 'generated_pdf' ||
322
+ item.documentType === 'source_upload')
323
+ );
324
+ if (document?.fileId) {
325
+ openStoredFile(document.fileId);
326
+ return;
327
+ }
328
+ if (!document?.fileContentBase64) {
329
+ showToastHandler?.('error', t('messages.noPdf'));
330
+ return;
331
+ }
332
+ downloadBase64File(
333
+ document.fileName,
334
+ document.mimeType,
335
+ document.fileContentBase64
336
+ );
337
+ };
338
+
339
+ const duplicateContract = (contractId: number) => {
340
+ openDuplicateSheet(contractId);
341
+ };
342
+
343
+ const handleDeleteContract = async () => {
344
+ if (!contractToDelete?.id) {
345
+ return;
346
+ }
347
+
348
+ try {
349
+ setIsDeletingContract(true);
350
+ await request({
351
+ url: `/operations/contracts/${contractToDelete.id}`,
352
+ method: 'DELETE',
353
+ });
354
+ showToastHandler?.('success', t('messages.deleteSuccess'));
355
+ setContractToDelete(null);
356
+ await refetch();
357
+ } catch {
358
+ showToastHandler?.('error', t('messages.deleteError'));
359
+ } finally {
360
+ setIsDeletingContract(false);
361
+ }
362
+ };
363
+
364
+ const toggleArchived = async (contract: OperationsContract) => {
365
+ try {
366
+ await mutateOperations(
367
+ request,
368
+ `/operations/contracts/${contract.id}`,
369
+ 'PATCH',
370
+ {
371
+ status: contract.status === 'archived' ? 'active' : 'archived',
372
+ isActive: contract.status === 'archived',
373
+ }
374
+ );
375
+ showToastHandler?.('success', t('messages.statusSuccess'));
376
+ await refetch();
377
+ } catch {
378
+ showToastHandler?.('error', t('messages.statusError'));
379
+ }
380
+ };
381
+
382
+ const renderContractActions = (
383
+ contract: OperationsContract,
384
+ align: 'start' | 'end' = 'end'
385
+ ) => (
386
+ <DropdownMenu>
387
+ <DropdownMenuTrigger asChild>
388
+ <Button
389
+ variant="outline"
390
+ size="icon"
391
+ className="cursor-pointer"
392
+ aria-label={commonT('labels.actions')}
393
+ >
394
+ <MoreHorizontal className="size-4" />
395
+ </Button>
396
+ </DropdownMenuTrigger>
397
+ <DropdownMenuContent align={align} className="w-52">
398
+ {access.isDirector ? (
399
+ <DropdownMenuItem onSelect={() => openEditSheet(contract.id)}>
400
+ <Pencil className="size-4" />
401
+ {commonT('actions.edit')}
402
+ </DropdownMenuItem>
403
+ ) : null}
404
+ <DropdownMenuItem onSelect={() => void downloadPdf(contract.id)}>
405
+ <Download className="size-4" />
406
+ {t('actions.downloadPdf')}
407
+ </DropdownMenuItem>
408
+ {access.isDirector ? (
409
+ <DropdownMenuItem
410
+ onSelect={() => {
411
+ uploadTargetRef.current = contract.id;
412
+ fileInputRef.current?.click();
413
+ }}
414
+ >
415
+ <Upload className="size-4" />
416
+ {t('actions.uploadPdf')}
417
+ </DropdownMenuItem>
418
+ ) : null}
419
+ {access.isDirector ? <DropdownMenuSeparator /> : null}
420
+ {access.isDirector ? (
421
+ <DropdownMenuItem
422
+ onSelect={() => void duplicateContract(contract.id)}
423
+ >
424
+ <FileStack className="size-4" />
425
+ {t('actions.duplicate')}
426
+ </DropdownMenuItem>
427
+ ) : null}
428
+ {access.isDirector ? (
429
+ <DropdownMenuItem onSelect={() => void toggleArchived(contract)}>
430
+ <Sparkles className="size-4" />
431
+ {contract.status === 'archived'
432
+ ? commonT('actions.activate')
433
+ : t('actions.archive')}
434
+ </DropdownMenuItem>
435
+ ) : null}
436
+ {access.isDirector ? <DropdownMenuSeparator /> : null}
437
+ {access.isDirector ? (
438
+ <DropdownMenuItem
439
+ onSelect={() => setContractToDelete(contract)}
440
+ className="text-destructive focus:text-destructive"
441
+ >
442
+ <Trash2 className="size-4 text-destructive" />
443
+ {t('actions.delete')}
444
+ </DropdownMenuItem>
445
+ ) : null}
446
+ </DropdownMenuContent>
447
+ </DropdownMenu>
448
+ );
449
+
450
+ return (
451
+ <Page>
452
+ <OperationsHeader
453
+ title={t('title')}
454
+ description={t('description')}
455
+ current={t('breadcrumb')}
456
+ actions={
457
+ access.isDirector ? (
458
+ <div className="flex flex-wrap gap-2">
459
+ <Button
460
+ size="sm"
461
+ className="cursor-pointer"
462
+ onClick={() => openCreateSheet()}
463
+ >
464
+ {commonT('actions.create')}
465
+ </Button>
466
+ </div>
467
+ ) : undefined
468
+ }
469
+ />
470
+
471
+ <KpiCardsGrid items={statsCards} columns={4} />
472
+
473
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
474
+ <div className="flex-1">
475
+ <SearchBar
476
+ searchQuery={search}
477
+ onSearchChange={setSearch}
478
+ onSearch={() => undefined}
479
+ placeholder={t('searchPlaceholder')}
480
+ controls={[
481
+ {
482
+ id: 'category',
483
+ type: 'select',
484
+ value: categoryFilter,
485
+ onChange: setCategoryFilter,
486
+ placeholder: commonT('labels.contractCategory'),
487
+ options: [
488
+ { value: 'all', label: commonT('filters.allTypes') },
489
+ ...[
490
+ 'employee',
491
+ 'contractor',
492
+ 'client',
493
+ 'supplier',
494
+ 'vendor',
495
+ 'partner',
496
+ 'internal',
497
+ 'other',
498
+ ].map((value) => ({
499
+ value,
500
+ label: getContractOptionLabel('contractCategories', value),
501
+ })),
502
+ ],
503
+ },
504
+ {
505
+ id: 'origin',
506
+ type: 'select',
507
+ value: originFilter,
508
+ onChange: setOriginFilter,
509
+ placeholder: t('filters.originType'),
510
+ options: [
511
+ { value: 'all', label: commonT('filters.allTypes') },
512
+ ...['manual', 'employee_hiring', 'client_project'].map(
513
+ (value) => ({
514
+ value,
515
+ label: getContractOptionLabel('originTypes', value),
516
+ })
517
+ ),
518
+ ],
519
+ },
520
+ {
521
+ id: 'status',
522
+ type: 'select',
523
+ value: statusFilter,
524
+ onChange: setStatusFilter,
525
+ placeholder: commonT('labels.status'),
526
+ options: [
527
+ { value: 'all', label: commonT('filters.allStatuses') },
528
+ ...[
529
+ 'draft',
530
+ 'under_review',
531
+ 'active',
532
+ 'renewal',
533
+ 'expired',
534
+ 'closed',
535
+ 'archived',
536
+ ].map((value) => ({
537
+ value,
538
+ label: getContractOptionLabel('statuses', value),
539
+ })),
540
+ ],
541
+ },
542
+ ]}
543
+ />
544
+ </div>
545
+
546
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
547
+ <span className="text-xs font-medium text-muted-foreground">
548
+ {t('viewMode')}
549
+ </span>
550
+ <ToggleGroup
551
+ type="single"
552
+ value={viewMode}
553
+ onValueChange={handleViewModeChange}
554
+ variant="outline"
555
+ size="sm"
556
+ aria-label={t('viewMode')}
557
+ >
558
+ <ToggleGroupItem
559
+ value="table"
560
+ className="gap-1.5 px-2.5"
561
+ aria-label={t('viewModeTable')}
562
+ >
563
+ <List className="h-4 w-4" />
564
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
565
+ </ToggleGroupItem>
566
+ <ToggleGroupItem
567
+ value="cards"
568
+ className="gap-1.5 px-2.5"
569
+ aria-label={t('viewModeCards')}
570
+ >
571
+ <LayoutGrid className="h-4 w-4" />
572
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
573
+ </ToggleGroupItem>
574
+ </ToggleGroup>
575
+ </div>
576
+ </div>
577
+
578
+ <input
579
+ ref={fileInputRef}
580
+ type="file"
581
+ accept="application/pdf"
582
+ className="hidden"
583
+ onChange={async (event) => {
584
+ const file = event.target.files?.[0];
585
+ const contractId = uploadTargetRef.current;
586
+ if (!file || !contractId) return;
587
+ try {
588
+ const fileContentBase64 = await fileToBase64(file);
589
+ await mutateOperations(
590
+ request,
591
+ `/operations/contracts/${contractId}`,
592
+ 'PATCH',
593
+ {
594
+ replaceUploadedPdfDocument: {
595
+ fileName: file.name,
596
+ mimeType: file.type || 'application/pdf',
597
+ fileContentBase64,
598
+ },
599
+ }
600
+ );
601
+ showToastHandler?.('success', t('messages.uploadSuccess'));
602
+ await refetch();
603
+ } catch {
604
+ showToastHandler?.('error', t('messages.uploadError'));
605
+ } finally {
606
+ uploadTargetRef.current = null;
607
+ event.target.value = '';
608
+ }
609
+ }}
610
+ />
611
+
612
+ {filteredRows.length ? (
613
+ viewMode === 'cards' ? (
614
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
615
+ {filteredRows.map((contract) => {
616
+ const financialTotal =
617
+ (contract.valueAmount ?? 0) + (contract.revenueAmount ?? 0);
618
+ const contractTitle =
619
+ contract.name ||
620
+ contract.code ||
621
+ commonT('labels.notAvailable');
622
+ const secondaryLine = [
623
+ contract.code,
624
+ getContractOptionLabel('contractTypes', contract.contractType),
625
+ getContractOptionLabel('originTypes', contract.originType),
626
+ ]
627
+ .filter(Boolean)
628
+ .join(' • ');
629
+
630
+ return (
631
+ <Card
632
+ key={contract.id}
633
+ className="overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
634
+ >
635
+ <CardContent className="space-y-4 p-4">
636
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
637
+ <div className="min-w-0">
638
+ <div className="truncate font-semibold">
639
+ {contractTitle}
640
+ </div>
641
+ <div className="truncate text-xs text-muted-foreground">
642
+ {secondaryLine || commonT('labels.notAvailable')}
643
+ </div>
644
+ </div>
645
+ <StatusBadge
646
+ label={getContractOptionLabel(
647
+ 'statuses',
648
+ contract.status
649
+ )}
650
+ className={getStatusBadgeClass(contract.status)}
651
+ />
652
+ </div>
653
+
654
+ <div className="flex flex-wrap gap-2">
655
+ <span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
656
+ {getContractOptionLabel(
657
+ 'contractCategories',
658
+ contract.contractCategory
659
+ )}
660
+ </span>
661
+ {contract.contractTemplateName ? (
662
+ <span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
663
+ {contract.contractTemplateName}
664
+ </span>
665
+ ) : null}
666
+ </div>
667
+
668
+ <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
669
+ <div>
670
+ <span className="font-medium text-foreground">
671
+ {commonT('labels.client')}:
672
+ </span>{' '}
673
+ {contract.clientName || commonT('labels.notAvailable')}
674
+ </div>
675
+ <div>
676
+ <span className="font-medium text-foreground">
677
+ {t('columns.party')}:
678
+ </span>{' '}
679
+ {contract.mainRelatedPartyName ||
680
+ commonT('labels.notAvailable')}
681
+ </div>
682
+ <div className="flex items-center gap-2">
683
+ <span className="font-medium text-foreground">
684
+ {t('columns.signatureStatus')}:
685
+ </span>
686
+ <StatusBadge
687
+ label={getContractOptionLabel(
688
+ 'signatureStatuses',
689
+ contract.signatureStatus
690
+ )}
691
+ className={getStatusBadgeClass(
692
+ contract.signatureStatus
693
+ )}
694
+ />
695
+ </div>
696
+ <div>
697
+ <span className="font-medium text-foreground">
698
+ {t('columns.financials')}:
699
+ </span>{' '}
700
+ {formatCurrency(financialTotal)}
701
+ </div>
702
+ <div>
703
+ <span className="font-medium text-foreground">
704
+ {commonT('labels.startDate')}:
705
+ </span>{' '}
706
+ {formatDate(contract.startDate)}
707
+ </div>
708
+ <div>
709
+ <span className="font-medium text-foreground">
710
+ {commonT('labels.endDate')}:
711
+ </span>{' '}
712
+ {formatDate(contract.endDate)}
713
+ </div>
714
+ </div>
715
+
716
+ <div className="flex justify-end">
717
+ {renderContractActions(contract, 'end')}
718
+ </div>
719
+ </CardContent>
720
+ </Card>
721
+ );
722
+ })}
723
+ </div>
724
+ ) : (
725
+ <Card className="overflow-hidden border-border/60 py-0 shadow-sm">
726
+ <CardContent className="p-0">
727
+ <Table className="table-fixed">
728
+ <TableHeader>
729
+ <TableRow>
730
+ <TableHead className="w-[34%]">
731
+ {t('columns.title')}
732
+ </TableHead>
733
+ <TableHead>{commonT('labels.status')}</TableHead>
734
+ <TableHead className="hidden md:table-cell">
735
+ {t('columns.signatureStatus')}
736
+ </TableHead>
737
+ <TableHead className="hidden lg:table-cell">
738
+ {t('columns.type')}
739
+ </TableHead>
740
+ <TableHead className="hidden xl:table-cell">
741
+ {commonT('labels.client')}
742
+ </TableHead>
743
+ <TableHead className="hidden 2xl:table-cell">
744
+ {t('columns.financials')}
745
+ </TableHead>
746
+ <TableHead className="w-30 text-right sm:w-42.5">
747
+ {commonT('labels.actions')}
748
+ </TableHead>
749
+ </TableRow>
750
+ </TableHeader>
751
+ <TableBody>
752
+ {filteredRows.map((contract) => {
753
+ const financialTotal =
754
+ (contract.valueAmount ?? 0) +
755
+ (contract.revenueAmount ?? 0);
756
+
757
+ return (
758
+ <TableRow key={contract.id} className="hover:bg-muted/30">
759
+ <TableCell>
760
+ <div className="min-w-0">
761
+ <div className="truncate font-medium">
762
+ {contract.name ||
763
+ contract.code ||
764
+ commonT('labels.notAvailable')}
765
+ </div>
766
+ <div className="truncate text-xs text-muted-foreground">
767
+ {[
768
+ contract.code,
769
+ contract.contractTemplateName,
770
+ contract.mainRelatedPartyName,
771
+ getContractOptionLabel(
772
+ 'originTypes',
773
+ contract.originType
774
+ ),
775
+ ]
776
+ .filter(Boolean)
777
+ .join(' • ') || commonT('labels.notAvailable')}
778
+ </div>
779
+ </div>
780
+ </TableCell>
781
+ <TableCell>
782
+ <StatusBadge
783
+ label={getContractOptionLabel(
784
+ 'statuses',
785
+ contract.status
786
+ )}
787
+ className={getStatusBadgeClass(contract.status)}
788
+ />
789
+ </TableCell>
790
+ <TableCell className="hidden md:table-cell">
791
+ <StatusBadge
792
+ label={getContractOptionLabel(
793
+ 'signatureStatuses',
794
+ contract.signatureStatus
795
+ )}
796
+ className={getStatusBadgeClass(
797
+ contract.signatureStatus
798
+ )}
799
+ />
800
+ </TableCell>
801
+ <TableCell className="hidden lg:table-cell">
802
+ <div className="truncate">
803
+ {getContractOptionLabel(
804
+ 'contractTypes',
805
+ contract.contractType
806
+ )}
807
+ </div>
808
+ </TableCell>
809
+ <TableCell className="hidden xl:table-cell">
810
+ <div className="truncate">
811
+ {contract.clientName ||
812
+ commonT('labels.notAvailable')}
813
+ </div>
814
+ </TableCell>
815
+ <TableCell className="hidden 2xl:table-cell">
816
+ {formatCurrency(financialTotal)}
817
+ </TableCell>
818
+ <TableCell>
819
+ <div className="flex justify-end">
820
+ {renderContractActions(contract, 'end')}
821
+ </div>
822
+ </TableCell>
823
+ </TableRow>
824
+ );
825
+ })}
826
+ </TableBody>
827
+ </Table>
828
+ </CardContent>
829
+ </Card>
830
+ )
831
+ ) : (
832
+ <EmptyState
833
+ icon={<FileText className="size-12" />}
834
+ title={commonT('states.emptyTitle')}
835
+ description={t('emptyDescription')}
836
+ actionLabel={
837
+ access.isDirector
838
+ ? commonT('actions.create')
839
+ : commonT('actions.refresh')
840
+ }
841
+ onAction={
842
+ access.isDirector ? () => openCreateSheet() : () => void refetch()
843
+ }
844
+ />
845
+ )}
846
+
847
+ <AlertDialog
848
+ open={contractToDelete !== null}
849
+ onOpenChange={(open) => {
850
+ if (!open && !isDeletingContract) {
851
+ setContractToDelete(null);
852
+ }
853
+ }}
854
+ >
855
+ <AlertDialogContent>
856
+ <AlertDialogHeader>
857
+ <AlertDialogTitle>
858
+ {t('messages.confirmDeleteTitle')}
859
+ </AlertDialogTitle>
860
+ <AlertDialogDescription>
861
+ {t('messages.confirmDeleteDescription', {
862
+ name:
863
+ contractToDelete?.name ||
864
+ contractToDelete?.code ||
865
+ commonT('labels.notAvailable'),
866
+ })}
867
+ </AlertDialogDescription>
868
+ </AlertDialogHeader>
869
+ <AlertDialogFooter>
870
+ <AlertDialogCancel disabled={isDeletingContract}>
871
+ {commonT('actions.cancel')}
872
+ </AlertDialogCancel>
873
+ <AlertDialogAction
874
+ disabled={isDeletingContract}
875
+ className="bg-destructive text-white hover:bg-destructive/90"
876
+ onClick={(event) => {
877
+ event.preventDefault();
878
+ void handleDeleteContract();
879
+ }}
880
+ >
881
+ {t('actions.delete')}
882
+ </AlertDialogAction>
883
+ </AlertDialogFooter>
884
+ </AlertDialogContent>
885
+ </AlertDialog>
886
+
887
+ <Sheet
888
+ open={
889
+ isCreateSheetOpen ||
890
+ editingContractId !== null ||
891
+ duplicatingContractId !== null ||
892
+ creatingFromTemplateId !== null
893
+ }
894
+ onOpenChange={(open) => {
895
+ if (!open) {
896
+ closeFormSheet();
897
+ }
898
+ }}
899
+ >
900
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
901
+ <SheetHeader className="sr-only">
902
+ <SheetTitle>{sheetTitle}</SheetTitle>
903
+ <SheetDescription>{t('description')}</SheetDescription>
904
+ </SheetHeader>
905
+ <ContractWizardSheet
906
+ key={
907
+ editingContractId
908
+ ? `edit-${editingContractId}`
909
+ : duplicatingContractId
910
+ ? `duplicate-${duplicatingContractId}`
911
+ : creatingFromTemplateId
912
+ ? `template-${creatingFromTemplateId}`
913
+ : isCreateSheetOpen
914
+ ? 'create-contract'
915
+ : 'contract-sheet'
916
+ }
917
+ contractId={editingContractId ?? undefined}
918
+ duplicateFromId={duplicatingContractId ?? undefined}
919
+ initialTemplateId={creatingFromTemplateId ?? undefined}
920
+ isCreateFlow={isCreateSheetOpen}
921
+ onCancel={closeFormSheet}
922
+ onSaved={async (contract) => {
923
+ await refetch();
924
+
925
+ if (
926
+ isCreateSheetOpen ||
927
+ duplicatingContractId !== null ||
928
+ creatingFromTemplateId !== null
929
+ ) {
930
+ openEditSheet(contract.id);
931
+ return;
932
+ }
933
+
934
+ closeFormSheet();
935
+ }}
936
+ />
937
+ </SheetContent>
938
+ </Sheet>
939
+ </Page>
940
+ );
941
+ }