@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
@@ -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
  );