@hed-hog/operations 0.0.299 → 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 (97) 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 +3590 -1267
  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 +232 -198
  25. package/hedhog/data/role.yaml +23 -23
  26. package/hedhog/data/role_route.yaml +39 -0
  27. package/hedhog/data/route.yaml +447 -317
  28. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  29. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  30. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  31. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  32. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  33. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  34. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  35. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  36. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  37. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  38. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  39. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  40. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  41. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  42. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  43. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  44. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  45. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  46. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  48. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  49. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  51. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  52. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  53. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  54. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  55. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  59. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  60. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  61. package/hedhog/frontend/messages/en.json +473 -12
  62. package/hedhog/frontend/messages/pt.json +528 -66
  63. package/hedhog/table/operations_approval.yaml +49 -49
  64. package/hedhog/table/operations_approval_history.yaml +29 -29
  65. package/hedhog/table/operations_collaborator.yaml +87 -67
  66. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
  67. package/hedhog/table/operations_contract.yaml +121 -100
  68. package/hedhog/table/operations_contract_document.yaml +40 -23
  69. package/hedhog/table/operations_contract_financial_term.yaml +40 -40
  70. package/hedhog/table/operations_contract_history.yaml +27 -27
  71. package/hedhog/table/operations_contract_party.yaml +46 -46
  72. package/hedhog/table/operations_contract_revision.yaml +38 -38
  73. package/hedhog/table/operations_contract_signature.yaml +38 -38
  74. package/hedhog/table/operations_contract_template.yaml +58 -0
  75. package/hedhog/table/operations_department.yaml +24 -0
  76. package/hedhog/table/operations_project.yaml +54 -54
  77. package/hedhog/table/operations_project_assignment.yaml +55 -55
  78. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
  79. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
  80. package/hedhog/table/operations_time_off_request.yaml +57 -57
  81. package/hedhog/table/operations_timesheet.yaml +41 -41
  82. package/hedhog/table/operations_timesheet_entry.yaml +40 -40
  83. package/package.json +5 -3
  84. package/src/operations.controller.ts +304 -182
  85. package/src/operations.module.ts +26 -22
  86. package/src/operations.proposal.subscriber.spec.ts +121 -0
  87. package/src/operations.proposal.subscriber.ts +86 -0
  88. package/src/operations.service.spec.ts +210 -0
  89. package/src/operations.service.ts +7317 -3595
  90. package/dist/operations-data.controller.d.ts +0 -139
  91. package/dist/operations-data.controller.d.ts.map +0 -1
  92. package/dist/operations-data.controller.js +0 -113
  93. package/dist/operations-data.controller.js.map +0 -1
  94. package/dist/operations-growth.controller.d.ts +0 -48
  95. package/dist/operations-growth.controller.d.ts.map +0 -1
  96. package/dist/operations-growth.controller.js +0 -90
  97. package/dist/operations-growth.controller.js.map +0 -1
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
4
  import { Button } from '@/components/ui/button';
