@hed-hog/operations 0.0.306 → 0.0.309

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 (123) hide show
  1. package/dist/controllers/operations-approvals.controller.d.ts +114 -1
  2. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-approvals.controller.js +16 -3
  4. package/dist/controllers/operations-approvals.controller.js.map +1 -1
  5. package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +16 -3
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +14 -453
  10. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-contracts.controller.js +11 -112
  12. package/dist/controllers/operations-contracts.controller.js.map +1 -1
  13. package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
  14. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
  15. package/dist/controllers/operations-org-structure.controller.js +18 -5
  16. package/dist/controllers/operations-org-structure.controller.js.map +1 -1
  17. package/dist/controllers/operations-projects.controller.d.ts +28 -4
  18. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-projects.controller.js +17 -5
  20. package/dist/controllers/operations-projects.controller.js.map +1 -1
  21. package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +16 -11
  24. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  25. package/dist/dto/list-approvals.dto.d.ts +6 -0
  26. package/dist/dto/list-approvals.dto.d.ts.map +1 -0
  27. package/dist/dto/list-approvals.dto.js +28 -0
  28. package/dist/dto/list-approvals.dto.js.map +1 -0
  29. package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
  30. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
  31. package/dist/dto/list-collaborator-types.dto.js +7 -1
  32. package/dist/dto/list-collaborator-types.dto.js.map +1 -1
  33. package/dist/dto/list-collaborators.dto.d.ts +1 -0
  34. package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
  35. package/dist/dto/list-collaborators.dto.js +5 -0
  36. package/dist/dto/list-collaborators.dto.js.map +1 -1
  37. package/dist/dto/list-contracts.dto.d.ts +8 -0
  38. package/dist/dto/list-contracts.dto.d.ts.map +1 -0
  39. package/dist/dto/list-contracts.dto.js +38 -0
  40. package/dist/dto/list-contracts.dto.js.map +1 -0
  41. package/dist/dto/list-departments.dto.d.ts +5 -0
  42. package/dist/dto/list-departments.dto.d.ts.map +1 -0
  43. package/dist/dto/list-departments.dto.js +23 -0
  44. package/dist/dto/list-departments.dto.js.map +1 -0
  45. package/dist/dto/list-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-projects.dto.js +23 -0
  48. package/dist/dto/list-projects.dto.js.map +1 -0
  49. package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
  50. package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
  51. package/dist/dto/list-schedule-adjustments.dto.js +23 -0
  52. package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
  53. package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
  54. package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
  55. package/dist/dto/list-time-off-requests.dto.js +23 -0
  56. package/dist/dto/list-time-off-requests.dto.js.map +1 -0
  57. package/dist/dto/list-timesheets.dto.d.ts +5 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
  59. package/dist/dto/list-timesheets.dto.js +23 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -0
  61. package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
  62. package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
  63. package/dist/dto/reorder-collaborator-types.dto.js +25 -0
  64. package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
  65. package/dist/operations.service.d.ts +340 -271
  66. package/dist/operations.service.d.ts.map +1 -1
  67. package/dist/operations.service.js +1007 -1043
  68. package/dist/operations.service.js.map +1 -1
  69. package/dist/operations.service.spec.js +0 -22
  70. package/dist/operations.service.spec.js.map +1 -1
  71. package/hedhog/data/menu.yaml +0 -36
  72. package/hedhog/data/route.yaml +42 -73
  73. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  74. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  75. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  76. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  77. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  78. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  79. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  80. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  81. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  82. package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
  83. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
  84. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  85. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  87. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  88. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  89. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
  90. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  91. package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
  92. package/hedhog/frontend/messages/en.json +143 -14
  93. package/hedhog/frontend/messages/pt.json +192 -54
  94. package/hedhog/table/operations_contract.yaml +0 -9
  95. package/package.json +5 -5
  96. package/src/controllers/operations-approvals.controller.ts +9 -3
  97. package/src/controllers/operations-collaborators.controller.ts +15 -2
  98. package/src/controllers/operations-contracts.controller.ts +8 -92
  99. package/src/controllers/operations-org-structure.controller.ts +17 -4
  100. package/src/controllers/operations-projects.controller.ts +10 -4
  101. package/src/controllers/operations-timesheets.controller.ts +17 -8
  102. package/src/dto/list-approvals.dto.ts +12 -0
  103. package/src/dto/list-collaborator-types.dto.ts +7 -2
  104. package/src/dto/list-collaborators.dto.ts +4 -0
  105. package/src/dto/list-contracts.dto.ts +20 -0
  106. package/src/dto/list-departments.dto.ts +8 -0
  107. package/src/dto/list-projects.dto.ts +8 -0
  108. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  109. package/src/dto/list-time-off-requests.dto.ts +8 -0
  110. package/src/dto/list-timesheets.dto.ts +8 -0
  111. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  112. package/src/operations.service.spec.ts +0 -30
  113. package/src/operations.service.ts +1557 -1806
  114. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  115. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  116. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  117. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  118. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  119. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  120. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  121. package/hedhog/table/operations_contract_revision.yaml +0 -38
  122. package/hedhog/table/operations_contract_signature.yaml +0 -38
  123. package/hedhog/table/operations_contract_template.yaml +0 -58
@@ -2,36 +2,22 @@
2
2
 
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
4
  import { Button } from '@/components/ui/button';
5
- import { FormActions } from '@/components/ui/form-actions';
6
5
  import { Input } from '@/components/ui/input';
7
6
  import { Label } from '@/components/ui/label';
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from '@/components/ui/select';
15
7
  import { Switch } from '@/components/ui/switch';
16
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
17
- import { Textarea } from '@/components/ui/textarea';
18
8
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
19
- import { ArrowLeft, FileText, Plus, Save } from 'lucide-react';
9
+ import { Download, FileText, Save, Upload } from 'lucide-react';
20
10
  import { useTranslations } from 'next-intl';
21
11
  import Link from 'next/link';
22
12
  import { useRouter } from 'next/navigation';
23
- import { type ReactNode, useEffect, useState } from 'react';
13
+ import { useEffect, useMemo, useState } from 'react';
24
14
  import { fetchOperations, mutateOperations } from '../_lib/api';
25
15
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
26
- import type {
27
- OperationsCollaborator,
28
- OperationsContractDetails,
29
- OperationsContractTemplate,
30
- } from '../_lib/types';
16
+ import type { OperationsContractDetails } from '../_lib/types';
31
17
  import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
32
- import { ContractContentEditor } from './contract-content-editor';
33
18
  import { OperationsHeader } from './operations-header';
34
19
  import { SectionCard } from './section-card';
20
+ import { StatusBadge } from './status-badge';
35
21
 
