@hed-hog/operations 0.0.299 → 0.0.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/operations.controller.d.ts +713 -31
  2. package/dist/operations.controller.d.ts.map +1 -1
  3. package/dist/operations.controller.js +157 -0
  4. package/dist/operations.controller.js.map +1 -1
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +5 -1
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.proposal.subscriber.d.ts +11 -0
  9. package/dist/operations.proposal.subscriber.d.ts.map +1 -0
  10. package/dist/operations.proposal.subscriber.js +80 -0
  11. package/dist/operations.proposal.subscriber.js.map +1 -0
  12. package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
  13. package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
  14. package/dist/operations.proposal.subscriber.spec.js +88 -0
  15. package/dist/operations.proposal.subscriber.spec.js.map +1 -0
  16. package/dist/operations.service.d.ts +490 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +3590 -1267
  19. package/dist/operations.service.js.map +1 -1
  20. package/dist/operations.service.spec.d.ts +2 -0
  21. package/dist/operations.service.spec.d.ts.map +1 -0
  22. package/dist/operations.service.spec.js +159 -0
  23. package/dist/operations.service.spec.js.map +1 -0
  24. package/hedhog/data/menu.yaml +232 -198
  25. package/hedhog/data/role.yaml +23 -23
  26. package/hedhog/data/role_route.yaml +39 -0
  27. package/hedhog/data/route.yaml +447 -317
  28. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  29. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  30. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  31. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  32. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  33. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  34. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  35. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  36. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  37. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  38. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  39. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  40. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  41. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  42. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  43. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  44. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  45. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  46. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  48. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  49. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  51. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  52. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  53. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  54. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  55. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  59. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  60. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  61. package/hedhog/frontend/messages/en.json +473 -12
  62. package/hedhog/frontend/messages/pt.json +528 -66
  63. package/hedhog/table/operations_approval.yaml +49 -49
  64. package/hedhog/table/operations_approval_history.yaml +29 -29
  65. package/hedhog/table/operations_collaborator.yaml +87 -67
  66. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
  67. package/hedhog/table/operations_contract.yaml +121 -100
  68. package/hedhog/table/operations_contract_document.yaml +40 -23
  69. package/hedhog/table/operations_contract_financial_term.yaml +40 -40
  70. package/hedhog/table/operations_contract_history.yaml +27 -27
  71. package/hedhog/table/operations_contract_party.yaml +46 -46
  72. package/hedhog/table/operations_contract_revision.yaml +38 -38
  73. package/hedhog/table/operations_contract_signature.yaml +38 -38
  74. package/hedhog/table/operations_contract_template.yaml +58 -0
  75. package/hedhog/table/operations_department.yaml +24 -0
  76. package/hedhog/table/operations_project.yaml +54 -54
  77. package/hedhog/table/operations_project_assignment.yaml +55 -55
  78. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
  79. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
  80. package/hedhog/table/operations_time_off_request.yaml +57 -57
  81. package/hedhog/table/operations_timesheet.yaml +41 -41
  82. package/hedhog/table/operations_timesheet_entry.yaml +40 -40
  83. package/package.json +5 -3
  84. package/src/operations.controller.ts +304 -182
  85. package/src/operations.module.ts +26 -22
  86. package/src/operations.proposal.subscriber.spec.ts +121 -0
  87. package/src/operations.proposal.subscriber.ts +86 -0
  88. package/src/operations.service.spec.ts +210 -0
  89. package/src/operations.service.ts +7317 -3595
  90. package/dist/operations-data.controller.d.ts +0 -139
  91. package/dist/operations-data.controller.d.ts.map +0 -1
  92. package/dist/operations-data.controller.js +0 -113
  93. package/dist/operations-data.controller.js.map +0 -1
  94. package/dist/operations-growth.controller.d.ts +0 -48
  95. package/dist/operations-growth.controller.d.ts.map +0 -1
  96. package/dist/operations-growth.controller.js +0 -90
  97. package/dist/operations-growth.controller.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
- import { RichTextEditor } from '@/components/rich-text-editor';
5
4
  import { Button } from '@/components/ui/button';
5
+ import { FormActions } from '@/components/ui/form-actions';
6
6
  import { Input } from '@/components/ui/input';