5
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
5
  import {
7
6
  Table,
8
7
  TableBody,
@@ -11,13 +10,11 @@ import {
11
10
  TableHeader,
12
11
  TableRow,
13
12
  } from '@/components/ui/table';
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
14
14
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
15
15
  import { Download, FileText, Pencil } from 'lucide-react';
16
- import Link from 'next/link';
17
16
  import { useTranslations } from 'next-intl';
18
- import { OperationsHeader } from './operations-header';
19
- import { SectionCard } from './section-card';
20
- import { StatusBadge } from './status-badge';
17
+ import Link from 'next/link';
21
18
  import { fetchOperations } from '../_lib/api';
22
19
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
23
20
  import type { OperationsContractDetails } from '../_lib/types';
@@ -28,8 +25,15 @@ import {
28
25
  formatEnumLabel,
29
26
  getStatusBadgeClass,
30
27
  } from '../_lib/utils/format';
28
+ import { OperationsHeader } from './operations-header';
29
+ import { SectionCard } from './section-card';
30
+ import { StatusBadge } from './status-badge';
31
31
 
32
- function downloadBase64File(fileName: string, mimeType: string, base64: string) {
32
+ function downloadBase64File(
33
+ fileName: string,
34
+ mimeType: string,
35
+ base64: string
36
+ ) {
33
37
  const href = `data:${mimeType};base64,${base64}`;
34
38
  const link = document.createElement('a');
35
39
  link.href = href;
@@ -37,22 +41,40 @@ function downloadBase64File(fileName: string, mimeType: string, base64: string)
37
41
  link.click();
38
42
  }
39
43
 
44
+ function openStoredFile(fileId?: number | null) {
45
+ if (!fileId) return;
46
+ const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
47
+ window.open(
48
+ `${baseUrl}/file/open/${fileId}`,
49
+ '_blank',
50
+ 'noopener,noreferrer'
51
+ );
52
+ }
53
+
40
54
  export function ContractDetailsScreen({ contractId }: { contractId: number }) {
41
55
  const t = useTranslations('operations.ContractDetailsPage');
42
56
  const commonT = useTranslations('operations.Common');
57
+ const formT = useTranslations('operations.ContractFormPage');
43
58
  const { request, currentLocaleCode } = useApp();
44
59
  const access = useOperationsAccess();
45
60
 
46
61
  const { data: contract, refetch } = useQuery<OperationsContractDetails>({
47
62
  queryKey: ['operations-contract-details', currentLocaleCode, contractId],
48
63
  queryFn: () =>
49
- fetchOperations<OperationsContractDetails>(request, `/operations/contracts/${contractId}`),
64
+ fetchOperations<OperationsContractDetails>(
65
+ request,
66
+ `/operations/contracts/${contractId}`
67
+ ),
50
68
  });
51
69
 
52
70
  if (!contract) {
53
71
  return (
54
72
  <Page>
55
- <OperationsHeader title={t('title')} description={t('description')} current={t('breadcrumb')} />
73
+ <OperationsHeader
74
+ title={t('title')}
75
+ description={t('description')}
76
+ current={t('breadcrumb')}
77
+ />
56
78
  <EmptyState
57
79
  icon={<FileText className="size-12" />}
58
80
  title={commonT('states.emptyTitle')}
@@ -65,17 +87,39 @@ export function ContractDetailsScreen({ contractId }: { contractId: number }) {
65
87
  }
66
88
 
67
89
  const currentPdf = contract.documents.find((document) => document.isCurrent);
90
+ const contractTitle =
91
+ contract.name || contract.code || commonT('labels.notAvailable');
92
+ const getOptionLabel = (group: string, value?: string | null) => {
93
+ if (!value) {
94
+ return '-';
95
+ }
96
+
97
+ const key = `options.${group}.${value}`;
98
+ return formT.has(key) ? formT(key) : formatEnumLabel(value);
99
+ };
68
100
 
69
101
  return (
70
102
  <Page>
71
103
  <OperationsHeader
72
- title={contract.name}
104
+ title={contractTitle}
73
105
  description={t('description')}
74
106
  current={t('breadcrumb')}
75
107
  actions={
76
108
  <div className="flex gap-2">
77
- {currentPdf?.fileContentBase64 ? (
78
- <Button variant="outline" size="sm" onClick={() => downloadBase64File(currentPdf.fileName, currentPdf.mimeType, currentPdf.fileContentBase64 || '')}>
109
+ {currentPdf?.fileId || currentPdf?.fileContentBase64 ? (
110
+ <Button
111
+ variant="outline"
112
+ size="sm"
113
+ onClick={() =>
114
+ currentPdf?.fileId
115
+ ? openStoredFile(currentPdf.fileId)
116
+ : downloadBase64File(
117
+ currentPdf.fileName,
118
+ currentPdf.mimeType,
119
+ currentPdf.fileContentBase64 || ''
120
+ )
121
+ }
122
+ >
79
123
  <Download className="size-4" />
80
124
  {t('actions.downloadPdf')}
81
125
  </Button>
@@ -106,26 +150,308 @@ export function ContractDetailsScreen({ contractId }: { contractId: number }) {
106
150
  <TabsContent value="overview">
107
151
  <SectionCard title={t('sections.overview')}>
108
152
  <dl className="grid gap-3 text-sm md:grid-cols-3">
109
- <div><dt className="text-muted-foreground">{commonT('labels.contract')}</dt><dd className="font-medium">{contract.name}</dd></div>
110
- <div><dt className="text-muted-foreground">{t('labels.origin')}</dt><dd className="font-medium">{formatEnumLabel(contract.originType)}</dd></div>
111
- <div><dt className="text-muted-foreground">{t('labels.mainParty')}</dt><dd className="font-medium">{contract.mainRelatedPartyName || commonT('labels.notAvailable')}</dd></div>
112
- <div><dt className="text-muted-foreground">{t('labels.contractType')}</dt><dd className="font-medium">{formatEnumLabel(contract.contractType)}</dd></div>
113
- <div><dt className="text-muted-foreground">{t('labels.signatureStatus')}</dt><dd className="font-medium"><StatusBadge label={formatEnumLabel(contract.signatureStatus)} className={getStatusBadgeClass(contract.signatureStatus)} /></dd></div>
114
- <div><dt className="text-muted-foreground">{t('labels.activeState')}</dt><dd className="font-medium">{contract.isActive ? t('labels.active') : t('labels.inactive')}</dd></div>
115
- <div><dt className="text-muted-foreground">{commonT('labels.startDate')}</dt><dd className="font-medium">{formatDate(contract.startDate)}</dd></div>
116
- <div><dt className="text-muted-foreground">{commonT('labels.endDate')}</dt><dd className="font-medium">{formatDate(contract.endDate)}</dd></div>
117
- <div><dt className="text-muted-foreground">{commonT('labels.status')}</dt><dd className="font-medium"><StatusBadge label={formatEnumLabel(contract.status)} className={getStatusBadgeClass(contract.status)} /></dd></div>
153
+ <div>
154
+ <dt className="text-muted-foreground">
155
+ {commonT('labels.contract')}
156
+ </dt>
157
+ <dd className="font-medium">{contractTitle}</dd>
158
+ </div>
159
+ <div>
160
+ <dt className="text-muted-foreground">{t('labels.origin')}</dt>
161
+ <dd className="font-medium">
162
+ {getOptionLabel('originTypes', contract.originType)}
163
+ </dd>
164
+ </div>
165
+ <div>
166
+ <dt className="text-muted-foreground">
167
+ {t('labels.mainParty')}
168
+ </dt>
169
+ <dd className="font-medium">
170
+ {contract.mainRelatedPartyName ||
171
+ commonT('labels.notAvailable')}
172
+ </dd>
173
+ </div>
174
+ <div>
175
+ <dt className="text-muted-foreground">
176
+ {t('labels.contractType')}
177
+ </dt>
178
+ <dd className="font-medium">
179
+ {getOptionLabel('contractTypes', contract.contractType)}
180
+ </dd>
181
+ </div>
182
+ <div>
183
+ <dt className="text-muted-foreground">
184
+ {t('labels.signatureStatus')}
185
+ </dt>
186
+ <dd className="font-medium">
187
+ <StatusBadge
188
+ label={getOptionLabel(
189
+ 'signatureStatuses',
190
+ contract.signatureStatus
191
+ )}
192
+ className={getStatusBadgeClass(contract.signatureStatus)}
193
+ />
194
+ </dd>
195
+ </div>
196
+ <div>
197
+ <dt className="text-muted-foreground">
198
+ {t('labels.activeState')}
199
+ </dt>
200
+ <dd className="font-medium">
201
+ {contract.isActive
202
+ ? t('labels.active')
203
+ : t('labels.inactive')}
204
+ </dd>
205
+ </div>
206
+ <div>
207
+ <dt className="text-muted-foreground">
208
+ {commonT('labels.startDate')}
209
+ </dt>
210
+ <dd className="font-medium">
211
+ {formatDate(contract.startDate)}
212
+ </dd>
213
+ </div>
214
+ <div>
215
+ <dt className="text-muted-foreground">
216
+ {commonT('labels.endDate')}
217
+ </dt>
218
+ <dd className="font-medium">{formatDate(contract.endDate)}</dd>
219
+ </div>
220
+ <div>
221
+ <dt className="text-muted-foreground">
222
+ {commonT('labels.status')}
223
+ </dt>
224
+ <dd className="font-medium">
225
+ <StatusBadge
226
+ label={getOptionLabel('statuses', contract.status)}
227
+ className={getStatusBadgeClass(contract.status)}
228
+ />
229
+ </dd>
230
+ </div>
118
231
  </dl>
119
- {contract.contentHtml ? <div className="prose prose-sm mt-4 max-w-none rounded-lg border p-4" dangerouslySetInnerHTML={{ __html: contract.contentHtml }} /> : null}
232
+ {contract.contentHtml ? (
233
+ <div
234
+ className="prose prose-sm mt-4 max-w-none rounded-lg border p-4"
235
+ dangerouslySetInnerHTML={{ __html: contract.contentHtml }}
236
+ />
237
+ ) : null}
120
238
  </SectionCard>
121
239
  </TabsContent>
122
240
 
123
- <TabsContent value="parties"><SectionCard title={t('sections.parties')}>{contract.parties.length ? <div className="overflow-x-auto rounded-md border"><Table><TableHeader><TableRow><TableHead>{t('labels.mainParty')}</TableHead><TableHead>{t('labels.partyRole')}</TableHead><TableHead>{t('labels.partyType')}</TableHead><TableHead>{t('labels.documentNumber')}</TableHead></TableRow></TableHeader><TableBody>{contract.parties.map((party) => <TableRow key={party.id}><TableCell>{party.displayName}</TableCell><TableCell>{formatEnumLabel(party.partyRole)}</TableCell><TableCell>{formatEnumLabel(party.partyType)}</TableCell><TableCell>{party.documentNumber || commonT('labels.notAvailable')}</TableCell></TableRow>)}</TableBody></Table></div> : <p className="text-sm text-muted-foreground">{t('states.noParties')}</p>}</SectionCard></TabsContent>
124
- <TabsContent value="signatures"><SectionCard title={t('sections.signatures')}>{contract.signatures.length ? <div className="overflow-x-auto rounded-md border"><Table><TableHeader><TableRow><TableHead>{t('labels.signer')}</TableHead><TableHead>{commonT('labels.status')}</TableHead><TableHead>{t('labels.signedAt')}</TableHead></TableRow></TableHeader><TableBody>{contract.signatures.map((signature) => <TableRow key={signature.id}><TableCell>{signature.signerName}</TableCell><TableCell><StatusBadge label={formatEnumLabel(signature.status)} className={getStatusBadgeClass(signature.status)} /></TableCell><TableCell>{formatDate(signature.signedAt)}</TableCell></TableRow>)}</TableBody></Table></div> : <p className="text-sm text-muted-foreground">{t('states.noSignatures')}</p>}</SectionCard></TabsContent>
125
- <TabsContent value="financials"><SectionCard title={t('sections.financials')}><dl className="grid gap-3 text-sm md:grid-cols-4"><div><dt className="text-muted-foreground">{t('labels.value')}</dt><dd className="font-medium">{formatCurrency(contract.valueAmount ?? 0)}</dd></div><div><dt className="text-muted-foreground">{t('labels.payment')}</dt><dd className="font-medium">{formatCurrency(contract.paymentAmount ?? 0)}</dd></div><div><dt className="text-muted-foreground">{t('labels.revenue')}</dt><dd className="font-medium">{formatCurrency(contract.revenueAmount ?? 0)}</dd></div><div><dt className="text-muted-foreground">{t('labels.fine')}</dt><dd className="font-medium">{formatCurrency(contract.fineAmount ?? 0)}</dd></div></dl></SectionCard></TabsContent>
126
- <TabsContent value="documents"><SectionCard title={t('sections.documents')}>{contract.documents.length ? <div className="space-y-3">{contract.documents.map((document) => <div key={document.id} className="flex items-center justify-between rounded-lg border px-4 py-3"><div><div className="font-medium">{document.fileName}</div><div className="text-xs text-muted-foreground">{formatEnumLabel(document.documentType)} • {formatDateTime(document.createdAt)}</div></div>{document.fileContentBase64 ? <Button variant="outline" size="sm" onClick={() => downloadBase64File(document.fileName, document.mimeType, document.fileContentBase64 || '')}><Download className="size-4" />{t('actions.download')}</Button> : null}</div>)}</div> : <p className="text-sm text-muted-foreground">{t('states.noDocuments')}</p>}</SectionCard></TabsContent>
127
- <TabsContent value="revisions"><SectionCard title={t('sections.revisions')}>{contract.revisions.length ? <div className="overflow-x-auto rounded-md border"><Table><TableHeader><TableRow><TableHead>{t('labels.revision')}</TableHead><TableHead>{commonT('labels.status')}</TableHead><TableHead>{commonT('labels.startDate')}</TableHead></TableRow></TableHeader><TableBody>{contract.revisions.map((revision) => <TableRow key={revision.id}><TableCell>{revision.title}</TableCell><TableCell><StatusBadge label={formatEnumLabel(revision.status)} className={getStatusBadgeClass(revision.status)} /></TableCell><TableCell>{formatDate(revision.effectiveDate)}</TableCell></TableRow>)}</TableBody></Table></div> : <p className="text-sm text-muted-foreground">{t('states.noRevisions')}</p>}</SectionCard></TabsContent>
128
- <TabsContent value="history"><SectionCard title={t('sections.history')}>{contract.history.length ? <div className="space-y-3">{contract.history.map((item) => <div key={item.id} className="rounded-lg border px-4 py-3 text-sm"><div className="font-medium">{formatEnumLabel(item.action)}</div><div className="text-muted-foreground">{formatDateTime(item.createdAt)}</div><div>{item.note || commonT('labels.noNotes')}</div></div>)}</div> : <p className="text-sm text-muted-foreground">{t('states.noHistory')}</p>}</SectionCard></TabsContent>
241
+ <TabsContent value="parties">
242
+ <SectionCard title={t('sections.parties')}>
243
+ {contract.parties.length ? (
244
+ <div className="overflow-x-auto rounded-md border">
245
+ <Table>
246
+ <TableHeader>
247
+ <TableRow>
248
+ <TableHead>{t('labels.mainParty')}</TableHead>
249
+ <TableHead>{t('labels.partyRole')}</TableHead>
250
+ <TableHead>{t('labels.partyType')}</TableHead>
251
+ <TableHead>{t('labels.documentNumber')}</TableHead>
252
+ </TableRow>
253
+ </TableHeader>
254
+ <TableBody>
255
+ {contract.parties.map((party) => (
256
+ <TableRow key={party.id}>
257
+ <TableCell>{party.displayName}</TableCell>
258
+ <TableCell>
259
+ {getOptionLabel('partyRoles', party.partyRole)}
260
+ </TableCell>
261
+ <TableCell>
262
+ {getOptionLabel('partyTypes', party.partyType)}
263
+ </TableCell>
264
+ <TableCell>
265
+ {party.documentNumber ||
266
+ commonT('labels.notAvailable')}
267
+ </TableCell>
268
+ </TableRow>
269
+ ))}
270
+ </TableBody>
271
+ </Table>
272
+ </div>
273
+ ) : (
274
+ <p className="text-sm text-muted-foreground">
275
+ {t('states.noParties')}
276
+ </p>
277
+ )}
278
+ </SectionCard>
279
+ </TabsContent>
280
+ <TabsContent value="signatures">
281
+ <SectionCard title={t('sections.signatures')}>
282
+ {contract.signatures.length ? (
283
+ <div className="overflow-x-auto rounded-md border">
284
+ <Table>
285
+ <TableHeader>
286
+ <TableRow>
287
+ <TableHead>{t('labels.signer')}</TableHead>
288
+ <TableHead>{commonT('labels.status')}</TableHead>
289
+ <TableHead>{t('labels.signedAt')}</TableHead>
290
+ </TableRow>
291
+ </TableHeader>
292
+ <TableBody>
293
+ {contract.signatures.map((signature) => (
294
+ <TableRow key={signature.id}>
295
+ <TableCell>{signature.signerName}</TableCell>
296
+ <TableCell>
297
+ <StatusBadge
298
+ label={getOptionLabel(
299
+ 'signatureStatuses',
300
+ signature.status
301
+ )}
302
+ className={getStatusBadgeClass(signature.status)}
303
+ />
304
+ </TableCell>
305
+ <TableCell>{formatDate(signature.signedAt)}</TableCell>
306
+ </TableRow>
307
+ ))}
308
+ </TableBody>
309
+ </Table>
310
+ </div>
311
+ ) : (
312
+ <p className="text-sm text-muted-foreground">
313
+ {t('states.noSignatures')}
314
+ </p>
315
+ )}
316
+ </SectionCard>
317
+ </TabsContent>
318
+ <TabsContent value="financials">
319
+ <SectionCard title={t('sections.financials')}>
320
+ <dl className="grid gap-3 text-sm md:grid-cols-4">
321
+ <div>
322
+ <dt className="text-muted-foreground">{t('labels.value')}</dt>
323
+ <dd className="font-medium">
324
+ {formatCurrency(contract.valueAmount ?? 0)}
325
+ </dd>
326
+ </div>
327
+ <div>
328
+ <dt className="text-muted-foreground">{t('labels.payment')}</dt>
329
+ <dd className="font-medium">
330
+ {formatCurrency(contract.paymentAmount ?? 0)}
331
+ </dd>
332
+ </div>
333
+ <div>
334
+ <dt className="text-muted-foreground">{t('labels.revenue')}</dt>
335
+ <dd className="font-medium">
336
+ {formatCurrency(contract.revenueAmount ?? 0)}
337
+ </dd>
338
+ </div>
339
+ <div>
340
+ <dt className="text-muted-foreground">{t('labels.fine')}</dt>
341
+ <dd className="font-medium">
342
+ {formatCurrency(contract.fineAmount ?? 0)}
343
+ </dd>
344
+ </div>
345
+ </dl>
346
+ </SectionCard>
347
+ </TabsContent>
348
+ <TabsContent value="documents">
349
+ <SectionCard title={t('sections.documents')}>
350
+ {contract.documents.length ? (
351
+ <div className="space-y-3">
352
+ {contract.documents.map((document) => (
353
+ <div
354
+ key={document.id}
355
+ className="flex items-center justify-between rounded-lg border px-4 py-3"
356
+ >
357
+ <div>
358
+ <div className="font-medium">{document.fileName}</div>
359
+ <div className="text-xs text-muted-foreground">
360
+ {getOptionLabel('documentTypes', document.documentType)}{' '}
361
+ • {formatDateTime(document.createdAt)}
362
+ </div>
363
+ </div>
364
+ {document.fileId || document.fileContentBase64 ? (
365
+ <Button
366
+ variant="outline"
367
+ size="sm"
368
+ onClick={() =>
369
+ document.fileId
370
+ ? openStoredFile(document.fileId)
371
+ : downloadBase64File(
372
+ document.fileName,
373
+ document.mimeType,
374
+ document.fileContentBase64 || ''
375
+ )
376
+ }
377
+ >
378
+ <Download className="size-4" />
379
+ {t('actions.download')}
380
+ </Button>
381
+ ) : null}
382
+ </div>
383
+ ))}
384
+ </div>
385
+ ) : (
386
+ <p className="text-sm text-muted-foreground">
387
+ {t('states.noDocuments')}
388
+ </p>
389
+ )}
390
+ </SectionCard>
391
+ </TabsContent>
392
+ <TabsContent value="revisions">
393
+ <SectionCard title={t('sections.revisions')}>
394
+ {contract.revisions.length ? (
395
+ <div className="overflow-x-auto rounded-md border">
396
+ <Table>
397
+ <TableHeader>
398
+ <TableRow>
399
+ <TableHead>{t('labels.revision')}</TableHead>
400
+ <TableHead>{commonT('labels.status')}</TableHead>
401
+ <TableHead>{commonT('labels.startDate')}</TableHead>
402
+ </TableRow>
403
+ </TableHeader>
404
+ <TableBody>
405
+ {contract.revisions.map((revision) => (
406
+ <TableRow key={revision.id}>
407
+ <TableCell>{revision.title}</TableCell>
408
+ <TableCell>
409
+ <StatusBadge
410
+ label={getOptionLabel('statuses', revision.status)}
411
+ className={getStatusBadgeClass(revision.status)}
412
+ />
413
+ </TableCell>
414
+ <TableCell>
415
+ {formatDate(revision.effectiveDate)}
416
+ </TableCell>
417
+ </TableRow>
418
+ ))}
419
+ </TableBody>
420
+ </Table>
421
+ </div>
422
+ ) : (
423
+ <p className="text-sm text-muted-foreground">
424
+ {t('states.noRevisions')}
425
+ </p>
426
+ )}
427
+ </SectionCard>
428
+ </TabsContent>
429
+ <TabsContent value="history">
430
+ <SectionCard title={t('sections.history')}>
431
+ {contract.history.length ? (
432
+ <div className="space-y-3">
433
+ {contract.history.map((item) => (
434
+ <div
435
+ key={item.id}
436
+ className="rounded-lg border px-4 py-3 text-sm"
437
+ >
438
+ <div className="font-medium">
439
+ {getOptionLabel('historyActions', item.action)}
440
+ </div>
441
+ <div className="text-muted-foreground">
442
+ {formatDateTime(item.createdAt)}
443
+ </div>
444
+ <div>{item.note || commonT('labels.noNotes')}</div>
445
+ </div>
446
+ ))}
447
+ </div>
448
+ ) : (
449
+ <p className="text-sm text-muted-foreground">
450
+ {t('states.noHistory')}
451
+ </p>
452
+ )}
453
+ </SectionCard>
454
+ </TabsContent>
129
455
  </Tabs>
130
456
  </Page>
131
457
  );