36
22
  type PartyState = {
37
23
  displayName: string;
@@ -43,36 +29,10 @@ type PartyState = {
43
29
  isPrimary: boolean;
44
30
  };
45
31
 
46
- type SignatureState = {
47
- signerName: string;
48
- signerRole: string;
49
- signerEmail: string;
50
- status: string;
51
- signedAt: string;
52
- };
53
-
54
- type FinancialTermState = {
55
- label: string;
56
- termType: string;
57
- amount: string;
58
- recurrence: string;
59
- dueDay: string;
60
- notes: string;
61
- };
62
-
63
- type RevisionState = {
64
- title: string;
65
- revisionType: string;
66
- effectiveDate: string;
67
- status: string;
68
- summary: string;
69
- };
70
-
71
32
  export type ContractFormState = {
72
33
  code: string;
73
34
  name: string;
74
35
  clientName: string;
75
- contractTemplateId: string;
76
36
  contractCategory: string;
77
37
  contractType: string;
78
38
  signatureStatus: string;
@@ -92,13 +52,11 @@ export type ContractFormState = {
92
52
  description: string;
93
53
  contentHtml: string;
94
54
  parties: PartyState[];
95
- signatures: SignatureState[];
96
- financialTerms: FinancialTermState[];
97
- revisions: RevisionState[];
98
55
  pdfDocument: {
56
+ fileId?: number | null;
99
57
  fileName: string;
100
58
  mimeType: string;
101
- fileContentBase64: string;
59
+ fileContentBase64?: string | null;
102
60
  } | null;
103
61
  };
104
62
 
@@ -114,43 +72,11 @@ function emptyParty(): PartyState {
114
72
  };
115
73
  }
116
74
 
117
- function emptySignature(): SignatureState {
118
- return {
119
- signerName: '',
120
- signerRole: '',
121
- signerEmail: '',
122
- status: 'pending',
123
- signedAt: '',
124
- };
125
- }
126
-
127
- function emptyFinancialTerm(): FinancialTermState {
128
- return {
129
- label: '',
130
- termType: 'value',
131
- amount: '',
132
- recurrence: 'one_time',
133
- dueDay: '',
134
- notes: '',
135
- };
136
- }
137
-
138
- function emptyRevision(): RevisionState {
139
- return {
140
- title: '',
141
- revisionType: 'revision',
142
- effectiveDate: '',
143
- status: 'draft',
144
- summary: '',
145
- };
146
- }
147
-
148
75
  function buildEmptyForm(): ContractFormState {
149
76
  return {
150
77
  code: '',
151
78
  name: '',
152
79
  clientName: '',
153
- contractTemplateId: 'none',
154
80
  contractCategory: 'client',
155
81
  contractType: 'service_agreement',
156
82
  signatureStatus: 'not_started',
@@ -166,13 +92,10 @@ function buildEmptyForm(): ContractFormState {
166
92
  effectiveDate: '',
167
93
  budgetAmount: '',
168
94
  monthlyHourCap: '',
169
- status: 'draft',
95
+ status: 'active',
170
96
  description: '',
171
97
  contentHtml: '',
172
98
  parties: [emptyParty()],
173
- signatures: [emptySignature()],
174
- financialTerms: [emptyFinancialTerm()],
175
- revisions: [],
176
99
  pdfDocument: null,
177
100
  };
178
101
  }
@@ -182,9 +105,6 @@ function toFormState(contract: OperationsContractDetails): ContractFormState {
182
105
  code: contract.code ?? '',
183
106
  name: contract.name ?? '',
184
107
  clientName: contract.clientName ?? '',
185
- contractTemplateId: contract.contractTemplateId
186
- ? String(contract.contractTemplateId)
187
- : 'none',
188
108
  contractCategory: contract.contractCategory ?? 'client',
189
109
  contractType: contract.contractType ?? 'service_agreement',
190
110
  signatureStatus: contract.signatureStatus ?? 'not_started',
@@ -197,7 +117,10 @@ function toFormState(contract: OperationsContractDetails): ContractFormState {
197
117
  ? String(contract.relatedCollaboratorId)
198
118
  : 'none',
199
119
  originType: contract.originType ?? 'manual',
200
- originId: contract.originId ? String(contract.originId) : '',
120
+ originId:
121
+ contract.originId !== null && contract.originId !== undefined
122
+ ? String(contract.originId)
123
+ : '',
201
124
  startDate: contract.startDate ?? '',
202
125
  endDate: contract.endDate ?? '',
203
126
  signedAt: contract.signedAt ?? '',
@@ -210,7 +133,7 @@ function toFormState(contract: OperationsContractDetails): ContractFormState {
210
133
  contract.monthlyHourCap !== null && contract.monthlyHourCap !== undefined
211
134
  ? String(contract.monthlyHourCap)
212
135
  : '',
213
- status: contract.status ?? 'draft',
136
+ status: contract.status ?? 'active',
214
137
  description: contract.description ?? '',
215
138
  contentHtml: contract.contentHtml ?? '',
216
139
  parties: contract.parties.length
@@ -224,40 +147,11 @@ function toFormState(contract: OperationsContractDetails): ContractFormState {
224
147
  isPrimary: party.isPrimary ?? false,
225
148
  }))
226
149
  : [emptyParty()],
227
- signatures: contract.signatures.length
228
- ? contract.signatures.map((signature) => ({
229
- signerName: signature.signerName ?? '',
230
- signerRole: signature.signerRole ?? '',
231
- signerEmail: signature.signerEmail ?? '',
232
- status: signature.status ?? 'pending',
233
- signedAt: signature.signedAt ?? '',
234
- }))
235
- : [emptySignature()],
236
- financialTerms: contract.financialTerms.length
237
- ? contract.financialTerms.map((term) => ({
238
- label: term.label ?? '',
239
- termType: term.termType ?? 'value',
240
- amount: String(term.amount ?? ''),
241
- recurrence: term.recurrence ?? 'one_time',
242
- dueDay:
243
- term.dueDay !== null && term.dueDay !== undefined
244
- ? String(term.dueDay)
245
- : '',
246
- notes: term.notes ?? '',
247
- }))
248
- : [emptyFinancialTerm()],
249
- revisions: contract.revisions.map((revision) => ({
250
- title: revision.title ?? '',
251
- revisionType: revision.revisionType ?? 'revision',
252
- effectiveDate: revision.effectiveDate ?? '',
253
- status: revision.status ?? 'draft',
254
- summary: revision.summary ?? '',
255
- })),
256
150
  pdfDocument: null,
257
151
  };
258
152
  }
259
153
 
260
- async function fileToBase64(file: File) {
154
+ function fileToBase64(file: File) {
261
155
  return new Promise<string>((resolve, reject) => {
262
156
  const reader = new FileReader();
263
157
  reader.onload = () => {
@@ -270,6 +164,26 @@ async function fileToBase64(file: File) {
270
164
  });
271
165
  }
272
166
 
167
+ function buildStoredFileUrl(fileId?: number | null) {
168
+ if (!fileId) {
169
+ return null;
170
+ }
171
+
172
+ const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
173
+ return `${baseUrl}/file/open/${fileId}`;
174
+ }
175
+
176
+ function downloadBase64File(
177
+ fileName: string,
178
+ mimeType: string,
179
+ base64: string
180
+ ) {
181
+ const link = document.createElement('a');
182
+ link.href = `data:${mimeType};base64,${base64}`;
183
+ link.download = fileName;
184
+ link.click();
185
+ }
186
+
273
187
  export function ContractFormScreen({
274
188
  contractId,
275
189
  duplicateFromId,
@@ -285,45 +199,20 @@ export function ContractFormScreen({
285
199
  onSaved?: (contract: OperationsContractDetails) => void | Promise<void>;
286
200
  onCancel?: () => void;
287
201
  }) {
202
+ void initialTemplateId;
203
+
288
204
  const t = useTranslations('operations.ContractFormPage');
289
205
  const commonT = useTranslations('operations.Common');
206
+ const detailT = useTranslations('operations.ContractDetailsPage');
290
207
  const { request, showToastHandler, currentLocaleCode } = useApp();
291
208
  const access = useOperationsAccess();
292
209
  const router = useRouter();
210
+ const sourceId = contractId ?? duplicateFromId;
211
+ const isSheetMode = Boolean(onCancel);
293
212
  const [form, setForm] = useState<ContractFormState>(() => ({
294
213
  ...buildEmptyForm(),
295
214
  ...initialValues,
296
215
  }));
297
- const isSheetMode = Boolean(onCancel);
298
-
299
- const sourceId = contractId ?? duplicateFromId;
300
-
301
- const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
302
- queryKey: ['operations-contract-form-collaborators', currentLocaleCode],
303
- enabled: access.isDirector,
304
- queryFn: () =>
305
- fetchOperations<OperationsCollaborator[]>(
306
- request,
307
- '/operations/collaborators'
308
- ),
309
- });
310
-
311
- const { data: contractTemplates = [] } = useQuery<
312
- OperationsContractTemplate[]
313
- >({
314
- queryKey: ['operations-contract-form-templates', currentLocaleCode],
315
- enabled: access.isDirector,
316
- queryFn: () =>
317
- fetchOperations<OperationsContractTemplate[]>(
318
- request,
319
- '/operations/contract-templates'
320
- ),
321
- });
322
-
323
- const selectedContractTemplate =
324
- contractTemplates.find(
325
- (template) => String(template.id) === form.contractTemplateId
326
- ) ?? null;
327
216
 
328
217
  const { data: contract, isLoading: isLoadingContract } =
329
218
  useQuery<OperationsContractDetails>({
@@ -342,90 +231,83 @@ export function ContractFormScreen({
342
231
  }
343
232
 
344
233
  const next = toFormState(contract);
234
+
345
235
  if (duplicateFromId) {
346
- next.code = `${contract.code}-COPY`;
236
+ const currentDocument =
237
+ contract.documents.find(
238
+ (document) =>
239
+ document.isCurrent &&
240
+ ['source_upload', 'generated_pdf'].includes(document.documentType)
241
+ ) ?? null;
242
+
243
+ next.code = '';
347
244
  next.name = `${contract.name || contract.code || 'Contrato'} Copy`;
348
- next.originType = 'manual';
349
- next.originId = '';
350
- next.status = 'draft';
351
- next.signatureStatus = 'not_started';
352
- next.signedAt = '';
353
245
  next.isActive = true;
246
+ next.status = 'active';
247
+ next.pdfDocument = currentDocument
248
+ ? {
249
+ fileId: currentDocument.fileId ?? null,
250
+ fileName: currentDocument.fileName,
251
+ mimeType: currentDocument.mimeType,
252
+ fileContentBase64: currentDocument.fileContentBase64 ?? null,
253
+ }
254
+ : null;
354
255
  }
355
- // eslint-disable-next-line react-hooks/set-state-in-effect
356
- setForm(next);
256
+
257
+ setForm({
258
+ ...next,
259
+ pdfDocument: next.pdfDocument,
260
+ });
357
261
  }, [contract, duplicateFromId]);
358
262
 
359
- const updateArrayItem = <T,>(
360
- key: 'parties' | 'signatures' | 'financialTerms' | 'revisions',
361
- index: number,
362
- patch: Partial<T>
363
- ) => {
364
- setForm((current) => ({
365
- ...current,
366
- [key]: (current[key] as T[]).map((item, itemIndex) =>
367
- itemIndex === index ? { ...item, ...patch } : item
368
- ),
369
- }));
370
- };
263
+ const currentDocument = useMemo(() => {
264
+ if (!contract) {
265
+ return null;
266
+ }
267
+
268
+ return (
269
+ contract.documents.find(
270
+ (document) =>
271
+ document.isCurrent &&
272
+ ['source_upload', 'generated_pdf'].includes(document.documentType)
273
+ ) ??
274
+ contract.documents.find((document) => document.isCurrent) ??
275
+ null
276
+ );
277
+ }, [contract]);
371
278
 
372
- const handleTemplateSelection = (value: string) => {
373
- const template =
374
- value === 'none'
375
- ? null
376
- : (contractTemplates.find((item) => String(item.id) === value) ?? null);
279
+ const previewSource = useMemo(() => {
280
+ if (form.pdfDocument?.fileContentBase64 && form.pdfDocument.mimeType) {
281
+ return `data:${form.pdfDocument.mimeType};base64,${form.pdfDocument.fileContentBase64}`;
282
+ }
377
283
 
378
- setForm((current) => ({
379
- ...current,
380
- contractTemplateId: value,
381
- contractCategory: template?.contractCategory ?? current.contractCategory,
382
- contractType: template?.contractType ?? current.contractType,
383
- billingModel: template?.billingModel ?? current.billingModel,
384
- signatureStatus: template?.signatureStatus ?? current.signatureStatus,
385
- name: current.name || template?.name || '',
386
- description: current.description || template?.description || '',
387
- contentHtml: template?.contentHtml ?? current.contentHtml,
388
- }));
389
- };
284
+ if (form.pdfDocument?.fileId) {
285
+ return buildStoredFileUrl(form.pdfDocument.fileId);
286
+ }
390
287
 
391
- useEffect(() => {
392
- if (
393
- !initialTemplateId ||
394
- sourceId ||
395
- !contractTemplates.length ||
396
- form.contractTemplateId !== 'none'
397
- ) {
398
- return;
288
+ if (currentDocument?.fileContentBase64 && currentDocument.mimeType) {
289
+ return `data:${currentDocument.mimeType};base64,${currentDocument.fileContentBase64}`;
399
290
  }
400
291
 
401
- const template = contractTemplates.find(
402
- (item) => item.id === initialTemplateId
403
- );
292
+ return buildStoredFileUrl(currentDocument?.fileId);
293
+ }, [currentDocument, form.pdfDocument]);
404
294
 
405
- // eslint-disable-next-line react-hooks/set-state-in-effect
406
- setForm((current) => ({
407
- ...current,
408
- contractTemplateId: String(initialTemplateId),
409
- contractCategory: template?.contractCategory ?? current.contractCategory,
410
- contractType: template?.contractType ?? current.contractType,
411
- billingModel: template?.billingModel ?? current.billingModel,
412
- signatureStatus: template?.signatureStatus ?? current.signatureStatus,
413
- name: current.name || template?.name || '',
414
- description: current.description || template?.description || '',
415
- contentHtml: template?.contentHtml ?? current.contentHtml,
416
- }));
417
- }, [contractTemplates, form.contractTemplateId, initialTemplateId, sourceId]);
295
+ const canPreview =
296
+ Boolean(previewSource) &&
297
+ (form.pdfDocument?.mimeType?.includes('pdf') ||
298
+ currentDocument?.mimeType?.includes('pdf'));
418
299
 
419
- const onSubmit = async () => {
420
- if (!form.code.trim() || !form.name.trim() || !form.clientName.trim()) {
300
+ const handleSubmit = async () => {
301
+ if (!form.name.trim()) {
421
302
  showToastHandler?.('error', t('messages.requiredFields'));
422
303
  return;
423
304
  }
424
305
 
306
+ const nextStatus = form.isActive ? 'active' : 'archived';
425
307
  const payload = {
426
- code: form.code.trim(),
308
+ code: trimToNull(form.code),
427
309
  name: form.name.trim(),
428
- clientName: form.clientName.trim(),
310
+ clientName: trimToNull(form.clientName),
429
311
  contractCategory: form.contractCategory,
430
312
  contractType: form.contractType,
431
313
  signatureStatus: form.signatureStatus,
@@ -439,19 +321,15 @@ export function ContractFormScreen({
439
321
  form.relatedCollaboratorId === 'none'
440
322
  ? null
441
323
  : parseNumberInput(form.relatedCollaboratorId),
442
- contractTemplateId:
443
- form.contractTemplateId === 'none'
444
- ? null
445
- : parseNumberInput(form.contractTemplateId),
446
324
  originType: form.originType,
447
325
  originId: parseNumberInput(form.originId),
448
- startDate: form.startDate,
326
+ startDate: trimToNull(form.startDate),
449
327
  endDate: trimToNull(form.endDate),
450
328
  signedAt: trimToNull(form.signedAt),
451
329
  effectiveDate: trimToNull(form.effectiveDate),
452
330
  budgetAmount: parseNumberInput(form.budgetAmount),
453
331
  monthlyHourCap: parseNumberInput(form.monthlyHourCap),
454
- status: form.status,
332
+ status: nextStatus,
455
333
  description: trimToNull(form.description),
456
334
  contentHtml: trimToNull(form.contentHtml),
457
335
  parties: form.parties
@@ -465,34 +343,6 @@ export function ContractFormScreen({
465
343
  phone: trimToNull(party.phone),
466
344
  isPrimary: party.isPrimary,
467
345
  })),
468
- signatures: form.signatures
469
- .filter((signature) => signature.signerName.trim())
470
- .map((signature) => ({
471
- signerName: signature.signerName.trim(),
472
- signerRole: trimToNull(signature.signerRole),
473
- signerEmail: trimToNull(signature.signerEmail),
474
- status: signature.status,
475
- signedAt: trimToNull(signature.signedAt),
476
- })),
477
- financialTerms: form.financialTerms
478
- .filter((term) => term.label.trim())
479
- .map((term) => ({
480
- label: term.label.trim(),
481
- termType: term.termType,
482
- amount: parseNumberInput(term.amount) ?? 0,
483
- recurrence: term.recurrence,
484
- dueDay: parseNumberInput(term.dueDay),
485
- notes: trimToNull(term.notes),
486
- })),
487
- revisions: form.revisions
488
- .filter((revision) => revision.title.trim())
489
- .map((revision) => ({
490
- title: revision.title.trim(),
491
- revisionType: revision.revisionType,
492
- effectiveDate: trimToNull(revision.effectiveDate),
493
- status: revision.status,
494
- summary: trimToNull(revision.summary),
495
- })),
496
346
  replaceUploadedPdfDocument: form.pdfDocument,
497
347
  };
498
348
 
@@ -557,1841 +407,203 @@ export function ContractFormScreen({
557
407
  );
558
408
  }
559
409
 
560
- const sectionDescriptionMode = isSheetMode ? 'tooltip' : 'inline';
561
- const overviewGridClass = isSheetMode
562
- ? 'gap-3 md:grid-cols-2 xl:grid-cols-12'
563
- : 'gap-4 md:grid-cols-3';
564
- const sectionCardClass = isSheetMode ? 'space-y-2.5' : 'space-y-4';
565
- const compactGridClass = isSheetMode
566
- ? 'gap-3 p-3 md:grid-cols-2 xl:grid-cols-6'
567
- : 'gap-4 p-4 md:grid-cols-3';
568
- const compactTwoColGridClass = isSheetMode
569
- ? 'gap-3 p-3 md:grid-cols-2 xl:grid-cols-6'
570
- : 'gap-4 p-4 md:grid-cols-2';
571
- const fieldGroupClass = isSheetMode ? 'space-y-1.5' : 'space-y-2';
572
- const fieldLabelClass = isSheetMode
573
- ? 'px-0.5 text-[10px] uppercase tracking-wide text-muted-foreground'
574
- : 'px-0.5 text-[11px] text-muted-foreground';
575
- const helpTextClass = isSheetMode
576
- ? 'px-0.5 text-[11px] leading-relaxed text-muted-foreground'
577
- : 'px-0.5 text-xs leading-relaxed text-muted-foreground';
578
-
579
- const renderField = (
580
- label: string,
581
- content: ReactNode,
582
- options?: {
583
- className?: string;
584
- description?: string;
585
- }
586
- ) => (
587
- <div
588
- className={[fieldGroupClass, options?.className]
589
- .filter(Boolean)
590
- .join(' ')}
591
- >
592
- <Label className={fieldLabelClass}>{label}</Label>
593
- {content}
594
- {options?.description ? (
595
- <p className={helpTextClass}>{options.description}</p>
596
- ) : null}
597
- </div>
598
- );
599
-
600
- const overviewSection = (
601
- <SectionCard
602
- title={t('sections.overview')}
603
- description={t('sections.overviewDescription')}
604
- compact={isSheetMode}
605
- descriptionMode={sectionDescriptionMode}
606
- >
607
- <div className={`grid ${overviewGridClass}`}>
608
- {renderField(
609
- t('fields.code'),
610
- <Input
611
- placeholder={t('fields.code')}
612
- value={form.code}
613
- onChange={(e) => setForm((c) => ({ ...c, code: e.target.value }))}
614
- />,
615
- { className: isSheetMode ? 'xl:col-span-3' : '' }
616
- )}
617
-
618
- {renderField(
619
- t('fields.name'),
620
- <Input
621
- placeholder={t('fields.name')}
622
- value={form.name}
623
- onChange={(e) => setForm((c) => ({ ...c, name: e.target.value }))}
624
- />,
625
- {
626
- className: isSheetMode
627
- ? 'md:col-span-2 xl:col-span-9'
628
- : 'md:col-span-2',
629
- }
630
- )}
631
-
632
- {renderField(
633
- t('fields.clientName'),
634
- <Input
635
- placeholder={t('fields.clientName')}
636
- value={form.clientName}
637
- onChange={(e) =>
638
- setForm((c) => ({ ...c, clientName: e.target.value }))
639
- }
640
- />,
641
- { className: isSheetMode ? 'md:col-span-2 xl:col-span-6' : '' }
642
- )}
643
-
644
- {renderField(
645
- t('fields.contractTemplate'),
646
- <div
647
- title={
648
- isSheetMode ? t('fields.contractTemplateDescription') : undefined
649
- }
650
- className={
651
- isSheetMode
652
- ? 'space-y-2 rounded-lg border bg-muted/15 px-3 py-2.5'
653
- : 'space-y-2.5 rounded-lg border px-4 py-3'
654
- }
655
- >
656
- <div className="flex flex-wrap items-center justify-between gap-2">
657
- <p className="text-[11px] text-muted-foreground">
658
- {t('fields.contractTemplateDescription')}
659
- </p>
660
- <Button type="button" variant="outline" size="sm" asChild>
661
- <Link href="/operations/contracts/templates">
662
- {commonT('actions.manageTemplates')}
663
- </Link>
664
- </Button>
665
- </div>
666
- <Select
667
- value={form.contractTemplateId}
668
- onValueChange={handleTemplateSelection}
669
- >
670
- <SelectTrigger>
671
- <SelectValue placeholder={t('fields.contractTemplate')} />
672
- </SelectTrigger>
673
- <SelectContent>
674
- <SelectItem value="none">
675
- {commonT('labels.notAssigned')}
676
- </SelectItem>
677
- {contractTemplates.map((template) => (
678
- <SelectItem key={template.id} value={String(template.id)}>
679
- {template.name}
680
- </SelectItem>
681
- ))}
682
- </SelectContent>
683
- </Select>
684
- {selectedContractTemplate ? (
685
- <div className="line-clamp-2 text-[11px] text-muted-foreground">
686
- {[
687
- selectedContractTemplate.code,
688
- selectedContractTemplate.description,
689
- ]
690
- .filter(Boolean)
691
- .join(' • ')}
692
- </div>
693
- ) : null}
694
- </div>,
695
- {
696
- className: isSheetMode
697
- ? 'md:col-span-2 xl:col-span-6'
698
- : 'md:col-span-2',
699
- }
700
- )}
701
-
702
- {renderField(
703
- t('fields.contractCategory'),
704
- <Select
705
- value={form.contractCategory}
706
- onValueChange={(value) =>
707
- setForm((c) => ({ ...c, contractCategory: value }))
708
- }
709
- >
710
- <SelectTrigger>
711
- <SelectValue placeholder={t('fields.contractCategory')} />
712
- </SelectTrigger>
713
- <SelectContent>
714
- {[
715
- 'employee',
716
- 'contractor',
717
- 'client',
718
- 'supplier',
719
- 'vendor',
720
- 'partner',
721
- 'internal',
722
- 'other',
723
- ].map((value) => (
724
- <SelectItem key={value} value={value}>
725
- {value}
726
- </SelectItem>
727
- ))}
728
- </SelectContent>
729
- </Select>,
730
- { className: isSheetMode ? 'xl:col-span-3' : '' }
731
- )}
732
-
733
- {renderField(
734
- t('fields.contractType'),
735
- <Select
736
- value={form.contractType}
737
- onValueChange={(value) =>
738
- setForm((c) => ({ ...c, contractType: value }))
739
- }
740
- >
741
- <SelectTrigger>
742
- <SelectValue placeholder={t('fields.contractType')} />
743
- </SelectTrigger>
744
- <SelectContent>
745
- {[
746
- 'clt',
747
- 'pj',
748
- 'freelancer_agreement',
749
- 'service_agreement',
750
- 'fixed_term',
751
- 'recurring_service',
752
- 'nda',
753
- 'amendment',
754
- 'addendum',
755
- 'other',
756
- ].map((value) => (
757
- <SelectItem key={value} value={value}>
758
- {value}
759
- </SelectItem>
760
- ))}
761
- </SelectContent>
762
- </Select>,
763
- { className: isSheetMode ? 'xl:col-span-3' : '' }
764
- )}
765
-
766
- {renderField(
767
- t('fields.originType'),
768
- <Select
769
- value={form.originType}
770
- onValueChange={(value) =>
771
- setForm((c) => ({ ...c, originType: value }))
772
- }
773
- >
774
- <SelectTrigger>
775
- <SelectValue placeholder={t('fields.originType')} />
776
- </SelectTrigger>
777
- <SelectContent>
778
- {['manual', 'employee_hiring', 'client_project'].map((value) => (
779
- <SelectItem key={value} value={value}>
780
- {value}
781
- </SelectItem>
782
- ))}
783
- </SelectContent>
784
- </Select>,
785
- { className: isSheetMode ? 'xl:col-span-3' : '' }
786
- )}
787
-
788
- {renderField(
789
- t('fields.originId'),
790
- <Input
791
- placeholder={t('fields.originId')}
792
- value={form.originId}
793
- onChange={(e) =>
794
- setForm((c) => ({ ...c, originId: e.target.value }))
795
- }
796
- />,
797
- { className: isSheetMode ? 'xl:col-span-3' : '' }
798
- )}
799
-
800
- {renderField(
801
- commonT('labels.billingModel'),
802
- <Select
803
- value={form.billingModel}
804
- onValueChange={(value) =>
805
- setForm((c) => ({ ...c, billingModel: value }))
806
- }
807
- >
808
- <SelectTrigger>
809
- <SelectValue placeholder={commonT('labels.billingModel')} />
810
- </SelectTrigger>
811
- <SelectContent>
812
- {['time_and_material', 'monthly_retainer', 'fixed_price'].map(
813
- (value) => (
814
- <SelectItem key={value} value={value}>
815
- {value}
816
- </SelectItem>
817
- )
818
- )}
819
- </SelectContent>
820
- </Select>,
821
- { className: isSheetMode ? 'xl:col-span-4' : '' }
822
- )}
823
-
824
- {renderField(
825
- t('fields.signatureStatus'),
826
- <Select
827
- value={form.signatureStatus}
828
- onValueChange={(value) =>
829
- setForm((c) => ({ ...c, signatureStatus: value }))
830
- }
831
- >
832
- <SelectTrigger>
833
- <SelectValue placeholder={t('fields.signatureStatus')} />
834
- </SelectTrigger>
835
- <SelectContent>
836
- {[
837
- 'not_started',
838
- 'pending',
839
- 'partially_signed',
840
- 'signed',
841
- 'expired',
842
- ].map((value) => (
843
- <SelectItem key={value} value={value}>
844
- {value}
845
- </SelectItem>
846
- ))}
847
- </SelectContent>
848
- </Select>,
849
- { className: isSheetMode ? 'xl:col-span-4' : '' }
850
- )}
851
-
852
- {renderField(
853
- commonT('labels.status'),
854
- <Select
855
- value={form.status}
856
- onValueChange={(value) => setForm((c) => ({ ...c, status: value }))}
857
- >
858
- <SelectTrigger>
859
- <SelectValue placeholder={commonT('labels.status')} />
860
- </SelectTrigger>
861
- <SelectContent>
862
- {[
863
- 'draft',
864
- 'under_review',
865
- 'active',
866
- 'renewal',
867
- 'expired',
868
- 'closed',
869
- 'archived',
870
- ].map((value) => (
871
- <SelectItem key={value} value={value}>
872
- {value}
873
- </SelectItem>
874
- ))}
875
- </SelectContent>
876
- </Select>,
877
- { className: isSheetMode ? 'xl:col-span-4' : '' }
878
- )}
879
-
880
- {renderField(
881
- commonT('labels.accountManager'),
882
- <Select
883
- value={form.accountManagerCollaboratorId}
884
- onValueChange={(value) =>
885
- setForm((c) => ({ ...c, accountManagerCollaboratorId: value }))
886
- }
887
- >
888
- <SelectTrigger>
889
- <SelectValue placeholder={commonT('labels.accountManager')} />
890
- </SelectTrigger>
891
- <SelectContent>
892
- <SelectItem value="none">
893
- {commonT('labels.notAssigned')}
894
- </SelectItem>
895
- {collaborators.map((collaborator) => (
896
- <SelectItem
897
- key={collaborator.id}
898
- value={String(collaborator.id)}
899
- >
900
- {collaborator.displayName}
901
- </SelectItem>
902
- ))}
903
- </SelectContent>
904
- </Select>,
905
- { className: isSheetMode ? 'xl:col-span-4' : '' }
906
- )}
907
-
908
- {renderField(
909
- commonT('labels.collaborator'),
910
- <Select
911
- value={form.relatedCollaboratorId}
912
- onValueChange={(value) =>
913
- setForm((c) => ({ ...c, relatedCollaboratorId: value }))
914
- }
915
- >
916
- <SelectTrigger>
917
- <SelectValue placeholder={commonT('labels.collaborator')} />
918
- </SelectTrigger>
919
- <SelectContent>
920
- <SelectItem value="none">
921
- {commonT('labels.notAssigned')}
922
- </SelectItem>
923
- {collaborators.map((collaborator) => (
924
- <SelectItem
925
- key={collaborator.id}
926
- value={String(collaborator.id)}
927
- >
928
- {collaborator.displayName}
929
- </SelectItem>
930
- ))}
931
- </SelectContent>
932
- </Select>,
933
- { className: isSheetMode ? 'xl:col-span-4' : '' }
934
- )}
410
+ const content = (
411
+ <div className="space-y-4">
412
+ <SectionCard
413
+ title={t('sections.overview')}
414
+ description={t('sections.simpleDescription')}
415
+ compact={isSheetMode}
416
+ descriptionMode={isSheetMode ? 'tooltip' : 'inline'}
417
+ >
418
+ <div className="grid gap-4 md:grid-cols-2">
419
+ <div className="space-y-2">
420
+ <Label>{t('fields.name')}</Label>
421
+ <Input
422
+ value={form.name}
423
+ placeholder={t('fields.name')}
424
+ onChange={(event) =>
425
+ setForm((current) => ({
426
+ ...current,
427
+ name: event.target.value,
428
+ }))
429
+ }
430
+ />
431
+ </div>
935
432
 
936
- <div className={isSheetMode ? 'xl:col-span-4' : 'md:col-span-3'}>
937
- <div className="rounded-lg border bg-muted/15 px-3 py-2.5">
938
- <div className="flex items-start justify-between gap-3">
939
- <div className="space-y-1">
940
- <Label className={fieldLabelClass}>
941
- {t('fields.isActive')}
942
- </Label>
943
- <p className={helpTextClass}>
944
- {t('fields.isActiveDescription')}
945
- </p>
433
+ <div className="space-y-2">
434
+ <Label>{commonT('labels.status')}</Label>
435
+ <div className="flex min-h-10 items-center justify-between rounded-lg border px-3">
436
+ <div className="flex items-center gap-2">
437
+ <StatusBadge
438
+ label={
439
+ form.isActive
440
+ ? detailT('labels.active')
441
+ : detailT('labels.inactive')
442
+ }
443
+ className={
444
+ form.isActive
445
+ ? 'bg-emerald-100 text-emerald-700'
446
+ : 'bg-muted text-muted-foreground'
447
+ }
448
+ />
946
449
  </div>
947
450
  <Switch
948
451
  checked={form.isActive}
949
452
  onCheckedChange={(checked) =>
950
- setForm((c) => ({ ...c, isActive: checked }))
453
+ setForm((current) => ({
454
+ ...current,
455
+ isActive: checked,
456
+ }))
951
457
  }
952
458
  />
953
459
  </div>
954
460
  </div>
955
- </div>
956
-
957
- {renderField(
958
- commonT('labels.startDate'),
959
- <Input
960
- type="date"
961
- value={form.startDate}
962
- onChange={(e) =>
963
- setForm((c) => ({ ...c, startDate: e.target.value }))
964
- }
965
- />,
966
- { className: isSheetMode ? 'xl:col-span-3' : '' }
967
- )}
968
-
969
- {renderField(
970
- commonT('labels.endDate'),
971
- <Input
972
- type="date"
973
- value={form.endDate}
974
- onChange={(e) =>
975
- setForm((c) => ({ ...c, endDate: e.target.value }))
976
- }
977
- />,
978
- { className: isSheetMode ? 'xl:col-span-3' : '' }
979
- )}
980
-
981
- {renderField(
982
- t('fields.effectiveDate'),
983
- <Input
984
- type="date"
985
- value={form.effectiveDate}
986
- onChange={(e) =>
987
- setForm((c) => ({ ...c, effectiveDate: e.target.value }))
988
- }
989
- />,
990
- { className: isSheetMode ? 'xl:col-span-3' : '' }
991
- )}
992
-
993
- {renderField(
994
- t('fields.signedAt'),
995
- <Input
996
- type="date"
997
- value={form.signedAt}
998
- onChange={(e) =>
999
- setForm((c) => ({ ...c, signedAt: e.target.value }))
1000
- }
1001
- />,
1002
- { className: isSheetMode ? 'xl:col-span-3' : '' }
1003
- )}
1004
-
1005
- {renderField(
1006
- t('fields.budgetAmount'),
1007
- <Input
1008
- type="number"
1009
- step="0.01"
1010
- placeholder={t('fields.budgetAmount')}
1011
- value={form.budgetAmount}
1012
- onChange={(e) =>
1013
- setForm((c) => ({ ...c, budgetAmount: e.target.value }))
1014
- }
1015
- />,
1016
- { className: isSheetMode ? 'xl:col-span-3' : '' }
1017
- )}
1018
-
1019
- {renderField(
1020
- t('fields.monthlyHourCap'),
1021
- <Input
1022
- type="number"
1023
- placeholder={t('fields.monthlyHourCap')}
1024
- value={form.monthlyHourCap}
1025
- onChange={(e) =>
1026
- setForm((c) => ({ ...c, monthlyHourCap: e.target.value }))
1027
- }
1028
- />,
1029
- { className: isSheetMode ? 'xl:col-span-3' : '' }
1030
- )}
1031
-
1032
- {renderField(
1033
- commonT('labels.description'),
1034
- <Textarea
1035
- rows={isSheetMode ? 3 : 4}
1036
- placeholder={commonT('labels.description')}
1037
- value={form.description}
1038
- onChange={(e) =>
1039
- setForm((c) => ({ ...c, description: e.target.value }))
1040
- }
1041
- />,
1042
- {
1043
- className: isSheetMode
1044
- ? 'md:col-span-2 xl:col-span-6'
1045
- : 'md:col-span-3',
1046
- }
1047
- )}
1048
- </div>
1049
- </SectionCard>
1050
- );
1051
-
1052
- const partiesSection = (
1053
- <SectionCard
1054
- title={t('sections.parties')}
1055
- description={t('sections.partiesDescription')}
1056
- compact={isSheetMode}
1057
- descriptionMode={sectionDescriptionMode}
1058
- >
1059
- <div className={sectionCardClass}>
1060
- {form.parties.map((party, index) => (
1061
- <div
1062
- key={index}
1063
- className={`grid rounded-lg border bg-muted/5 ${compactGridClass}`}
1064
- >
1065
- <div
1066
- className={`flex items-center justify-between gap-3 rounded-md bg-muted/30 px-3 py-2 ${
1067
- isSheetMode ? 'md:col-span-2 xl:col-span-6' : 'md:col-span-3'
1068
- }`}
1069
- >
1070
- <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
1071
- #{index + 1}
1072
- </span>
1073
- <div className="flex items-center gap-2">
1074
- <Label className={fieldLabelClass}>
1075
- {t('fields.isPrimaryParty')}
1076
- </Label>
1077
- <Switch
1078
- checked={party.isPrimary}
1079
- onCheckedChange={(checked) =>
1080
- updateArrayItem<PartyState>('parties', index, {
1081
- isPrimary: checked,
1082
- })
1083
- }
1084
- />
1085
- </div>
1086
- </div>
1087
-
1088
- {renderField(
1089
- t('fields.partyDisplayName'),
1090
- <Input
1091
- placeholder={t('fields.partyDisplayName')}
1092
- value={party.displayName}
1093
- onChange={(e) =>
1094
- updateArrayItem<PartyState>('parties', index, {
1095
- displayName: e.target.value,
1096
- })
1097
- }
1098
- />,
1099
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1100
- )}
1101
-
1102
- {renderField(
1103
- t('fields.partyRole'),
1104
- <Input
1105
- placeholder={t('fields.partyRole')}
1106
- value={party.partyRole}
1107
- onChange={(e) =>
1108
- updateArrayItem<PartyState>('parties', index, {
1109
- partyRole: e.target.value,
1110
- })
1111
- }
1112
- />,
1113
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1114
- )}
1115
-
1116
- {renderField(
1117
- t('fields.partyType'),
1118
- <Input
1119
- placeholder={t('fields.partyType')}
1120
- value={party.partyType}
1121
- onChange={(e) =>
1122
- updateArrayItem<PartyState>('parties', index, {
1123
- partyType: e.target.value,
1124
- })
1125
- }
1126
- />,
1127
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1128
- )}
1129
-
1130
- {renderField(
1131
- t('fields.documentNumber'),
1132
- <Input
1133
- placeholder={t('fields.documentNumber')}
1134
- value={party.documentNumber}
1135
- onChange={(e) =>
1136
- updateArrayItem<PartyState>('parties', index, {
1137
- documentNumber: e.target.value,
1138
- })
1139
- }
1140
- />,
1141
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1142
- )}
1143
-
1144
- {renderField(
1145
- t('fields.email'),
1146
- <Input
1147
- placeholder={t('fields.email')}
1148
- value={party.email}
1149
- onChange={(e) =>
1150
- updateArrayItem<PartyState>('parties', index, {
1151
- email: e.target.value,
1152
- })
1153
- }
1154
- />,
1155
- { className: isSheetMode ? 'xl:col-span-3' : '' }
1156
- )}
1157
-
1158
- {renderField(
1159
- t('fields.phone'),
1160
- <Input
1161
- placeholder={t('fields.phone')}
1162
- value={party.phone}
1163
- onChange={(e) =>
1164
- updateArrayItem<PartyState>('parties', index, {
1165
- phone: e.target.value,
1166
- })
1167
- }
1168
- />,
1169
- { className: isSheetMode ? 'xl:col-span-3' : '' }
1170
- )}
1171
- </div>
1172
- ))}
1173
- <Button
1174
- variant="outline"
1175
- onClick={() =>
1176
- setForm((c) => ({ ...c, parties: [...c.parties, emptyParty()] }))
1177
- }
1178
- >
1179
- <Plus className="size-4" />
1180
- {commonT('actions.addLine')}
1181
- </Button>
1182
- </div>
1183
- </SectionCard>
1184
- );
1185
-
1186
- const signaturesSection = (
1187
- <SectionCard
1188
- title={t('sections.signatures')}
1189
- description={t('sections.signaturesDescription')}
1190
- compact={isSheetMode}
1191
- descriptionMode={sectionDescriptionMode}
1192
- >
1193
- <div className={sectionCardClass}>
1194
- {form.signatures.map((signature, index) => (
1195
- <div
1196
- key={index}
1197
- className={`grid rounded-lg border bg-muted/5 ${compactTwoColGridClass}`}
1198
- >
1199
- {renderField(
1200
- t('fields.signerName'),
1201
- <Input
1202
- placeholder={t('fields.signerName')}
1203
- value={signature.signerName}
1204
- onChange={(e) =>
1205
- updateArrayItem<SignatureState>('signatures', index, {
1206
- signerName: e.target.value,
1207
- })
1208
- }
1209
- />,
1210
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1211
- )}
1212
-
1213
- {renderField(
1214
- t('fields.signerRole'),
1215
- <Input
1216
- placeholder={t('fields.signerRole')}
1217
- value={signature.signerRole}
1218
- onChange={(e) =>
1219
- updateArrayItem<SignatureState>('signatures', index, {
1220
- signerRole: e.target.value,
1221
- })
1222
- }
1223
- />,
1224
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1225
- )}
1226
-
1227
- {renderField(
1228
- t('fields.signerEmail'),
1229
- <Input
1230
- placeholder={t('fields.signerEmail')}
1231
- value={signature.signerEmail}
1232
- onChange={(e) =>
1233
- updateArrayItem<SignatureState>('signatures', index, {
1234
- signerEmail: e.target.value,
1235
- })
1236
- }
1237
- />,
1238
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1239
- )}
1240
-
1241
- {renderField(
1242
- t('fields.signatureItemStatus'),
1243
- <Input
1244
- placeholder={t('fields.signatureItemStatus')}
1245
- value={signature.status}
1246
- onChange={(e) =>
1247
- updateArrayItem<SignatureState>('signatures', index, {
1248
- status: e.target.value,
1249
- })
1250
- }
1251
- />,
1252
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1253
- )}
1254
-
1255
- {renderField(
1256
- t('fields.signedAt'),
1257
- <Input
1258
- type="date"
1259
- value={signature.signedAt}
1260
- onChange={(e) =>
1261
- updateArrayItem<SignatureState>('signatures', index, {
1262
- signedAt: e.target.value,
1263
- })
1264
- }
1265
- />,
1266
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1267
- )}
1268
- </div>
1269
- ))}
1270
- <Button
1271
- variant="outline"
1272
- onClick={() =>
1273
- setForm((c) => ({
1274
- ...c,
1275
- signatures: [...c.signatures, emptySignature()],
1276
- }))
1277
- }
1278
- >
1279
- <Plus className="size-4" />
1280
- {commonT('actions.addLine')}
1281
- </Button>
1282
- </div>
1283
- </SectionCard>
1284
- );
1285
-
1286
- const financialsSection = (
1287
- <SectionCard
1288
- title={t('sections.financials')}
1289
- description={t('sections.financialsDescription')}
1290
- compact={isSheetMode}
1291
- descriptionMode={sectionDescriptionMode}
1292
- >
1293
- <div className={sectionCardClass}>
1294
- {form.financialTerms.map((term, index) => (
1295
- <div
1296
- key={index}
1297
- className={`grid rounded-lg border bg-muted/5 ${compactGridClass}`}
1298
- >
1299
- {renderField(
1300
- t('fields.financialLabel'),
1301
- <Input
1302
- placeholder={t('fields.financialLabel')}
1303
- value={term.label}
1304
- onChange={(e) =>
1305
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1306
- label: e.target.value,
1307
- })
1308
- }
1309
- />,
1310
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1311
- )}
1312
-
1313
- {renderField(
1314
- t('fields.termType'),
1315
- <Input
1316
- placeholder={t('fields.termType')}
1317
- value={term.termType}
1318
- onChange={(e) =>
1319
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1320
- termType: e.target.value,
1321
- })
1322
- }
1323
- />,
1324
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1325
- )}
1326
-
1327
- {renderField(
1328
- t('fields.amount'),
1329
- <Input
1330
- type="number"
1331
- step="0.01"
1332
- placeholder={t('fields.amount')}
1333
- value={term.amount}
1334
- onChange={(e) =>
1335
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1336
- amount: e.target.value,
1337
- })
1338
- }
1339
- />,
1340
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1341
- )}
1342
-
1343
- {renderField(
1344
- t('fields.recurrence'),
1345
- <Input
1346
- placeholder={t('fields.recurrence')}
1347
- value={term.recurrence}
1348
- onChange={(e) =>
1349
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1350
- recurrence: e.target.value,
1351
- })
1352
- }
1353
- />,
1354
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1355
- )}
1356
-
1357
- {renderField(
1358
- t('fields.dueDay'),
1359
- <Input
1360
- type="number"
1361
- placeholder={t('fields.dueDay')}
1362
- value={term.dueDay}
1363
- onChange={(e) =>
1364
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1365
- dueDay: e.target.value,
1366
- })
1367
- }
1368
- />,
1369
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1370
- )}
1371
-
1372
- {renderField(
1373
- t('fields.notes'),
1374
- <Textarea
1375
- rows={isSheetMode ? 2 : 3}
1376
- placeholder={t('fields.notes')}
1377
- value={term.notes}
1378
- onChange={(e) =>
1379
- updateArrayItem<FinancialTermState>('financialTerms', index, {
1380
- notes: e.target.value,
1381
- })
1382
- }
1383
- />,
1384
- {
1385
- className: isSheetMode
1386
- ? 'md:col-span-2 xl:col-span-6'
1387
- : 'md:col-span-3',
1388
- }
1389
- )}
1390
- </div>
1391
- ))}
1392
- <Button
1393
- variant="outline"
1394
- onClick={() =>
1395
- setForm((c) => ({
1396
- ...c,
1397
- financialTerms: [...c.financialTerms, emptyFinancialTerm()],
1398
- }))
1399
- }
1400
- >
1401
- <Plus className="size-4" />
1402
- {commonT('actions.addLine')}
1403
- </Button>
1404
- </div>
1405
- </SectionCard>
1406
- );
1407
-
1408
- const documentsSection = (
1409
- <SectionCard
1410
- title={t('sections.documents')}
1411
- description={t('sections.documentsDescription')}
1412
- compact={isSheetMode}
1413
- descriptionMode={sectionDescriptionMode}
1414
- >
1415
- <div className="rounded-lg border border-dashed bg-muted/10 px-3 py-3">
1416
- <div className={fieldGroupClass}>
1417
- <Label className={fieldLabelClass}>{t('sections.documents')}</Label>
1418
- <input
1419
- className="block w-full text-sm file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:bg-primary file:px-3 file:py-2 file:text-sm file:font-medium file:text-primary-foreground"
1420
- type="file"
1421
- accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1422
- onChange={async (event) => {
1423
- const file = event.target.files?.[0];
1424
- if (!file) return;
1425
- const fileContentBase64 = await fileToBase64(file);
1426
- setForm((current) => ({
1427
- ...current,
1428
- pdfDocument: {
1429
- fileName: file.name,
1430
- mimeType: file.type || 'application/pdf',
1431
- fileContentBase64,
1432
- },
1433
- }));
1434
- }}
1435
- />
1436
- <p className={helpTextClass}>
1437
- {form.pdfDocument
1438
- ? t('messages.pdfReady', { name: form.pdfDocument.fileName })
1439
- : t('messages.pdfHint')}
1440
- </p>
1441
- </div>
1442
- </div>
1443
- </SectionCard>
1444
- );
1445
-
1446
- const revisionsSection = (
1447
- <SectionCard
1448
- title={t('sections.revisions')}
1449
- description={t('sections.revisionsDescription')}
1450
- compact={isSheetMode}
1451
- descriptionMode={sectionDescriptionMode}
1452
- >
1453
- <div className={sectionCardClass}>
1454
- {form.revisions.map((revision, index) => (
1455
- <div
1456
- key={index}
1457
- className={`grid rounded-lg border bg-muted/5 ${compactTwoColGridClass}`}
1458
- >
1459
- {renderField(
1460
- t('fields.revisionTitle'),
1461
- <Input
1462
- placeholder={t('fields.revisionTitle')}
1463
- value={revision.title}
1464
- onChange={(e) =>
1465
- updateArrayItem<RevisionState>('revisions', index, {
1466
- title: e.target.value,
1467
- })
1468
- }
1469
- />,
1470
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1471
- )}
1472
-
1473
- {renderField(
1474
- t('fields.revisionType'),
1475
- <Input
1476
- placeholder={t('fields.revisionType')}
1477
- value={revision.revisionType}
1478
- onChange={(e) =>
1479
- updateArrayItem<RevisionState>('revisions', index, {
1480
- revisionType: e.target.value,
1481
- })
1482
- }
1483
- />,
1484
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1485
- )}
1486
-
1487
- {renderField(
1488
- t('fields.revisionStatus'),
1489
- <Input
1490
- placeholder={t('fields.revisionStatus')}
1491
- value={revision.status}
1492
- onChange={(e) =>
1493
- updateArrayItem<RevisionState>('revisions', index, {
1494
- status: e.target.value,
1495
- })
1496
- }
1497
- />,
1498
- { className: isSheetMode ? 'xl:col-span-1' : '' }
1499
- )}
1500
-
1501
- {renderField(
1502
- t('fields.effectiveDate'),
1503
- <Input
1504
- type="date"
1505
- value={revision.effectiveDate}
1506
- onChange={(e) =>
1507
- updateArrayItem<RevisionState>('revisions', index, {
1508
- effectiveDate: e.target.value,
1509
- })
1510
- }
1511
- />,
1512
- { className: isSheetMode ? 'xl:col-span-2' : '' }
1513
- )}
1514
-
1515
- {renderField(
1516
- t('fields.summary'),
1517
- <Textarea
1518
- rows={isSheetMode ? 2 : 3}
1519
- placeholder={t('fields.summary')}
1520
- value={revision.summary}
1521
- onChange={(e) =>
1522
- updateArrayItem<RevisionState>('revisions', index, {
1523
- summary: e.target.value,
1524
- })
1525
- }
1526
- />,
1527
- {
1528
- className: isSheetMode
1529
- ? 'md:col-span-2 xl:col-span-6'
1530
- : 'md:col-span-2',
1531
- }
1532
- )}
1533
- </div>
1534
- ))}
1535
- <Button
1536
- variant="outline"
1537
- onClick={() =>
1538
- setForm((c) => ({
1539
- ...c,
1540
- revisions: [...c.revisions, emptyRevision()],
1541
- }))
1542
- }
1543
- >
1544
- <Plus className="size-4" />
1545
- {commonT('actions.addLine')}
1546
- </Button>
1547
- </div>
1548
- </SectionCard>
1549
- );
1550
461
 
1551
- const editorSection = (
1552
- <div className={isSheetMode ? 'overflow-hidden' : undefined}>
1553
- <ContractContentEditor
1554
- value={form.contentHtml}
1555
- onChange={(value) =>
1556
- setForm((current) => ({ ...current, contentHtml: value }))
1557
- }
1558
- compact={isSheetMode}
1559
- descriptionMode={sectionDescriptionMode}
1560
- editorTitle={t('sections.editor')}
1561
- editorDescription={t('sections.editorDescription')}
1562
- previewTitle={t('sections.preview')}
1563
- previewDescription={t('sections.previewDescription')}
1564
- promptContext={{
1565
- name: form.name,
1566
- code: form.code,
1567
- client_name: form.clientName,
1568
- contract_type: form.contractType,
1569
- billing_model: form.billingModel,
1570
- description: form.description,
1571
- }}
1572
- previewFallbackHtml="<p>No contract content yet.</p>"
1573
- />
1574
- </div>
1575
- );
1576
-
1577
- const formBody = isSheetMode ? (
1578
- <>
1579
- {overviewSection}
1580
- {partiesSection}
1581
- {signaturesSection}
1582
- {financialsSection}
1583
- {documentsSection}
1584
- {revisionsSection}
1585
- {editorSection}
1586
-
1587
- {sourceId && isLoadingContract ? (
1588
- <div className="text-sm text-muted-foreground">{t('loading')}</div>
1589
- ) : null}
1590
- </>
1591
- ) : (
1592
- <>
1593
- <Tabs defaultValue="overview" className="space-y-4">
1594
- <TabsList className="flex-wrap">
1595
- <TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
1596
- <TabsTrigger value="parties">{t('tabs.parties')}</TabsTrigger>
1597
- <TabsTrigger value="signatures">{t('tabs.signatures')}</TabsTrigger>
1598
- <TabsTrigger value="financials">{t('tabs.financials')}</TabsTrigger>
1599
- <TabsTrigger value="documents">{t('tabs.documents')}</TabsTrigger>
1600
- <TabsTrigger value="revisions">{t('tabs.revisions')}</TabsTrigger>
1601
- <TabsTrigger value="editor">{t('tabs.editor')}</TabsTrigger>
1602
- </TabsList>
1603
- <TabsContent value="overview">
1604
- <SectionCard
1605
- title={t('sections.overview')}
1606
- description={t('sections.overviewDescription')}
1607
- >
1608
- <div className="grid gap-4 md:grid-cols-3">
1609
- <Input
1610
- placeholder={t('fields.code')}
1611
- value={form.code}
1612
- onChange={(e) =>
1613
- setForm((c) => ({ ...c, code: e.target.value }))
1614
- }
1615
- />
1616
- <Input
1617
- className="md:col-span-2"
1618
- placeholder={t('fields.name')}
1619
- value={form.name}
1620
- onChange={(e) =>
1621
- setForm((c) => ({ ...c, name: e.target.value }))
1622
- }
1623
- />
1624
- <Input
1625
- placeholder={t('fields.clientName')}
1626
- value={form.clientName}
1627
- onChange={(e) =>
1628
- setForm((c) => ({ ...c, clientName: e.target.value }))
1629
- }
1630
- />
1631
- <div className="space-y-3 rounded-lg border px-4 py-3 md:col-span-2">
1632
- <div className="flex flex-wrap items-start justify-between gap-2">
1633
- <div>
1634
- <div className="text-sm font-medium">
1635
- {t('fields.contractTemplate')}
1636
- </div>
1637
- <div className="text-xs text-muted-foreground">
1638
- {t('fields.contractTemplateDescription')}
1639
- </div>
1640
- </div>
1641
- <Button type="button" variant="outline" size="sm" asChild>
1642
- <Link href="/operations/contracts/templates">
1643
- {commonT('actions.manageTemplates')}
1644
- </Link>
1645
- </Button>
462
+ <div className="space-y-2 md:col-span-2">
463
+ <Label>{t('sections.documents')}</Label>
464
+ <label className="flex cursor-pointer flex-col gap-3 rounded-lg border border-dashed px-4 py-4 transition-colors hover:border-primary/40 hover:bg-accent/20">
465
+ <div className="flex items-center gap-3">
466
+ <div className="rounded-full bg-primary/10 p-2 text-primary">
467
+ <Upload className="size-4" />
1646
468
  </div>
1647
- <Select
1648
- value={form.contractTemplateId}
1649
- onValueChange={handleTemplateSelection}
1650
- >
1651
- <SelectTrigger>
1652
- <SelectValue placeholder={t('fields.contractTemplate')} />
1653
- </SelectTrigger>
1654
- <SelectContent>
1655
- <SelectItem value="none">
1656
- {commonT('labels.notAssigned')}
1657
- </SelectItem>
1658
- {contractTemplates.map((template) => (
1659
- <SelectItem key={template.id} value={String(template.id)}>
1660
- {template.name}
1661
- </SelectItem>
1662
- ))}
1663
- </SelectContent>
1664
- </Select>
1665
- {selectedContractTemplate ? (
1666
- <div className="text-xs text-muted-foreground">
1667
- {[
1668
- selectedContractTemplate.code,
1669
- selectedContractTemplate.description,
1670
- ]
1671
- .filter(Boolean)
1672
- .join(' • ')}
1673
- </div>
1674
- ) : null}
1675
- </div>
1676
- <Select
1677
- value={form.contractCategory}
1678
- onValueChange={(value) =>
1679
- setForm((c) => ({ ...c, contractCategory: value }))
1680
- }
1681
- >
1682
- <SelectTrigger>
1683
- <SelectValue placeholder={t('fields.contractCategory')} />
1684
- </SelectTrigger>
1685
- <SelectContent>
1686
- {[
1687
- 'employee',
1688
- 'contractor',
1689
- 'client',
1690
- 'supplier',
1691
- 'vendor',
1692
- 'partner',
1693
- 'internal',
1694
- 'other',
1695
- ].map((value) => (
1696
- <SelectItem key={value} value={value}>
1697
- {value}
1698
- </SelectItem>
1699
- ))}
1700
- </SelectContent>
1701
- </Select>
1702
- <Select
1703
- value={form.contractType}
1704
- onValueChange={(value) =>
1705
- setForm((c) => ({ ...c, contractType: value }))
1706
- }
1707
- >
1708
- <SelectTrigger>
1709
- <SelectValue placeholder={t('fields.contractType')} />
1710
- </SelectTrigger>
1711
- <SelectContent>
1712
- {[
1713
- 'clt',
1714
- 'pj',
1715
- 'freelancer_agreement',
1716
- 'service_agreement',
1717
- 'fixed_term',
1718
- 'recurring_service',
1719
- 'nda',
1720
- 'amendment',
1721
- 'addendum',
1722
- 'other',
1723
- ].map((value) => (
1724
- <SelectItem key={value} value={value}>
1725
- {value}
1726
- </SelectItem>
1727
- ))}
1728
- </SelectContent>
1729
- </Select>
1730
- <Select
1731
- value={form.originType}
1732
- onValueChange={(value) =>
1733
- setForm((c) => ({ ...c, originType: value }))
1734
- }
1735
- >
1736
- <SelectTrigger>
1737
- <SelectValue placeholder={t('fields.originType')} />
1738
- </SelectTrigger>
1739
- <SelectContent>
1740
- {['manual', 'employee_hiring', 'client_project'].map(
1741
- (value) => (
1742
- <SelectItem key={value} value={value}>
1743
- {value}
1744
- </SelectItem>
1745
- )
1746
- )}
1747
- </SelectContent>
1748
- </Select>
1749
- <Input
1750
- placeholder={t('fields.originId')}
1751
- value={form.originId}
1752
- onChange={(e) =>
1753
- setForm((c) => ({ ...c, originId: e.target.value }))
1754
- }
1755
- />
1756
- <Select
1757
- value={form.billingModel}
1758
- onValueChange={(value) =>
1759
- setForm((c) => ({ ...c, billingModel: value }))
1760
- }
1761
- >
1762
- <SelectTrigger>
1763
- <SelectValue placeholder={commonT('labels.billingModel')} />
1764
- </SelectTrigger>
1765
- <SelectContent>
1766
- {['time_and_material', 'monthly_retainer', 'fixed_price'].map(
1767
- (value) => (
1768
- <SelectItem key={value} value={value}>
1769
- {value}
1770
- </SelectItem>
1771
- )
1772
- )}
1773
- </SelectContent>
1774
- </Select>
1775
- <Select
1776
- value={form.signatureStatus}
1777
- onValueChange={(value) =>
1778
- setForm((c) => ({ ...c, signatureStatus: value }))
1779
- }
1780
- >
1781
- <SelectTrigger>
1782
- <SelectValue placeholder={t('fields.signatureStatus')} />
1783
- </SelectTrigger>
1784
- <SelectContent>
1785
- {[
1786
- 'not_started',
1787
- 'pending',
1788
- 'partially_signed',
1789
- 'signed',
1790
- 'expired',
1791
- ].map((value) => (
1792
- <SelectItem key={value} value={value}>
1793
- {value}
1794
- </SelectItem>
1795
- ))}
1796
- </SelectContent>
1797
- </Select>
1798
- <Select
1799
- value={form.status}
1800
- onValueChange={(value) =>
1801
- setForm((c) => ({ ...c, status: value }))
1802
- }
1803
- >
1804
- <SelectTrigger>
1805
- <SelectValue placeholder={commonT('labels.status')} />
1806
- </SelectTrigger>
1807
- <SelectContent>
1808
- {[
1809
- 'draft',
1810
- 'under_review',
1811
- 'active',
1812
- 'renewal',
1813
- 'expired',
1814
- 'closed',
1815
- 'archived',
1816
- ].map((value) => (
1817
- <SelectItem key={value} value={value}>
1818
- {value}
1819
- </SelectItem>
1820
- ))}
1821
- </SelectContent>
1822
- </Select>
1823
- <Select
1824
- value={form.accountManagerCollaboratorId}
1825
- onValueChange={(value) =>
1826
- setForm((c) => ({
1827
- ...c,
1828
- accountManagerCollaboratorId: value,
1829
- }))
1830
- }
1831
- >
1832
- <SelectTrigger>
1833
- <SelectValue placeholder={commonT('labels.accountManager')} />
1834
- </SelectTrigger>
1835
- <SelectContent>
1836
- <SelectItem value="none">
1837
- {commonT('labels.notAssigned')}
1838
- </SelectItem>
1839
- {collaborators.map((collaborator) => (
1840
- <SelectItem
1841
- key={collaborator.id}
1842
- value={String(collaborator.id)}
1843
- >
1844
- {collaborator.displayName}
1845
- </SelectItem>
1846
- ))}
1847
- </SelectContent>
1848
- </Select>
1849
- <Select
1850
- value={form.relatedCollaboratorId}
1851
- onValueChange={(value) =>
1852
- setForm((c) => ({ ...c, relatedCollaboratorId: value }))
1853
- }
1854
- >
1855
- <SelectTrigger>
1856
- <SelectValue placeholder={commonT('labels.collaborator')} />
1857
- </SelectTrigger>
1858
- <SelectContent>
1859
- <SelectItem value="none">
1860
- {commonT('labels.notAssigned')}
1861
- </SelectItem>
1862
- {collaborators.map((collaborator) => (
1863
- <SelectItem
1864
- key={collaborator.id}
1865
- value={String(collaborator.id)}
1866
- >
1867
- {collaborator.displayName}
1868
- </SelectItem>
1869
- ))}
1870
- </SelectContent>
1871
- </Select>
1872
- <Input
1873
- type="date"
1874
- value={form.startDate}
1875
- onChange={(e) =>
1876
- setForm((c) => ({ ...c, startDate: e.target.value }))
1877
- }
1878
- />
1879
- <Input
1880
- type="date"
1881
- value={form.endDate}
1882
- onChange={(e) =>
1883
- setForm((c) => ({ ...c, endDate: e.target.value }))
1884
- }
1885
- />
1886
- <Input
1887
- type="date"
1888
- value={form.effectiveDate}
1889
- onChange={(e) =>
1890
- setForm((c) => ({ ...c, effectiveDate: e.target.value }))
1891
- }
1892
- />
1893
- <Input
1894
- type="date"
1895
- value={form.signedAt}
1896
- onChange={(e) =>
1897
- setForm((c) => ({ ...c, signedAt: e.target.value }))
1898
- }
1899
- />
1900
- <Input
1901
- type="number"
1902
- step="0.01"
1903
- placeholder={t('fields.budgetAmount')}
1904
- value={form.budgetAmount}
1905
- onChange={(e) =>
1906
- setForm((c) => ({ ...c, budgetAmount: e.target.value }))
1907
- }
1908
- />
1909
- <Input
1910
- type="number"
1911
- placeholder={t('fields.monthlyHourCap')}
1912
- value={form.monthlyHourCap}
1913
- onChange={(e) =>
1914
- setForm((c) => ({ ...c, monthlyHourCap: e.target.value }))
1915
- }
1916
- />
1917
- <div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-3">
1918
- <div>
1919
- <div className="font-medium">{t('fields.isActive')}</div>
1920
- <div className="text-sm text-muted-foreground">
1921
- {t('fields.isActiveDescription')}
469
+ <div className="min-w-0">
470
+ <div className="font-medium">
471
+ {form.pdfDocument?.fileName ||
472
+ currentDocument?.fileName ||
473
+ t('sections.documents')}
1922
474
  </div>
475
+ <p className="text-sm text-muted-foreground">
476
+ {t('messages.pdfHint')}
477
+ </p>
1923
478
  </div>
1924
- <Switch
1925
- checked={form.isActive}
1926
- onCheckedChange={(checked) =>
1927
- setForm((c) => ({ ...c, isActive: checked }))
1928
- }
1929
- />
1930
479
  </div>
1931
- <Textarea
1932
- className="md:col-span-3"
1933
- rows={4}
1934
- placeholder={commonT('labels.description')}
1935
- value={form.description}
1936
- onChange={(e) =>
1937
- setForm((c) => ({ ...c, description: e.target.value }))
1938
- }
1939
- />
1940
- </div>
1941
- </SectionCard>
1942
- </TabsContent>
1943
-
1944
- <TabsContent value="parties">
1945
- <SectionCard
1946
- title={t('sections.parties')}
1947
- description={t('sections.partiesDescription')}
1948
- >
1949
- <div className="space-y-4">
1950
- {form.parties.map((party, index) => (
1951
- <div
1952
- key={index}
1953
- className="grid gap-4 rounded-lg border p-4 md:grid-cols-3"
1954
- >
1955
- <Input
1956
- placeholder={t('fields.partyDisplayName')}
1957
- value={party.displayName}
1958
- onChange={(e) =>
1959
- updateArrayItem<PartyState>('parties', index, {
1960
- displayName: e.target.value,
1961
- })
1962
- }
1963
- />
1964
- <Input
1965
- placeholder={t('fields.partyRole')}
1966
- value={party.partyRole}
1967
- onChange={(e) =>
1968
- updateArrayItem<PartyState>('parties', index, {
1969
- partyRole: e.target.value,
1970
- })
1971
- }
1972
- />
1973
- <Input
1974
- placeholder={t('fields.partyType')}
1975
- value={party.partyType}
1976
- onChange={(e) =>
1977
- updateArrayItem<PartyState>('parties', index, {
1978
- partyType: e.target.value,
1979
- })
1980
- }
1981
- />
1982
- <Input
1983
- placeholder={t('fields.documentNumber')}
1984
- value={party.documentNumber}
1985
- onChange={(e) =>
1986
- updateArrayItem<PartyState>('parties', index, {
1987
- documentNumber: e.target.value,
1988
- })
1989
- }
1990
- />
1991
- <Input
1992
- placeholder={t('fields.email')}
1993
- value={party.email}
1994
- onChange={(e) =>
1995
- updateArrayItem<PartyState>('parties', index, {
1996
- email: e.target.value,
1997
- })
1998
- }
1999
- />
2000
- <Input
2001
- placeholder={t('fields.phone')}
2002
- value={party.phone}
2003
- onChange={(e) =>
2004
- updateArrayItem<PartyState>('parties', index, {
2005
- phone: e.target.value,
2006
- })
2007
- }
2008
- />
2009
- <div className="flex items-center justify-between rounded-md border px-3 py-2 md:col-span-3">
2010
- <span className="text-sm">
2011
- {t('fields.isPrimaryParty')}
2012
- </span>
2013
- <Switch
2014
- checked={party.isPrimary}
2015
- onCheckedChange={(checked) =>
2016
- updateArrayItem<PartyState>('parties', index, {
2017
- isPrimary: checked,
2018
- })
2019
- }
2020
- />
2021
- </div>
2022
- </div>
2023
- ))}
2024
- <Button
2025
- variant="outline"
2026
- onClick={() =>
2027
- setForm((c) => ({
2028
- ...c,
2029
- parties: [...c.parties, emptyParty()],
2030
- }))
2031
- }
2032
- >
2033
- <Plus className="size-4" />
2034
- {commonT('actions.addLine')}
2035
- </Button>
2036
- </div>
2037
- </SectionCard>
2038
- </TabsContent>
2039
- <TabsContent value="signatures">
2040
- <SectionCard
2041
- title={t('sections.signatures')}
2042
- description={t('sections.signaturesDescription')}
2043
- >
2044
- <div className="space-y-4">
2045
- {form.signatures.map((signature, index) => (
2046
- <div
2047
- key={index}
2048
- className="grid gap-4 rounded-lg border p-4 md:grid-cols-2"
2049
- >
2050
- <Input
2051
- placeholder={t('fields.signerName')}
2052
- value={signature.signerName}
2053
- onChange={(e) =>
2054
- updateArrayItem<SignatureState>('signatures', index, {
2055
- signerName: e.target.value,
2056
- })
2057
- }
2058
- />
2059
- <Input
2060
- placeholder={t('fields.signerRole')}
2061
- value={signature.signerRole}
2062
- onChange={(e) =>
2063
- updateArrayItem<SignatureState>('signatures', index, {
2064
- signerRole: e.target.value,
2065
- })
2066
- }
2067
- />
2068
- <Input
2069
- placeholder={t('fields.signerEmail')}
2070
- value={signature.signerEmail}
2071
- onChange={(e) =>
2072
- updateArrayItem<SignatureState>('signatures', index, {
2073
- signerEmail: e.target.value,
2074
- })
2075
- }
2076
- />
2077
- <Input
2078
- placeholder={t('fields.signatureItemStatus')}
2079
- value={signature.status}
2080
- onChange={(e) =>
2081
- updateArrayItem<SignatureState>('signatures', index, {
2082
- status: e.target.value,
2083
- })
2084
- }
2085
- />
2086
- <Input
2087
- type="date"
2088
- value={signature.signedAt}
2089
- onChange={(e) =>
2090
- updateArrayItem<SignatureState>('signatures', index, {
2091
- signedAt: e.target.value,
2092
- })
2093
- }
2094
- />
2095
- </div>
2096
- ))}
2097
- <Button
2098
- variant="outline"
2099
- onClick={() =>
2100
- setForm((c) => ({
2101
- ...c,
2102
- signatures: [...c.signatures, emptySignature()],
2103
- }))
2104
- }
2105
- >
2106
- <Plus className="size-4" />
2107
- {commonT('actions.addLine')}
2108
- </Button>
2109
- </div>
2110
- </SectionCard>
2111
- </TabsContent>
2112
- <TabsContent value="financials">
2113
- <SectionCard
2114
- title={t('sections.financials')}
2115
- description={t('sections.financialsDescription')}
2116
- >
2117
- <div className="space-y-4">
2118
- {form.financialTerms.map((term, index) => (
2119
- <div
2120
- key={index}
2121
- className="grid gap-4 rounded-lg border p-4 md:grid-cols-3"
2122
- >
2123
- <Input
2124
- placeholder={t('fields.financialLabel')}
2125
- value={term.label}
2126
- onChange={(e) =>
2127
- updateArrayItem<FinancialTermState>(
2128
- 'financialTerms',
2129
- index,
2130
- { label: e.target.value }
2131
- )
2132
- }
2133
- />
2134
- <Input
2135
- placeholder={t('fields.termType')}
2136
- value={term.termType}
2137
- onChange={(e) =>
2138
- updateArrayItem<FinancialTermState>(
2139
- 'financialTerms',
2140
- index,
2141
- { termType: e.target.value }
2142
- )
2143
- }
2144
- />
2145
- <Input
2146
- type="number"
2147
- step="0.01"
2148
- placeholder={t('fields.amount')}
2149
- value={term.amount}
2150
- onChange={(e) =>
2151
- updateArrayItem<FinancialTermState>(
2152
- 'financialTerms',
2153
- index,
2154
- { amount: e.target.value }
2155
- )
2156
- }
2157
- />
2158
- <Input
2159
- placeholder={t('fields.recurrence')}
2160
- value={term.recurrence}
2161
- onChange={(e) =>
2162
- updateArrayItem<FinancialTermState>(
2163
- 'financialTerms',
2164
- index,
2165
- { recurrence: e.target.value }
2166
- )
2167
- }
2168
- />
2169
- <Input
2170
- type="number"
2171
- placeholder={t('fields.dueDay')}
2172
- value={term.dueDay}
2173
- onChange={(e) =>
2174
- updateArrayItem<FinancialTermState>(
2175
- 'financialTerms',
2176
- index,
2177
- { dueDay: e.target.value }
2178
- )
2179
- }
2180
- />
2181
- <Input
2182
- placeholder={t('fields.notes')}
2183
- value={term.notes}
2184
- onChange={(e) =>
2185
- updateArrayItem<FinancialTermState>(
2186
- 'financialTerms',
2187
- index,
2188
- { notes: e.target.value }
2189
- )
2190
- }
2191
- />
2192
- </div>
2193
- ))}
2194
- <Button
2195
- variant="outline"
2196
- onClick={() =>
2197
- setForm((c) => ({
2198
- ...c,
2199
- financialTerms: [...c.financialTerms, emptyFinancialTerm()],
2200
- }))
2201
- }
2202
- >
2203
- <Plus className="size-4" />
2204
- {commonT('actions.addLine')}
2205
- </Button>
2206
- </div>
2207
- </SectionCard>
2208
- </TabsContent>
2209
- <TabsContent value="documents">
2210
- <SectionCard
2211
- title={t('sections.documents')}
2212
- description={t('sections.documentsDescription')}
2213
- >
2214
- <div className="space-y-4">
2215
480
  <input
2216
481
  type="file"
482
+ className="hidden"
2217
483
  accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2218
484
  onChange={async (event) => {
2219
485
  const file = event.target.files?.[0];
2220
- if (!file) return;
486
+ if (!file) {
487
+ return;
488
+ }
489
+
2221
490
  const fileContentBase64 = await fileToBase64(file);
2222
491
  setForm((current) => ({
2223
492
  ...current,
2224
493
  pdfDocument: {
2225
494
  fileName: file.name,
2226
- mimeType: file.type || 'application/pdf',
495
+ mimeType:
496
+ file.type ||
497
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
2227
498
  fileContentBase64,
2228
499
  },
2229
500
  }));
2230
501
  }}
2231
502
  />
2232
- <p className="text-sm text-muted-foreground">
2233
- {form.pdfDocument
2234
- ? t('messages.pdfReady', { name: form.pdfDocument.fileName })
2235
- : t('messages.pdfHint')}
2236
- </p>
2237
- </div>
2238
- </SectionCard>
2239
- </TabsContent>
2240
- <TabsContent value="revisions">
2241
- <SectionCard
2242
- title={t('sections.revisions')}
2243
- description={t('sections.revisionsDescription')}
2244
- >
2245
- <div className="space-y-4">
2246
- {form.revisions.map((revision, index) => (
2247
- <div
2248
- key={index}
2249
- className="grid gap-4 rounded-lg border p-4 md:grid-cols-2"
2250
- >
2251
- <Input
2252
- placeholder={t('fields.revisionTitle')}
2253
- value={revision.title}
2254
- onChange={(e) =>
2255
- updateArrayItem<RevisionState>('revisions', index, {
2256
- title: e.target.value,
2257
- })
2258
- }
2259
- />
2260
- <Input
2261
- placeholder={t('fields.revisionType')}
2262
- value={revision.revisionType}
2263
- onChange={(e) =>
2264
- updateArrayItem<RevisionState>('revisions', index, {
2265
- revisionType: e.target.value,
2266
- })
2267
- }
2268
- />
2269
- <Input
2270
- type="date"
2271
- value={revision.effectiveDate}
2272
- onChange={(e) =>
2273
- updateArrayItem<RevisionState>('revisions', index, {
2274
- effectiveDate: e.target.value,
2275
- })
2276
- }
2277
- />
2278
- <Input
2279
- placeholder={t('fields.revisionStatus')}
2280
- value={revision.status}
2281
- onChange={(e) =>
2282
- updateArrayItem<RevisionState>('revisions', index, {
2283
- status: e.target.value,
2284
- })
2285
- }
2286
- />
2287
- <Textarea
2288
- className="md:col-span-2"
2289
- rows={3}
2290
- placeholder={t('fields.summary')}
2291
- value={revision.summary}
2292
- onChange={(e) =>
2293
- updateArrayItem<RevisionState>('revisions', index, {
2294
- summary: e.target.value,
2295
- })
2296
- }
2297
- />
2298
- </div>
2299
- ))}
2300
- <Button
2301
- variant="outline"
2302
- onClick={() =>
2303
- setForm((c) => ({
2304
- ...c,
2305
- revisions: [...c.revisions, emptyRevision()],
2306
- }))
2307
- }
2308
- >
2309
- <Plus className="size-4" />
2310
- {commonT('actions.addLine')}
2311
- </Button>
2312
- </div>
2313
- </SectionCard>
2314
- </TabsContent>
2315
- <TabsContent value="editor">
2316
- <ContractContentEditor
2317
- value={form.contentHtml}
2318
- onChange={(value) =>
2319
- setForm((current) => ({ ...current, contentHtml: value }))
503
+ </label>
504
+ </div>
505
+ </div>
506
+ </SectionCard>
507
+
508
+ <SectionCard
509
+ title={detailT('sections.preview')}
510
+ description="Pré-visualização rápida do arquivo atual do contrato."
511
+ compact={isSheetMode}
512
+ descriptionMode={isSheetMode ? 'tooltip' : 'inline'}
513
+ >
514
+ {canPreview && previewSource ? (
515
+ <iframe
516
+ title={detailT('sections.preview')}
517
+ src={previewSource}
518
+ className={
519
+ isSheetMode
520
+ ? 'h-112 w-full rounded-lg border'
521
+ : 'h-130 w-full rounded-lg border'
2320
522
  }
2321
- editorTitle={t('sections.editor')}
2322
- editorDescription={t('sections.editorDescription')}
2323
- previewTitle={t('sections.preview')}
2324
- previewDescription={t('sections.previewDescription')}
2325
- promptContext={{
2326
- name: form.name,
2327
- code: form.code,
2328
- client_name: form.clientName,
2329
- contract_type: form.contractType,
2330
- billing_model: form.billingModel,
2331
- description: form.description,
2332
- }}
2333
- previewFallbackHtml="<p>No contract content yet.</p>"
2334
523
  />
2335
- </TabsContent>
2336
- </Tabs>
524
+ ) : (
525
+ <div className="rounded-lg border border-dashed px-4 py-10 text-center text-sm text-muted-foreground">
526
+ {detailT('states.previewUnavailable')}
527
+ </div>
528
+ )}
2337
529
 
2338
- {sourceId && isLoadingContract ? (
2339
- <div className="text-sm text-muted-foreground">{t('loading')}</div>
2340
- ) : null}
2341
- </>
530
+ <div className="mt-4 flex flex-wrap gap-2">
531
+ {currentDocument?.fileId ? (
532
+ <Button type="button" variant="outline" asChild>
533
+ <a
534
+ href={buildStoredFileUrl(currentDocument.fileId) || '#'}
535
+ target="_blank"
536
+ rel="noreferrer"
537
+ >
538
+ <Download className="size-4" />
539
+ {detailT('actions.downloadPdf')}
540
+ </a>
541
+ </Button>
542
+ ) : null}
543
+ {currentDocument?.fileContentBase64 ? (
544
+ <Button
545
+ type="button"
546
+ variant="outline"
547
+ onClick={() =>
548
+ downloadBase64File(
549
+ currentDocument.fileName,
550
+ currentDocument.mimeType,
551
+ currentDocument.fileContentBase64 || ''
552
+ )
553
+ }
554
+ >
555
+ <Download className="size-4" />
556
+ {detailT('actions.downloadPdf')}
557
+ </Button>
558
+ ) : null}
559
+ </div>
560
+ </SectionCard>
561
+
562
+ <div className="flex flex-wrap justify-between gap-2">
563
+ {isSheetMode ? (
564
+ <Button type="button" variant="outline" onClick={onCancel}>
565
+ {commonT('actions.cancel')}
566
+ </Button>
567
+ ) : (
568
+ <Button type="button" variant="outline" asChild>
569
+ <Link href="/operations/contracts">
570
+ {commonT('actions.cancel')}
571
+ </Link>
572
+ </Button>
573
+ )}
574
+
575
+ <Button type="button" onClick={() => void handleSubmit()}>
576
+ <Save className="size-4" />
577
+ {commonT(contractId ? 'actions.save' : 'actions.create')}
578
+ </Button>
579
+ </div>
580
+ </div>
2342
581
  );
2343
582
 
2344
583
  if (isSheetMode) {
2345
- return (
2346
- <div className="mt-3 space-y-3 pb-4">
2347
- {formBody}
584
+ if (sourceId && isLoadingContract) {
585
+ return (
586
+ <div className="px-4 py-6 text-sm text-muted-foreground">
587
+ {t('loading')}
588
+ </div>
589
+ );
590
+ }
2348
591
 
2349
- <FormActions
2350
- sheet
2351
- cancelLabel={commonT('actions.cancel')}
2352
- onCancel={onCancel}
2353
- onSubmit={() => void onSubmit()}
2354
- submitIcon={<Save className="size-4" />}
2355
- submitLabel={commonT('actions.save')}
2356
- submitSize="lg"
2357
- />
2358
- </div>
2359
- );
592
+ return <div className="px-4 py-4">{content}</div>;
2360
593
  }
2361
594
 
2362
595
  return (
2363
596
  <Page>
2364
597
  <OperationsHeader
2365
- title={
2366
- duplicateFromId
2367
- ? t('duplicateTitle')
2368
- : t(contractId ? 'editTitle' : 'newTitle')
2369
- }
598
+ title={t(contractId ? 'editTitle' : 'newTitle')}
2370
599
  description={t('description')}
2371
600
  current={t('breadcrumb')}
2372
- actions={
2373
- <div className="flex gap-2">
2374
- <Button variant="outline" size="sm" asChild>
2375
- <Link
2376
- href={
2377
- contractId
2378
- ? `/operations/contracts/${contractId}`
2379
- : '/operations/contracts'
2380
- }
2381
- >
2382
- <ArrowLeft className="size-4" />
2383
- {commonT('actions.back')}
2384
- </Link>
2385
- </Button>
2386
- <Button size="sm" onClick={() => void onSubmit()}>
2387
- <Save className="size-4" />
2388
- {commonT('actions.save')}
2389
- </Button>
2390
- </div>
2391
- }
2392
601
  />
2393
-
2394
- {formBody}
602
+ {sourceId && isLoadingContract ? (
603
+ <div className="text-sm text-muted-foreground">{t('loading')}</div>
604
+ ) : (
605
+ content
606
+ )}
2395
607
  </Page>
2396
608
  );
2397
609
  }