7
+ import { Label } from '@/components/ui/label';
7
8
  import {
8
9
  Select,
9
10
  SelectContent,
@@ -16,19 +17,21 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
16
17
  import { Textarea } from '@/components/ui/textarea';
17
18
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
18
19
  import { ArrowLeft, FileText, Plus, Save } from 'lucide-react';
20
+ import { useTranslations } from 'next-intl';
19
21
  import Link from 'next/link';
20
22
  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';
23
+ import { type ReactNode, useEffect, useState } from 'react';
25
24
  import { fetchOperations, mutateOperations } from '../_lib/api';
26
25
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
27
26
  import type {
28
27
  OperationsCollaborator,
29
28
  OperationsContractDetails,
29
+ OperationsContractTemplate,
30
30
  } from '../_lib/types';
31
31
  import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
32
+ import { ContractContentEditor } from './contract-content-editor';
33
+ import { OperationsHeader } from './operations-header';
34
+ import { SectionCard } from './section-card';
32
35
 
33
36
  type PartyState = {
34
37
  displayName: string;
@@ -65,10 +68,11 @@ type RevisionState = {
65
68
  summary: string;
66
69
  };
67
70
 
68
- type ContractFormState = {
71
+ export type ContractFormState = {
69
72
  code: string;
70
73
  name: string;
71
74
  clientName: string;
75
+ contractTemplateId: string;
72
76
  contractCategory: string;
73
77
  contractType: string;
74
78
  signatureStatus: string;
@@ -146,6 +150,7 @@ function buildEmptyForm(): ContractFormState {
146
150
  code: '',
147
151
  name: '',
148
152
  clientName: '',
153
+ contractTemplateId: 'none',
149
154
  contractCategory: 'client',
150
155
  contractType: 'service_agreement',
151
156
  signatureStatus: 'not_started',
@@ -177,6 +182,9 @@ function toFormState(contract: OperationsContractDetails): ContractFormState {
177
182
  code: contract.code ?? '',
178
183
  name: contract.name ?? '',
179
184
  clientName: contract.clientName ?? '',
185
+ contractTemplateId: contract.contractTemplateId
186
+ ? String(contract.contractTemplateId)
187
+ : 'none',
180
188
  contractCategory: contract.contractCategory ?? 'client',
181
189
  contractType: contract.contractType ?? 'service_agreement',
182
190
  signatureStatus: contract.signatureStatus ?? 'not_started',
@@ -265,16 +273,28 @@ async function fileToBase64(file: File) {
265
273
  export function ContractFormScreen({
266
274
  contractId,
267
275
  duplicateFromId,
276
+ initialTemplateId,
277
+ initialValues,
278
+ onSaved,
279
+ onCancel,
268
280
  }: {
269
281
  contractId?: number;
270
282
  duplicateFromId?: number;
283
+ initialTemplateId?: number;
284
+ initialValues?: Partial<ContractFormState>;
285
+ onSaved?: (contract: OperationsContractDetails) => void | Promise<void>;
286
+ onCancel?: () => void;
271
287
  }) {
272
288
  const t = useTranslations('operations.ContractFormPage');
273
289
  const commonT = useTranslations('operations.Common');
274
290
  const { request, showToastHandler, currentLocaleCode } = useApp();
275
291
  const access = useOperationsAccess();
276
292
  const router = useRouter();
277
- const [form, setForm] = useState<ContractFormState>(buildEmptyForm());
293
+ const [form, setForm] = useState<ContractFormState>(() => ({
294
+ ...buildEmptyForm(),
295
+ ...initialValues,
296
+ }));
297
+ const isSheetMode = Boolean(onCancel);
278
298
 
279
299
  const sourceId = contractId ?? duplicateFromId;
280
300
 
@@ -282,9 +302,29 @@ export function ContractFormScreen({
282
302
  queryKey: ['operations-contract-form-collaborators', currentLocaleCode],
283
303
  enabled: access.isDirector,
284
304
  queryFn: () =>
285
- fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
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
+ ),
286
321
  });
287
322
 
323
+ const selectedContractTemplate =
324
+ contractTemplates.find(
325
+ (template) => String(template.id) === form.contractTemplateId
326
+ ) ?? null;
327
+
288
328
  const { data: contract, isLoading: isLoadingContract } =
289
329
  useQuery<OperationsContractDetails>({
290
330
  queryKey: ['operations-contract-form', currentLocaleCode, sourceId],
@@ -301,17 +341,18 @@ export function ContractFormScreen({
301
341
  return;
302
342
  }
303
343
 
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';
344
+ const next = toFormState(contract);
345
+ if (duplicateFromId) {
346
+ next.code = `${contract.code}-COPY`;
347
+ next.name = `${contract.name || contract.code || 'Contrato'} Copy`;
348
+ next.originType = 'manual';
349
+ next.originId = '';
350
+ next.status = 'draft';
311
351
  next.signatureStatus = 'not_started';
312
352
  next.signedAt = '';
313
353
  next.isActive = true;
314
354
  }
355
+ // eslint-disable-next-line react-hooks/set-state-in-effect
315
356
  setForm(next);
316
357
  }, [contract, duplicateFromId]);
317
358
 
@@ -328,6 +369,53 @@ export function ContractFormScreen({
328
369
  }));
329
370
  };
330
371
 
372
+ const handleTemplateSelection = (value: string) => {
373
+ const template =
374
+ value === 'none'
375
+ ? null
376
+ : (contractTemplates.find((item) => String(item.id) === value) ?? null);
377
+
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
+ };
390
+
391
+ useEffect(() => {
392
+ if (
393
+ !initialTemplateId ||
394
+ sourceId ||
395
+ !contractTemplates.length ||
396
+ form.contractTemplateId !== 'none'
397
+ ) {
398
+ return;
399
+ }
400
+
401
+ const template = contractTemplates.find(
402
+ (item) => item.id === initialTemplateId
403
+ );
404
+
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]);
418
+
331
419
  const onSubmit = async () => {
332
420
  if (!form.code.trim() || !form.name.trim() || !form.clientName.trim()) {
333
421
  showToastHandler?.('error', t('messages.requiredFields'));
@@ -351,6 +439,10 @@ export function ContractFormScreen({
351
439
  form.relatedCollaboratorId === 'none'
352
440
  ? null
353
441
  : parseNumberInput(form.relatedCollaboratorId),
442
+ contractTemplateId:
443
+ form.contractTemplateId === 'none'
444
+ ? null
445
+ : parseNumberInput(form.contractTemplateId),
354
446
  originType: form.originType,
355
447
  originId: parseNumberInput(form.originId),
356
448
  startDate: form.startDate,
@@ -423,6 +515,12 @@ export function ContractFormScreen({
423
515
  'success',
424
516
  contractId ? t('messages.updateSuccess') : t('messages.createSuccess')
425
517
  );
518
+
519
+ if (onSaved) {
520
+ await onSaved(response);
521
+ return;
522
+ }
523
+
426
524
  router.push(`/operations/contracts/${response.id}`);
427
525
  } catch {
428
526
  showToastHandler?.(
@@ -432,44 +530,1067 @@ export function ContractFormScreen({
432
530
  }
433
531
  };
434
532
 
533
+ const noAccessState = (
534
+ <EmptyState
535
+ icon={<FileText className="size-12" />}
536
+ title={commonT('states.noAccessTitle')}
537
+ description={t('noAccessDescription')}
538
+ actionLabel={commonT('actions.refresh')}
539
+ onAction={() => router.refresh()}
540
+ />
541
+ );
542
+
435
543
  if (!access.isDirector && !access.isLoading) {
544
+ if (isSheetMode) {
545
+ return <div className="pt-4">{noAccessState}</div>;
546
+ }
547
+
436
548
  return (
437
549
  <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()}
550
+ <OperationsHeader
551
+ title={t(contractId ? 'editTitle' : 'newTitle')}
552
+ description={t('description')}
553
+ current={t('breadcrumb')}
445
554
  />
555
+ {noAccessState}
446
556
  </Page>
447
557
  );
448
558
  }
449
559
 
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>
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
+ )}
935
+
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>
946
+ </div>
947
+ <Switch
948
+ checked={form.isActive}
949
+ onCheckedChange={(checked) =>
950
+ setForm((c) => ({ ...c, isActive: checked }))
951
+ }
952
+ />
953
+ </div>
468
954
  </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
+
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 }))
469
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>"
470
1573
  />
1574
+ </div>
1575
+ );
1576
+
1577
+ const formBody = isSheetMode ? (
1578
+ <>
1579
+ {overviewSection}
1580
+ {partiesSection}
1581
+ {signaturesSection}
1582
+ {financialsSection}
1583
+ {documentsSection}
1584
+ {revisionsSection}
1585
+ {editorSection}
471
1586
 
472
- <Tabs defaultValue="overview">
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">
473
1594
  <TabsList className="flex-wrap">
474
1595
  <TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
475
1596
  <TabsTrigger value="parties">{t('tabs.parties')}</TabsTrigger>
@@ -480,79 +1601,797 @@ export function ContractFormScreen({
480
1601
  <TabsTrigger value="editor">{t('tabs.editor')}</TabsTrigger>
481
1602
  </TabsList>
482
1603
  <TabsContent value="overview">
483
- <SectionCard title={t('sections.overview')} description={t('sections.overviewDescription')}>
1604
+ <SectionCard
1605
+ title={t('sections.overview')}
1606
+ description={t('sections.overviewDescription')}
1607
+ >
484
1608
  <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>
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>
1646
+ </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>
491
1701
  </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>
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>
495
1729
  </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>
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>
499
1748
  </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>
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>
504
1774
  </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>
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>
508
1797
  </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>
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>
512
1822
  </Select>
513
- <Select value={form.accountManagerCollaboratorId} onValueChange={(value) => setForm((c) => ({ ...c, accountManagerCollaboratorId: value }))}>
514
- <SelectTrigger><SelectValue placeholder={commonT('labels.accountManager')} /></SelectTrigger>
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>
515
1835
  <SelectContent>
516
- <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
517
- {collaborators.map((collaborator) => <SelectItem key={collaborator.id} value={String(collaborator.id)}>{collaborator.displayName}</SelectItem>)}
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
+ ))}
518
1847
  </SelectContent>
519
1848
  </Select>
520
- <Select value={form.relatedCollaboratorId} onValueChange={(value) => setForm((c) => ({ ...c, relatedCollaboratorId: value }))}>
521
- <SelectTrigger><SelectValue placeholder={commonT('labels.collaborator')} /></SelectTrigger>
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>
522
1858
  <SelectContent>
523
- <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
524
- {collaborators.map((collaborator) => <SelectItem key={collaborator.id} value={String(collaborator.id)}>{collaborator.displayName}</SelectItem>)}
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
+ ))}
525
1870
  </SelectContent>
526
1871
  </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 }))} />
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
+ />
533
1917
  <div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-3">
