@hed-hog/operations 0.0.295 → 0.0.296

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 (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +8 -7
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. package/src/operations-growth.controller.ts +0 -44
@@ -0,0 +1,558 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, Page } from '@/components/entity-list';
4
+ import { RichTextEditor } from '@/components/rich-text-editor';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@/components/ui/select';
14
+ import { Switch } from '@/components/ui/switch';
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
16
+ import { Textarea } from '@/components/ui/textarea';
17
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
18
+ import { ArrowLeft, FileText, Plus, Save } from 'lucide-react';
19
+ import Link from 'next/link';
20
+ import { useRouter } from 'next/navigation';
21
+ import { useEffect, useState } from 'react';
22
+ import { useTranslations } from 'next-intl';
23
+ import { OperationsHeader } from './operations-header';
24
+ import { SectionCard } from './section-card';
25
+ import { fetchOperations, mutateOperations } from '../_lib/api';
26
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
27
+ import type {
28
+ OperationsCollaborator,
29
+ OperationsContractDetails,
30
+ } from '../_lib/types';
31
+ import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
32
+
33
+ type PartyState = {
34
+ displayName: string;
35
+ partyRole: string;
36
+ partyType: string;
37
+ documentNumber: string;
38
+ email: string;
39
+ phone: string;
40
+ isPrimary: boolean;
41
+ };
42
+
43
+ type SignatureState = {
44
+ signerName: string;
45
+ signerRole: string;
46
+ signerEmail: string;
47
+ status: string;
48
+ signedAt: string;
49
+ };
50
+
51
+ type FinancialTermState = {
52
+ label: string;
53
+ termType: string;
54
+ amount: string;
55
+ recurrence: string;
56
+ dueDay: string;
57
+ notes: string;
58
+ };
59
+
60
+ type RevisionState = {
61
+ title: string;
62
+ revisionType: string;
63
+ effectiveDate: string;
64
+ status: string;
65
+ summary: string;
66
+ };
67
+
68
+ type ContractFormState = {
69
+ code: string;
70
+ name: string;
71
+ clientName: string;
72
+ contractCategory: string;
73
+ contractType: string;
74
+ signatureStatus: string;
75
+ isActive: boolean;
76
+ billingModel: string;
77
+ accountManagerCollaboratorId: string;
78
+ relatedCollaboratorId: string;
79
+ originType: string;
80
+ originId: string;
81
+ startDate: string;
82
+ endDate: string;
83
+ signedAt: string;
84
+ effectiveDate: string;
85
+ budgetAmount: string;
86
+ monthlyHourCap: string;
87
+ status: string;
88
+ description: string;
89
+ contentHtml: string;
90
+ parties: PartyState[];
91
+ signatures: SignatureState[];
92
+ financialTerms: FinancialTermState[];
93
+ revisions: RevisionState[];
94
+ pdfDocument: {
95
+ fileName: string;
96
+ mimeType: string;
97
+ fileContentBase64: string;
98
+ } | null;
99
+ };
100
+
101
+ function emptyParty(): PartyState {
102
+ return {
103
+ displayName: '',
104
+ partyRole: 'client',
105
+ partyType: 'company',
106
+ documentNumber: '',
107
+ email: '',
108
+ phone: '',
109
+ isPrimary: false,
110
+ };
111
+ }
112
+
113
+ function emptySignature(): SignatureState {
114
+ return {
115
+ signerName: '',
116
+ signerRole: '',
117
+ signerEmail: '',
118
+ status: 'pending',
119
+ signedAt: '',
120
+ };
121
+ }
122
+
123
+ function emptyFinancialTerm(): FinancialTermState {
124
+ return {
125
+ label: '',
126
+ termType: 'value',
127
+ amount: '',
128
+ recurrence: 'one_time',
129
+ dueDay: '',
130
+ notes: '',
131
+ };
132
+ }
133
+
134
+ function emptyRevision(): RevisionState {
135
+ return {
136
+ title: '',
137
+ revisionType: 'revision',
138
+ effectiveDate: '',
139
+ status: 'draft',
140
+ summary: '',
141
+ };
142
+ }
143
+
144
+ function buildEmptyForm(): ContractFormState {
145
+ return {
146
+ code: '',
147
+ name: '',
148
+ clientName: '',
149
+ contractCategory: 'client',
150
+ contractType: 'service_agreement',
151
+ signatureStatus: 'not_started',
152
+ isActive: true,
153
+ billingModel: 'time_and_material',
154
+ accountManagerCollaboratorId: 'none',
155
+ relatedCollaboratorId: 'none',
156
+ originType: 'manual',
157
+ originId: '',
158
+ startDate: '',
159
+ endDate: '',
160
+ signedAt: '',
161
+ effectiveDate: '',
162
+ budgetAmount: '',
163
+ monthlyHourCap: '',
164
+ status: 'draft',
165
+ description: '',
166
+ contentHtml: '',
167
+ parties: [emptyParty()],
168
+ signatures: [emptySignature()],
169
+ financialTerms: [emptyFinancialTerm()],
170
+ revisions: [],
171
+ pdfDocument: null,
172
+ };
173
+ }
174
+
175
+ function toFormState(contract: OperationsContractDetails): ContractFormState {
176
+ return {
177
+ code: contract.code ?? '',
178
+ name: contract.name ?? '',
179
+ clientName: contract.clientName ?? '',
180
+ contractCategory: contract.contractCategory ?? 'client',
181
+ contractType: contract.contractType ?? 'service_agreement',
182
+ signatureStatus: contract.signatureStatus ?? 'not_started',
183
+ isActive: contract.isActive ?? true,
184
+ billingModel: contract.billingModel ?? 'time_and_material',
185
+ accountManagerCollaboratorId: contract.accountManagerCollaboratorId
186
+ ? String(contract.accountManagerCollaboratorId)
187
+ : 'none',
188
+ relatedCollaboratorId: contract.relatedCollaboratorId
189
+ ? String(contract.relatedCollaboratorId)
190
+ : 'none',
191
+ originType: contract.originType ?? 'manual',
192
+ originId: contract.originId ? String(contract.originId) : '',
193
+ startDate: contract.startDate ?? '',
194
+ endDate: contract.endDate ?? '',
195
+ signedAt: contract.signedAt ?? '',
196
+ effectiveDate: contract.effectiveDate ?? '',
197
+ budgetAmount:
198
+ contract.budgetAmount !== null && contract.budgetAmount !== undefined
199
+ ? String(contract.budgetAmount)
200
+ : '',
201
+ monthlyHourCap:
202
+ contract.monthlyHourCap !== null && contract.monthlyHourCap !== undefined
203
+ ? String(contract.monthlyHourCap)
204
+ : '',
205
+ status: contract.status ?? 'draft',
206
+ description: contract.description ?? '',
207
+ contentHtml: contract.contentHtml ?? '',
208
+ parties: contract.parties.length
209
+ ? contract.parties.map((party) => ({
210
+ displayName: party.displayName ?? '',
211
+ partyRole: party.partyRole ?? 'other',
212
+ partyType: party.partyType ?? 'company',
213
+ documentNumber: party.documentNumber ?? '',
214
+ email: party.email ?? '',
215
+ phone: party.phone ?? '',
216
+ isPrimary: party.isPrimary ?? false,
217
+ }))
218
+ : [emptyParty()],
219
+ signatures: contract.signatures.length
220
+ ? contract.signatures.map((signature) => ({
221
+ signerName: signature.signerName ?? '',
222
+ signerRole: signature.signerRole ?? '',
223
+ signerEmail: signature.signerEmail ?? '',
224
+ status: signature.status ?? 'pending',
225
+ signedAt: signature.signedAt ?? '',
226
+ }))
227
+ : [emptySignature()],
228
+ financialTerms: contract.financialTerms.length
229
+ ? contract.financialTerms.map((term) => ({
230
+ label: term.label ?? '',
231
+ termType: term.termType ?? 'value',
232
+ amount: String(term.amount ?? ''),
233
+ recurrence: term.recurrence ?? 'one_time',
234
+ dueDay:
235
+ term.dueDay !== null && term.dueDay !== undefined
236
+ ? String(term.dueDay)
237
+ : '',
238
+ notes: term.notes ?? '',
239
+ }))
240
+ : [emptyFinancialTerm()],
241
+ revisions: contract.revisions.map((revision) => ({
242
+ title: revision.title ?? '',
243
+ revisionType: revision.revisionType ?? 'revision',
244
+ effectiveDate: revision.effectiveDate ?? '',
245
+ status: revision.status ?? 'draft',
246
+ summary: revision.summary ?? '',
247
+ })),
248
+ pdfDocument: null,
249
+ };
250
+ }
251
+
252
+ async function fileToBase64(file: File) {
253
+ return new Promise<string>((resolve, reject) => {
254
+ const reader = new FileReader();
255
+ reader.onload = () => {
256
+ const result = String(reader.result ?? '');
257
+ const [, base64 = ''] = result.split(',');
258
+ resolve(base64);
259
+ };
260
+ reader.onerror = reject;
261
+ reader.readAsDataURL(file);
262
+ });
263
+ }
264
+
265
+ export function ContractFormScreen({
266
+ contractId,
267
+ duplicateFromId,
268
+ }: {
269
+ contractId?: number;
270
+ duplicateFromId?: number;
271
+ }) {
272
+ const t = useTranslations('operations.ContractFormPage');
273
+ const commonT = useTranslations('operations.Common');
274
+ const { request, showToastHandler, currentLocaleCode } = useApp();
275
+ const access = useOperationsAccess();
276
+ const router = useRouter();
277
+ const [form, setForm] = useState<ContractFormState>(buildEmptyForm());
278
+
279
+ const sourceId = contractId ?? duplicateFromId;
280
+
281
+ const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
282
+ queryKey: ['operations-contract-form-collaborators', currentLocaleCode],
283
+ enabled: access.isDirector,
284
+ queryFn: () =>
285
+ fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
286
+ });
287
+
288
+ const { data: contract, isLoading: isLoadingContract } =
289
+ useQuery<OperationsContractDetails>({
290
+ queryKey: ['operations-contract-form', currentLocaleCode, sourceId],
291
+ enabled: Boolean(sourceId),
292
+ queryFn: () =>
293
+ fetchOperations<OperationsContractDetails>(
294
+ request,
295
+ `/operations/contracts/${sourceId}`
296
+ ),
297
+ });
298
+
299
+ useEffect(() => {
300
+ if (!contract) {
301
+ return;
302
+ }
303
+
304
+ const next = toFormState(contract);
305
+ if (duplicateFromId) {
306
+ next.code = `${contract.code}-COPY`;
307
+ next.name = `${contract.name} Copy`;
308
+ next.originType = 'manual';
309
+ next.originId = '';
310
+ next.status = 'draft';
311
+ next.signatureStatus = 'not_started';
312
+ next.signedAt = '';
313
+ next.isActive = true;
314
+ }
315
+ setForm(next);
316
+ }, [contract, duplicateFromId]);
317
+
318
+ const updateArrayItem = <T,>(
319
+ key: 'parties' | 'signatures' | 'financialTerms' | 'revisions',
320
+ index: number,
321
+ patch: Partial<T>
322
+ ) => {
323
+ setForm((current) => ({
324
+ ...current,
325
+ [key]: (current[key] as T[]).map((item, itemIndex) =>
326
+ itemIndex === index ? { ...item, ...patch } : item
327
+ ),
328
+ }));
329
+ };
330
+
331
+ const onSubmit = async () => {
332
+ if (!form.code.trim() || !form.name.trim() || !form.clientName.trim()) {
333
+ showToastHandler?.('error', t('messages.requiredFields'));
334
+ return;
335
+ }
336
+
337
+ const payload = {
338
+ code: form.code.trim(),
339
+ name: form.name.trim(),
340
+ clientName: form.clientName.trim(),
341
+ contractCategory: form.contractCategory,
342
+ contractType: form.contractType,
343
+ signatureStatus: form.signatureStatus,
344
+ isActive: form.isActive,
345
+ billingModel: form.billingModel,
346
+ accountManagerCollaboratorId:
347
+ form.accountManagerCollaboratorId === 'none'
348
+ ? null
349
+ : parseNumberInput(form.accountManagerCollaboratorId),
350
+ relatedCollaboratorId:
351
+ form.relatedCollaboratorId === 'none'
352
+ ? null
353
+ : parseNumberInput(form.relatedCollaboratorId),
354
+ originType: form.originType,
355
+ originId: parseNumberInput(form.originId),
356
+ startDate: form.startDate,
357
+ endDate: trimToNull(form.endDate),
358
+ signedAt: trimToNull(form.signedAt),
359
+ effectiveDate: trimToNull(form.effectiveDate),
360
+ budgetAmount: parseNumberInput(form.budgetAmount),
361
+ monthlyHourCap: parseNumberInput(form.monthlyHourCap),
362
+ status: form.status,
363
+ description: trimToNull(form.description),
364
+ contentHtml: trimToNull(form.contentHtml),
365
+ parties: form.parties
366
+ .filter((party) => party.displayName.trim())
367
+ .map((party) => ({
368
+ displayName: party.displayName.trim(),
369
+ partyRole: party.partyRole,
370
+ partyType: party.partyType,
371
+ documentNumber: trimToNull(party.documentNumber),
372
+ email: trimToNull(party.email),
373
+ phone: trimToNull(party.phone),
374
+ isPrimary: party.isPrimary,
375
+ })),
376
+ signatures: form.signatures
377
+ .filter((signature) => signature.signerName.trim())
378
+ .map((signature) => ({
379
+ signerName: signature.signerName.trim(),
380
+ signerRole: trimToNull(signature.signerRole),
381
+ signerEmail: trimToNull(signature.signerEmail),
382
+ status: signature.status,
383
+ signedAt: trimToNull(signature.signedAt),
384
+ })),
385
+ financialTerms: form.financialTerms
386
+ .filter((term) => term.label.trim())
387
+ .map((term) => ({
388
+ label: term.label.trim(),
389
+ termType: term.termType,
390
+ amount: parseNumberInput(term.amount) ?? 0,
391
+ recurrence: term.recurrence,
392
+ dueDay: parseNumberInput(term.dueDay),
393
+ notes: trimToNull(term.notes),
394
+ })),
395
+ revisions: form.revisions
396
+ .filter((revision) => revision.title.trim())
397
+ .map((revision) => ({
398
+ title: revision.title.trim(),
399
+ revisionType: revision.revisionType,
400
+ effectiveDate: trimToNull(revision.effectiveDate),
401
+ status: revision.status,
402
+ summary: trimToNull(revision.summary),
403
+ })),
404
+ replaceUploadedPdfDocument: form.pdfDocument,
405
+ };
406
+
407
+ try {
408
+ const response = contractId
409
+ ? await mutateOperations<OperationsContractDetails>(
410
+ request,
411
+ `/operations/contracts/${contractId}`,
412
+ 'PATCH',
413
+ payload
414
+ )
415
+ : await mutateOperations<OperationsContractDetails>(
416
+ request,
417
+ '/operations/contracts',
418
+ 'POST',
419
+ payload
420
+ );
421
+
422
+ showToastHandler?.(
423
+ 'success',
424
+ contractId ? t('messages.updateSuccess') : t('messages.createSuccess')
425
+ );
426
+ router.push(`/operations/contracts/${response.id}`);
427
+ } catch {
428
+ showToastHandler?.(
429
+ 'error',
430
+ contractId ? t('messages.updateError') : t('messages.createError')
431
+ );
432
+ }
433
+ };
434
+
435
+ if (!access.isDirector && !access.isLoading) {
436
+ return (
437
+ <Page>
438
+ <OperationsHeader title={t(contractId ? 'editTitle' : 'newTitle')} description={t('description')} current={t('breadcrumb')} />
439
+ <EmptyState
440
+ icon={<FileText className="size-12" />}
441
+ title={commonT('states.noAccessTitle')}
442
+ description={t('noAccessDescription')}
443
+ actionLabel={commonT('actions.refresh')}
444
+ onAction={() => router.refresh()}
445
+ />
446
+ </Page>
447
+ );
448
+ }
449
+
450
+ return (
451
+ <Page>
452
+ <OperationsHeader
453
+ title={duplicateFromId ? t('duplicateTitle') : t(contractId ? 'editTitle' : 'newTitle')}
454
+ description={t('description')}
455
+ current={t('breadcrumb')}
456
+ actions={
457
+ <div className="flex gap-2">
458
+ <Button variant="outline" size="sm" asChild>
459
+ <Link href={contractId ? `/operations/contracts/${contractId}` : '/operations/contracts'}>
460
+ <ArrowLeft className="size-4" />
461
+ {commonT('actions.back')}
462
+ </Link>
463
+ </Button>
464
+ <Button size="sm" onClick={() => void onSubmit()}>
465
+ <Save className="size-4" />
466
+ {commonT('actions.save')}
467
+ </Button>
468
+ </div>
469
+ }
470
+ />
471
+
472
+ <Tabs defaultValue="overview">
473
+ <TabsList className="flex-wrap">
474
+ <TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
475
+ <TabsTrigger value="parties">{t('tabs.parties')}</TabsTrigger>
476
+ <TabsTrigger value="signatures">{t('tabs.signatures')}</TabsTrigger>
477
+ <TabsTrigger value="financials">{t('tabs.financials')}</TabsTrigger>
478
+ <TabsTrigger value="documents">{t('tabs.documents')}</TabsTrigger>
479
+ <TabsTrigger value="revisions">{t('tabs.revisions')}</TabsTrigger>
480
+ <TabsTrigger value="editor">{t('tabs.editor')}</TabsTrigger>
481
+ </TabsList>
482
+ <TabsContent value="overview">
483
+ <SectionCard title={t('sections.overview')} description={t('sections.overviewDescription')}>
484
+ <div className="grid gap-4 md:grid-cols-3">
485
+ <Input placeholder={t('fields.code')} value={form.code} onChange={(e) => setForm((c) => ({ ...c, code: e.target.value }))} />
486
+ <Input className="md:col-span-2" placeholder={t('fields.name')} value={form.name} onChange={(e) => setForm((c) => ({ ...c, name: e.target.value }))} />
487
+ <Input placeholder={t('fields.clientName')} value={form.clientName} onChange={(e) => setForm((c) => ({ ...c, clientName: e.target.value }))} />
488
+ <Select value={form.contractCategory} onValueChange={(value) => setForm((c) => ({ ...c, contractCategory: value }))}>
489
+ <SelectTrigger><SelectValue placeholder={t('fields.contractCategory')} /></SelectTrigger>
490
+ <SelectContent>{['employee','contractor','client','supplier','vendor','partner','internal','other'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
491
+ </Select>
492
+ <Select value={form.contractType} onValueChange={(value) => setForm((c) => ({ ...c, contractType: value }))}>
493
+ <SelectTrigger><SelectValue placeholder={t('fields.contractType')} /></SelectTrigger>
494
+ <SelectContent>{['clt','pj','freelancer_agreement','service_agreement','fixed_term','recurring_service','nda','amendment','addendum','other'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
495
+ </Select>
496
+ <Select value={form.originType} onValueChange={(value) => setForm((c) => ({ ...c, originType: value }))}>
497
+ <SelectTrigger><SelectValue placeholder={t('fields.originType')} /></SelectTrigger>
498
+ <SelectContent>{['manual','employee_hiring','client_project'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
499
+ </Select>
500
+ <Input placeholder={t('fields.originId')} value={form.originId} onChange={(e) => setForm((c) => ({ ...c, originId: e.target.value }))} />
501
+ <Select value={form.billingModel} onValueChange={(value) => setForm((c) => ({ ...c, billingModel: value }))}>
502
+ <SelectTrigger><SelectValue placeholder={commonT('labels.billingModel')} /></SelectTrigger>
503
+ <SelectContent>{['time_and_material','monthly_retainer','fixed_price'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
504
+ </Select>
505
+ <Select value={form.signatureStatus} onValueChange={(value) => setForm((c) => ({ ...c, signatureStatus: value }))}>
506
+ <SelectTrigger><SelectValue placeholder={t('fields.signatureStatus')} /></SelectTrigger>
507
+ <SelectContent>{['not_started','pending','partially_signed','signed','expired'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
508
+ </Select>
509
+ <Select value={form.status} onValueChange={(value) => setForm((c) => ({ ...c, status: value }))}>
510
+ <SelectTrigger><SelectValue placeholder={commonT('labels.status')} /></SelectTrigger>
511
+ <SelectContent>{['draft','under_review','active','renewal','expired','closed','archived'].map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>
512
+ </Select>
513
+ <Select value={form.accountManagerCollaboratorId} onValueChange={(value) => setForm((c) => ({ ...c, accountManagerCollaboratorId: value }))}>
514
+ <SelectTrigger><SelectValue placeholder={commonT('labels.accountManager')} /></SelectTrigger>
515
+ <SelectContent>
516
+ <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
517
+ {collaborators.map((collaborator) => <SelectItem key={collaborator.id} value={String(collaborator.id)}>{collaborator.displayName}</SelectItem>)}
518
+ </SelectContent>
519
+ </Select>
520
+ <Select value={form.relatedCollaboratorId} onValueChange={(value) => setForm((c) => ({ ...c, relatedCollaboratorId: value }))}>
521
+ <SelectTrigger><SelectValue placeholder={commonT('labels.collaborator')} /></SelectTrigger>
522
+ <SelectContent>
523
+ <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
524
+ {collaborators.map((collaborator) => <SelectItem key={collaborator.id} value={String(collaborator.id)}>{collaborator.displayName}</SelectItem>)}
525
+ </SelectContent>
526
+ </Select>
527
+ <Input type="date" value={form.startDate} onChange={(e) => setForm((c) => ({ ...c, startDate: e.target.value }))} />
528
+ <Input type="date" value={form.endDate} onChange={(e) => setForm((c) => ({ ...c, endDate: e.target.value }))} />
529
+ <Input type="date" value={form.effectiveDate} onChange={(e) => setForm((c) => ({ ...c, effectiveDate: e.target.value }))} />
530
+ <Input type="date" value={form.signedAt} onChange={(e) => setForm((c) => ({ ...c, signedAt: e.target.value }))} />
531
+ <Input type="number" step="0.01" placeholder={t('fields.budgetAmount')} value={form.budgetAmount} onChange={(e) => setForm((c) => ({ ...c, budgetAmount: e.target.value }))} />
532
+ <Input type="number" placeholder={t('fields.monthlyHourCap')} value={form.monthlyHourCap} onChange={(e) => setForm((c) => ({ ...c, monthlyHourCap: e.target.value }))} />
533
+ <div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-3">
534
+ <div>
535
+ <div className="font-medium">{t('fields.isActive')}</div>
536
+ <div className="text-sm text-muted-foreground">{t('fields.isActiveDescription')}</div>
537
+ </div>
538
+ <Switch checked={form.isActive} onCheckedChange={(checked) => setForm((c) => ({ ...c, isActive: checked }))} />
539
+ </div>
540
+ <Textarea className="md:col-span-3" rows={4} placeholder={commonT('labels.description')} value={form.description} onChange={(e) => setForm((c) => ({ ...c, description: e.target.value }))} />
541
+ </div>
542
+ </SectionCard>
543
+ </TabsContent>
544
+
545
+ <TabsContent value="parties"><SectionCard title={t('sections.parties')} description={t('sections.partiesDescription')}><div className="space-y-4">{form.parties.map((party, index) => <div key={index} className="grid gap-4 rounded-lg border p-4 md:grid-cols-3"><Input placeholder={t('fields.partyDisplayName')} value={party.displayName} onChange={(e) => updateArrayItem<PartyState>('parties', index, { displayName: e.target.value })} /><Input placeholder={t('fields.partyRole')} value={party.partyRole} onChange={(e) => updateArrayItem<PartyState>('parties', index, { partyRole: e.target.value })} /><Input placeholder={t('fields.partyType')} value={party.partyType} onChange={(e) => updateArrayItem<PartyState>('parties', index, { partyType: e.target.value })} /><Input placeholder={t('fields.documentNumber')} value={party.documentNumber} onChange={(e) => updateArrayItem<PartyState>('parties', index, { documentNumber: e.target.value })} /><Input placeholder={t('fields.email')} value={party.email} onChange={(e) => updateArrayItem<PartyState>('parties', index, { email: e.target.value })} /><Input placeholder={t('fields.phone')} value={party.phone} onChange={(e) => updateArrayItem<PartyState>('parties', index, { phone: e.target.value })} /><div className="flex items-center justify-between rounded-md border px-3 py-2 md:col-span-3"><span className="text-sm">{t('fields.isPrimaryParty')}</span><Switch checked={party.isPrimary} onCheckedChange={(checked) => updateArrayItem<PartyState>('parties', index, { isPrimary: checked })} /></div></div>)}<Button variant="outline" onClick={() => setForm((c) => ({ ...c, parties: [...c.parties, emptyParty()] }))}><Plus className="size-4" />{commonT('actions.addLine')}</Button></div></SectionCard></TabsContent>
546
+ <TabsContent value="signatures"><SectionCard title={t('sections.signatures')} description={t('sections.signaturesDescription')}><div className="space-y-4">{form.signatures.map((signature, index) => <div key={index} className="grid gap-4 rounded-lg border p-4 md:grid-cols-2"><Input placeholder={t('fields.signerName')} value={signature.signerName} onChange={(e) => updateArrayItem<SignatureState>('signatures', index, { signerName: e.target.value })} /><Input placeholder={t('fields.signerRole')} value={signature.signerRole} onChange={(e) => updateArrayItem<SignatureState>('signatures', index, { signerRole: e.target.value })} /><Input placeholder={t('fields.signerEmail')} value={signature.signerEmail} onChange={(e) => updateArrayItem<SignatureState>('signatures', index, { signerEmail: e.target.value })} /><Input placeholder={t('fields.signatureItemStatus')} value={signature.status} onChange={(e) => updateArrayItem<SignatureState>('signatures', index, { status: e.target.value })} /><Input type="date" value={signature.signedAt} onChange={(e) => updateArrayItem<SignatureState>('signatures', index, { signedAt: e.target.value })} /></div>)}<Button variant="outline" onClick={() => setForm((c) => ({ ...c, signatures: [...c.signatures, emptySignature()] }))}><Plus className="size-4" />{commonT('actions.addLine')}</Button></div></SectionCard></TabsContent>
547
+ <TabsContent value="financials"><SectionCard title={t('sections.financials')} description={t('sections.financialsDescription')}><div className="space-y-4">{form.financialTerms.map((term, index) => <div key={index} className="grid gap-4 rounded-lg border p-4 md:grid-cols-3"><Input placeholder={t('fields.financialLabel')} value={term.label} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { label: e.target.value })} /><Input placeholder={t('fields.termType')} value={term.termType} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { termType: e.target.value })} /><Input type="number" step="0.01" placeholder={t('fields.amount')} value={term.amount} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { amount: e.target.value })} /><Input placeholder={t('fields.recurrence')} value={term.recurrence} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { recurrence: e.target.value })} /><Input type="number" placeholder={t('fields.dueDay')} value={term.dueDay} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { dueDay: e.target.value })} /><Input placeholder={t('fields.notes')} value={term.notes} onChange={(e) => updateArrayItem<FinancialTermState>('financialTerms', index, { notes: e.target.value })} /></div>)}<Button variant="outline" onClick={() => setForm((c) => ({ ...c, financialTerms: [...c.financialTerms, emptyFinancialTerm()] }))}><Plus className="size-4" />{commonT('actions.addLine')}</Button></div></SectionCard></TabsContent>
548
+ <TabsContent value="documents"><SectionCard title={t('sections.documents')} description={t('sections.documentsDescription')}><div className="space-y-4"><input type="file" accept="application/pdf" onChange={async (event) => { const file = event.target.files?.[0]; if (!file) return; const fileContentBase64 = await fileToBase64(file); setForm((current) => ({ ...current, pdfDocument: { fileName: file.name, mimeType: file.type || 'application/pdf', fileContentBase64 } })); }} /><p className="text-sm text-muted-foreground">{form.pdfDocument ? t('messages.pdfReady', { name: form.pdfDocument.fileName }) : t('messages.pdfHint')}</p></div></SectionCard></TabsContent>
549
+ <TabsContent value="revisions"><SectionCard title={t('sections.revisions')} description={t('sections.revisionsDescription')}><div className="space-y-4">{form.revisions.map((revision, index) => <div key={index} className="grid gap-4 rounded-lg border p-4 md:grid-cols-2"><Input placeholder={t('fields.revisionTitle')} value={revision.title} onChange={(e) => updateArrayItem<RevisionState>('revisions', index, { title: e.target.value })} /><Input placeholder={t('fields.revisionType')} value={revision.revisionType} onChange={(e) => updateArrayItem<RevisionState>('revisions', index, { revisionType: e.target.value })} /><Input type="date" value={revision.effectiveDate} onChange={(e) => updateArrayItem<RevisionState>('revisions', index, { effectiveDate: e.target.value })} /><Input placeholder={t('fields.revisionStatus')} value={revision.status} onChange={(e) => updateArrayItem<RevisionState>('revisions', index, { status: e.target.value })} /><Textarea className="md:col-span-2" rows={3} placeholder={t('fields.summary')} value={revision.summary} onChange={(e) => updateArrayItem<RevisionState>('revisions', index, { summary: e.target.value })} /></div>)}<Button variant="outline" onClick={() => setForm((c) => ({ ...c, revisions: [...c.revisions, emptyRevision()] }))}><Plus className="size-4" />{commonT('actions.addLine')}</Button></div></SectionCard></TabsContent>
550
+ <TabsContent value="editor"><div className="grid gap-4 xl:grid-cols-2"><SectionCard title={t('sections.editor')} description={t('sections.editorDescription')}><RichTextEditor value={form.contentHtml} onChange={(value) => setForm((c) => ({ ...c, contentHtml: value }))} /></SectionCard><SectionCard title={t('sections.preview')} description={t('sections.previewDescription')}><div className="prose prose-sm max-w-none rounded-lg border p-4" dangerouslySetInnerHTML={{ __html: form.contentHtml || '<p>No contract content yet.</p>' }} /></SectionCard></div></TabsContent>
551
+ </Tabs>
552
+
553
+ {sourceId && isLoadingContract ? (
554
+ <div className="text-sm text-muted-foreground">{t('loading')}</div>
555
+ ) : null}
556
+ </Page>
557
+ );
558
+ }