534
1918
  <div>
535
1919
  <div className="font-medium">{t('fields.isActive')}</div>
536
- <div className="text-sm text-muted-foreground">{t('fields.isActiveDescription')}</div>
1920
+ <div className="text-sm text-muted-foreground">
1921
+ {t('fields.isActiveDescription')}
1922
+ </div>
537
1923
  </div>
538
- <Switch checked={form.isActive} onCheckedChange={(checked) => setForm((c) => ({ ...c, isActive: checked }))} />
1924
+ <Switch
1925
+ checked={form.isActive}
1926
+ onCheckedChange={(checked) =>
1927
+ setForm((c) => ({ ...c, isActive: checked }))
1928
+ }
1929
+ />
539
1930
  </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 }))} />
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
+ />
541
1940
  </div>
542
1941
  </SectionCard>
543
1942
  </TabsContent>
544
1943
 
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>
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
+ <input
2216
+ type="file"
2217
+ accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2218
+ onChange={async (event) => {
2219
+ const file = event.target.files?.[0];
2220
+ if (!file) return;
2221
+ const fileContentBase64 = await fileToBase64(file);
2222
+ setForm((current) => ({
2223
+ ...current,
2224
+ pdfDocument: {
2225
+ fileName: file.name,
2226
+ mimeType: file.type || 'application/pdf',
2227
+ fileContentBase64,
2228
+ },
2229
+ }));
2230
+ }}
2231
+ />
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 }))
2320
+ }
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
+ />
2335
+ </TabsContent>
551
2336
  </Tabs>
552
2337
 
553
2338
  {sourceId && isLoadingContract ? (
554
2339
  <div className="text-sm text-muted-foreground">{t('loading')}</div>
555
2340
  ) : null}
2341
+ </>
2342
+ );
2343
+
2344
+ if (isSheetMode) {
2345
+ return (
2346
+ <div className="mt-3 space-y-3 pb-4">
2347
+ {formBody}
2348
+
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
+ );
2360
+ }
2361
+
2362
+ return (
2363
+ <Page>
2364
+ <OperationsHeader
2365
+ title={
2366
+ duplicateFromId
2367
+ ? t('duplicateTitle')
2368
+ : t(contractId ? 'editTitle' : 'newTitle')
2369
+ }
2370
+ description={t('description')}
2371
+ 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
+ />
2393
+
2394
+ {formBody}
556
2395
  </Page>
557
2396
  );
558
2397
  }