@hed-hog/operations 0.0.318 → 0.0.321

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 (138) hide show
  1. package/dist/controllers/operations-collaborator-costs.controller.d.ts +144 -0
  2. package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
  3. package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
  4. package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
  5. package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +11 -0
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +9 -9
  10. package/dist/controllers/operations-projects.controller.d.ts +31 -0
  11. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  12. package/dist/controllers/operations-projects.controller.js +23 -0
  13. package/dist/controllers/operations-projects.controller.js.map +1 -1
  14. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  15. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  16. package/dist/controllers/operations-reports.controller.js +53 -0
  17. package/dist/controllers/operations-reports.controller.js.map +1 -0
  18. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  19. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  20. package/dist/controllers/operations-tasks.controller.js +17 -5
  21. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  22. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  23. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  24. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  25. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  26. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  27. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  28. package/dist/dto/create-collaborator.dto.js +0 -6
  29. package/dist/dto/create-collaborator.dto.js.map +1 -1
  30. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  31. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  32. package/dist/dto/create-cost-type.dto.js +87 -0
  33. package/dist/dto/create-cost-type.dto.js.map +1 -0
  34. package/dist/dto/list-approvals.dto.d.ts +2 -0
  35. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  36. package/dist/dto/list-approvals.dto.js +10 -0
  37. package/dist/dto/list-approvals.dto.js.map +1 -1
  38. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  39. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  40. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  41. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  42. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  43. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  44. package/dist/dto/list-cost-types.dto.js +35 -0
  45. package/dist/dto/list-cost-types.dto.js.map +1 -0
  46. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  47. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  48. package/dist/dto/list-my-projects.dto.js +23 -0
  49. package/dist/dto/list-my-projects.dto.js.map +1 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  51. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  52. package/dist/dto/list-my-tasks.dto.js +33 -0
  53. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  54. package/dist/dto/list-projects.dto.d.ts +1 -0
  55. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  56. package/dist/dto/list-projects.dto.js +7 -0
  57. package/dist/dto/list-projects.dto.js.map +1 -1
  58. package/dist/dto/list-reports.dto.d.ts +16 -0
  59. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  60. package/dist/dto/list-reports.dto.js +75 -0
  61. package/dist/dto/list-reports.dto.js.map +1 -0
  62. package/dist/dto/list-tasks.dto.d.ts +2 -0
  63. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  64. package/dist/dto/list-tasks.dto.js +12 -0
  65. package/dist/dto/list-tasks.dto.js.map +1 -1
  66. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  67. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  68. package/dist/dto/list-timesheets.dto.js +10 -0
  69. package/dist/dto/list-timesheets.dto.js.map +1 -1
  70. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  71. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  72. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  73. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  74. package/dist/dto/update-task.dto.d.ts +1 -0
  75. package/dist/dto/update-task.dto.d.ts.map +1 -1
  76. package/dist/dto/update-task.dto.js +6 -0
  77. package/dist/dto/update-task.dto.js.map +1 -1
  78. package/dist/operations.module.d.ts.map +1 -1
  79. package/dist/operations.module.js +4 -0
  80. package/dist/operations.module.js.map +1 -1
  81. package/dist/operations.service.d.ts +457 -3
  82. package/dist/operations.service.d.ts.map +1 -1
  83. package/dist/operations.service.js +1445 -208
  84. package/dist/operations.service.js.map +1 -1
  85. package/dist/operations.service.spec.js +31 -7
  86. package/dist/operations.service.spec.js.map +1 -1
  87. package/hedhog/data/menu.yaml +112 -7
  88. package/hedhog/data/operations_cost_type.yaml +166 -0
  89. package/hedhog/data/route.yaml +185 -0
  90. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  91. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +80 -1
  92. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  93. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  94. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  95. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  96. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  97. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  98. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  99. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  100. package/hedhog/frontend/app/_lib/types.ts.ejs +227 -1
  101. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  102. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  103. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  104. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  105. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  106. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  107. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  108. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  109. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  110. package/hedhog/frontend/messages/en.json +234 -25
  111. package/hedhog/frontend/messages/pt.json +234 -25
  112. package/hedhog/table/operations_collaborator.yaml +0 -4
  113. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  114. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  115. package/hedhog/table/operations_cost_type.yaml +38 -0
  116. package/package.json +7 -7
  117. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  118. package/src/controllers/operations-collaborators.controller.ts +19 -8
  119. package/src/controllers/operations-projects.controller.ts +19 -8
  120. package/src/controllers/operations-reports.controller.ts +32 -0
  121. package/src/controllers/operations-tasks.controller.ts +32 -12
  122. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  123. package/src/dto/create-collaborator.dto.ts +9 -14
  124. package/src/dto/create-cost-type.dto.ts +62 -0
  125. package/src/dto/list-approvals.dto.ts +8 -0
  126. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  127. package/src/dto/list-cost-types.dto.ts +19 -0
  128. package/src/dto/list-my-projects.dto.ts +8 -0
  129. package/src/dto/list-my-tasks.dto.ts +17 -0
  130. package/src/dto/list-projects.dto.ts +7 -1
  131. package/src/dto/list-reports.dto.ts +51 -0
  132. package/src/dto/list-tasks.dto.ts +11 -1
  133. package/src/dto/list-timesheets.dto.ts +8 -0
  134. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  135. package/src/dto/update-task.dto.ts +6 -0
  136. package/src/operations.module.ts +4 -0
  137. package/src/operations.service.spec.ts +45 -7
  138. package/src/operations.service.ts +1988 -221
@@ -21,6 +21,7 @@ import { StatusBadge } from './status-badge';
21
21
  import { fetchOperations } from '../_lib/api';
22
22
  import type { OperationsCollaboratorDetails } from '../_lib/types';
23
23
  import {
24
+ formatCurrency,
24
25
  formatDate,
25
26
  formatDateRange,
26
27
  formatEnumLabel,
@@ -37,7 +38,7 @@ export function CollaboratorDetailsScreen({
37
38
  }) {
38
39
  const t = useTranslations('operations.CollaboratorDetailsPage');
39
40
  const commonT = useTranslations('operations.Common');
40
- const { request, currentLocaleCode } = useApp();
41
+ const { request, currentLocaleCode, getSettingValue } = useApp();
41
42
  const access = useOperationsAccess();
42
43
 
43
44
  const { data: collaborator, refetch } =
@@ -54,6 +55,32 @@ export function CollaboratorDetailsScreen({
54
55
  ),
55
56
  });
56
57
 
58
+ type CompensationHistoryEntry = {
59
+ id: number;
60
+ collaboratorId: number;
61
+ amount: string;
62
+ effectiveDate: string | null;
63
+ actorUserId: number | null;
64
+ actorName: string | null;
65
+ notes: string | null;
66
+ createdAt: string;
67
+ };
68
+
69
+ const { data: compensationHistory = [] } =
70
+ useQuery<CompensationHistoryEntry[]>({
71
+ queryKey: [
72
+ 'operations-collaborator-compensation-history',
73
+ currentLocaleCode,
74
+ collaboratorId,
75
+ ],
76
+ enabled: access.isDirector,
77
+ queryFn: () =>
78
+ fetchOperations<CompensationHistoryEntry[]>(
79
+ request,
80
+ `/operations/collaborators/${collaboratorId}/compensation-history`
81
+ ),
82
+ });
83
+
57
84
  if (!collaborator) {
58
85
  return (
59
86
  <Page>
@@ -392,6 +419,58 @@ export function CollaboratorDetailsScreen({
392
419
  )}
393
420
  </SectionCard>
394
421
  </div>
422
+
423
+ {access.isDirector && (
424
+ <SectionCard
425
+ title={t('sections.compensationHistory')}
426
+ description={t('sections.compensationHistoryDescription')}
427
+ >
428
+ {compensationHistory.length > 0 ? (
429
+ <div className="overflow-x-auto rounded-md border">
430
+ <Table>
431
+ <TableHeader>
432
+ <TableRow>
433
+ <TableHead>{t('compensationHistory.amount')}</TableHead>
434
+ <TableHead>{t('compensationHistory.effectiveDate')}</TableHead>
435
+ <TableHead>{t('compensationHistory.recordedAt')}</TableHead>
436
+ <TableHead>{t('compensationHistory.changedBy')}</TableHead>
437
+ <TableHead>{t('compensationHistory.notes')}</TableHead>
438
+ </TableRow>
439
+ </TableHeader>
440
+ <TableBody>
441
+ {compensationHistory.map((entry) => (
442
+ <TableRow key={entry.id}>
443
+ <TableCell className="font-medium">
444
+ {formatCurrency(
445
+ Number(entry.amount),
446
+ getSettingValue,
447
+ currentLocaleCode
448
+ )}
449
+ </TableCell>
450
+ <TableCell>
451
+ {entry.effectiveDate
452
+ ? formatDate(entry.effectiveDate)
453
+ : '—'}
454
+ </TableCell>
455
+ <TableCell>{formatDate(entry.createdAt)}</TableCell>
456
+ <TableCell>
457
+ {entry.actorName ?? '—'}
458
+ </TableCell>
459
+ <TableCell className="text-muted-foreground">
460
+ {entry.notes ?? '—'}
461
+ </TableCell>
462
+ </TableRow>
463
+ ))}
464
+ </TableBody>
465
+ </Table>
466
+ </div>
467
+ ) : (
468
+ <p className="text-sm text-muted-foreground">
469
+ {t('noCompensationHistory')}
470
+ </p>
471
+ )}
472
+ </SectionCard>
473
+ )}
395
474
  </Page>
396
475
  );
397
476
  }
@@ -1,6 +1,18 @@
1
1
  'use client';
2
2
 
3
+ import { PersonFormSheet } from '@/app/(app)/(libraries)/contact/person/_components/person-form-sheet';
4
+ import type {
5
+ ContactTypeOption,
6
+ DocumentTypeOption,
7
+ Person,
8
+ } from '@/app/(app)/(libraries)/contact/person/_components/person-types';
3
9
  import { EmptyState, Page } from '@/components/entity-list';
10
+ import {
11
+ Accordion,
12
+ AccordionContent,
13
+ AccordionItem,
14
+ AccordionTrigger,
15
+ } from '@/components/ui/accordion';
4
16
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
5
17
  import { Button } from '@/components/ui/button';
6
18
  import {
@@ -11,6 +23,7 @@ import {
11
23
  CommandItem,
12
24
  CommandList,
13
25
  } from '@/components/ui/command';
26
+ import { EntityPicker } from '@/components/ui/entity-picker';
14
27
  import { FormActions } from '@/components/ui/form-actions';
15
28
  import { Input } from '@/components/ui/input';
16
29
  import { InputMoney } from '@/components/ui/input-money';
@@ -40,6 +53,7 @@ import {
40
53
  Check,
41
54
  ChevronsUpDown,
42
55
  Info,
56
+ Pencil,
43
57
  Save,
44
58
  UserRound,
45
59
  } from 'lucide-react';
@@ -57,9 +71,11 @@ import type {
57
71
  OperationsCollaborator,
58
72
  OperationsCollaboratorDetails,
59
73
  OperationsCollaboratorType,
74
+ OperationsContract,
60
75
  OperationsDepartment,
61
76
  OperationsJobTitle,
62
77
  OperationsWeeklyScheduleDay,
78
+ PaginatedResponse,
63
79
  } from '../_lib/types';
64
80
  import {
65
81
  formatDate,
@@ -74,6 +90,7 @@ import {
74
90
  parseNumberInput,
75
91
  trimToNull,
76
92
  } from '../_lib/utils/forms';
93
+ import { CollaboratorCostsSection } from './collaborator-costs-section';
77
94
  import { DepartmentSelectWithCreate } from './department-select-with-create';
78
95
  import { OperationsHeader } from './operations-header';
79
96
  import { PersonSelectWithCreate } from './person-select-with-create';
@@ -212,11 +229,9 @@ const EQUITY_ENABLED_TYPE_SLUGS = new Set([
212
229
  type CollaboratorFormState = {
213
230
  userId: string;
214
231
  personId: string;
215
- code: string;
216
232
  displayName: string;
217
233
  collaboratorTypeId: string;
218
234
  departmentId: string;
219
- department: string;
220
235
  jobTitleId: string;
221
236
  title: string;
222
237
  levelLabel: string;
@@ -226,8 +241,7 @@ type CollaboratorFormState = {
226
241
  leftAt: string;
227
242
  supervisorCollaboratorId: string;
228
243
  compensationAmount: string;
229
- contractDescription: string;
230
- autoGenerateContractDraft: boolean;
244
+ contractId: string;
231
245
  notes: string;
232
246
  equityParticipation: {
233
247
  participationType: string;
@@ -260,11 +274,9 @@ function buildEmptyForm(): CollaboratorFormState {
260
274
  return {
261
275
  userId: '',
262
276
  personId: '',
263
- code: '',
264
277
  displayName: '',
265
278
  collaboratorTypeId: '',
266
279
  departmentId: '',
267
- department: '',
268
280
  jobTitleId: '',
269
281
  title: '',
270
282
  levelLabel: '',
@@ -274,8 +286,7 @@ function buildEmptyForm(): CollaboratorFormState {
274
286
  leftAt: '',
275
287
  supervisorCollaboratorId: 'none',
276
288
  compensationAmount: '',
277
- contractDescription: '',
278
- autoGenerateContractDraft: true,
289
+ contractId: '',
279
290
  notes: '',
280
291
  equityParticipation: {
281
292
  participationType: 'partner_quota_holder',
@@ -319,7 +330,6 @@ function toFormState(
319
330
  return {
320
331
  userId: collaborator.userId ? String(collaborator.userId) : '',
321
332
  personId: collaborator.personId ? String(collaborator.personId) : '',
322
- code: collaborator.code ?? '',
323
333
  displayName: collaborator.displayName ?? '',
324
334
  collaboratorTypeId: collaborator.collaboratorTypeId
325
335
  ? String(collaborator.collaboratorTypeId)
@@ -327,7 +337,6 @@ function toFormState(
327
337
  departmentId: collaborator.departmentId
328
338
  ? String(collaborator.departmentId)
329
339
  : '',
330
- department: collaborator.department ?? '',
331
340
  jobTitleId: collaborator.jobTitleId ? String(collaborator.jobTitleId) : '',
332
341
  title: collaborator.title ?? '',
333
342
  levelLabel: normalizeCollaboratorLevel(collaborator.levelLabel),
@@ -350,8 +359,9 @@ function toFormState(
350
359
  collaborator.relatedContracts?.[0]?.budgetAmount !== undefined
351
360
  ? String(collaborator.relatedContracts[0].budgetAmount)
352
361
  : '',
353
- contractDescription: collaborator.relatedContracts?.[0]?.description ?? '',
354
- autoGenerateContractDraft: true,
362
+ contractId: collaborator.relatedContracts?.[0]?.id
363
+ ? String(collaborator.relatedContracts[0].id)
364
+ : '',
355
365
  notes: collaborator.notes ?? '',
356
366
  equityParticipation: {
357
367
  participationType:
@@ -677,6 +687,49 @@ export function CollaboratorFormScreen({
677
687
  fetchOperations<OperationsJobTitle[]>(request, '/operations/job-titles'),
678
688
  });
679
689
 
690
+ const [personSheetOpen, setPersonSheetOpen] = useState(false);
691
+ const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
692
+
693
+ const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
694
+ queryKey: ['contact-person-contact-types', currentLocaleCode],
695
+ enabled: personSheetOpen,
696
+ queryFn: async () => {
697
+ const response = await request<{ data: ContactTypeOption[] }>({
698
+ url: '/person-contact-type?pageSize=100',
699
+ method: 'GET',
700
+ });
701
+ return response.data.data || [];
702
+ },
703
+ placeholderData: (previous) => previous ?? [],
704
+ });
705
+
706
+ const { data: documentTypes = [] } = useQuery<DocumentTypeOption[]>({
707
+ queryKey: ['contact-person-document-types', currentLocaleCode],
708
+ enabled: personSheetOpen,
709
+ queryFn: async () => {
710
+ const response = await request<{ data: DocumentTypeOption[] }>({
711
+ url: '/person-document-type?pageSize=100',
712
+ method: 'GET',
713
+ });
714
+ return response.data.data || [];
715
+ },
716
+ placeholderData: (previous) => previous ?? [],
717
+ });
718
+
719
+ const handleOpenPersonSheet = async () => {
720
+ if (!collaborator?.personId) return;
721
+ try {
722
+ const response = await request<Person>({
723
+ url: `/person/${collaborator.personId}`,
724
+ method: 'GET',
725
+ });
726
+ setPersonToEdit(response.data);
727
+ setPersonSheetOpen(true);
728
+ } catch {
729
+ showToastHandler?.('error', t('messages.updateError'));
730
+ }
731
+ };
732
+
680
733
  useEffect(() => {
681
734
  if (collaborator) {
682
735
  // eslint-disable-next-line react-hooks/set-state-in-effect
@@ -793,40 +846,16 @@ export function CollaboratorFormScreen({
793
846
  };
794
847
 
795
848
  const departmentOptions = useMemo(() => {
796
- const selectedDepartment = trimToNull(form.department);
797
- const options: Array<{
798
- id?: number | null;
799
- name: string;
800
- code?: string | null;
801
- description?: string | null;
802
- }> = departments
803
- .filter(
804
- (item) => item.status === 'active' || item.name === selectedDepartment
805
- )
849
+ return departments
850
+ .filter((item) => item.status === 'active')
806
851
  .map((item) => ({
807
852
  id: item.id,
808
853
  name: item.name,
809
854
  code: item.code ?? null,
810
855
  description: item.description ?? null,
811
- }));
812
-
813
- if (selectedDepartment) {
814
- const alreadyIncluded = options.some(
815
- (item) => item.name === selectedDepartment
816
- );
817
-
818
- if (!alreadyIncluded) {
819
- options.push({
820
- id: form.departmentId ? Number(form.departmentId) : undefined,
821
- name: selectedDepartment,
822
- code: null,
823
- description: null,
824
- });
825
- }
826
- }
827
-
828
- return options.sort((left, right) => left.name.localeCompare(right.name));
829
- }, [departments, form.department, form.departmentId]);
856
+ }))
857
+ .sort((left, right) => left.name.localeCompare(right.name));
858
+ }, [departments]);
830
859
 
831
860
  const titleOptions = useMemo(() => {
832
861
  const selectedTitle = trimToNull(form.title);
@@ -968,11 +997,9 @@ export function CollaboratorFormScreen({
968
997
  const payload = {
969
998
  userId: userId ?? undefined,
970
999
  personId: personId ?? undefined,
971
- code: trimToNull(form.code) ?? undefined,
972
1000
  displayName: trimToNull(form.displayName),
973
1001
  collaboratorTypeId,
974
1002
  departmentId: departmentId ?? undefined,
975
- department: departmentId ? undefined : trimToNull(form.department),
976
1003
  jobTitleId: jobTitleId ?? undefined,
977
1004
  title: trimToNull(form.title),
978
1005
  levelLabel: trimToNull(form.levelLabel),
@@ -986,8 +1013,7 @@ export function CollaboratorFormScreen({
986
1013
  : parseNumberInput(form.supervisorCollaboratorId),
987
1014
  compensationAmount:
988
1015
  compensationAmount !== null ? compensationAmount : null,
989
- contractDescription: trimToNull(form.contractDescription),
990
- autoGenerateContractDraft: form.autoGenerateContractDraft,
1016
+ contractId: parseNumberInput(form.contractId),
991
1017
  notes: trimToNull(form.notes),
992
1018
  equityParticipation: shouldShowEquitySection
993
1019
  ? {
@@ -1101,7 +1127,7 @@ export function CollaboratorFormScreen({
1101
1127
  </div>
1102
1128
  </div>
1103
1129
 
1104
- <div className="flex flex-wrap gap-2">
1130
+ <div className="flex flex-wrap items-center gap-2">
1105
1131
  <StatusBadge
1106
1132
  label={getStatusLabel(collaborator.status)}
1107
1133
  className={getStatusBadgeClass(collaborator.status)}
@@ -1113,6 +1139,23 @@ export function CollaboratorFormScreen({
1113
1139
  collaborator.collaboratorTypeName
1114
1140
  )}
1115
1141
  </span>
1142
+ {collaborator.personId ? (
1143
+ <Tooltip>
1144
+ <TooltipTrigger asChild>
1145
+ <Button
1146
+ variant="outline"
1147
+ size="icon"
1148
+ className="h-7 w-7"
1149
+ onClick={() => void handleOpenPersonSheet()}
1150
+ >
1151
+ <Pencil className="size-3.5" />
1152
+ </Button>
1153
+ </TooltipTrigger>
1154
+ <TooltipContent side="bottom">
1155
+ {t('actions.editPersonCrm')}
1156
+ </TooltipContent>
1157
+ </Tooltip>
1158
+ ) : null}
1116
1159
  </div>
1117
1160
  </div>
1118
1161
 
@@ -1216,20 +1259,7 @@ export function CollaboratorFormScreen({
1216
1259
  {t('fields.userIdDescription')}
1217
1260
  </p>
1218
1261
  </div>
1219
- <div className="space-y-2">
1220
- <Label>{t('fields.code')}</Label>
1221
- <Input
1222
- className="h-10"
1223
- value={form.code}
1224
- placeholder="COL-001"
1225
- onChange={(event) =>
1226
- setForm((current) => ({
1227
- ...current,
1228
- code: event.target.value,
1229
- }))
1230
- }
1231
- />
1232
- </div>
1262
+
1233
1263
  {!isCreateMode ? (
1234
1264
  <div className="space-y-2">
1235
1265
  <Label>{t('fields.levelLabel')}</Label>
@@ -1258,7 +1288,10 @@ export function CollaboratorFormScreen({
1258
1288
  <div className="space-y-2 xl:col-span-1">
1259
1289
  <DepartmentSelectWithCreate
1260
1290
  label={t('fields.department')}
1261
- value={form.department}
1291
+ value={
1292
+ departmentOptions.find((d) => String(d.id) === form.departmentId)
1293
+ ?.name ?? ''
1294
+ }
1262
1295
  options={departmentOptions}
1263
1296
  selectPlaceholder={t('placeholders.department')}
1264
1297
  createDescription={t('fields.departmentDescription')}
@@ -1267,7 +1300,6 @@ export function CollaboratorFormScreen({
1267
1300
  setForm((current) => ({
1268
1301
  ...current,
1269
1302
  departmentId: department.id ? String(department.id) : '',
1270
- department: department.name,
1271
1303
  }))
1272
1304
  }
1273
1305
  />
@@ -1290,6 +1322,73 @@ export function CollaboratorFormScreen({
1290
1322
  }
1291
1323
  />
1292
1324
  </div>
1325
+ <div className="space-y-2 xl:col-span-1">
1326
+ <label className="text-sm font-medium">{t('fields.contract')}</label>
1327
+ <EntityPicker<OperationsContract>
1328
+ value={form.contractId ? Number(form.contractId) : null}
1329
+ onChange={(v) =>
1330
+ setForm((current) => ({
1331
+ ...current,
1332
+ contractId: v ? String(v) : '',
1333
+ }))
1334
+ }
1335
+ placeholder={t('fields.contractPlaceholder')}
1336
+ emptyLabel={t('fields.contractEmpty')}
1337
+ initialSelectedLabel={
1338
+ form.contractId
1339
+ ? (collaborator?.relatedContracts.find(
1340
+ (c) => String(c.id) === form.contractId
1341
+ )?.name ??
1342
+ collaborator?.relatedContracts.find(
1343
+ (c) => String(c.id) === form.contractId
1344
+ )?.code ??
1345
+ undefined)
1346
+ : undefined
1347
+ }
1348
+ clearable
1349
+ showCreateButton
1350
+ createTitle={t('fields.contractCreateTitle')}
1351
+ createDescription={t('fields.contractCreateDescription')}
1352
+ createFields={[
1353
+ {
1354
+ name: 'name',
1355
+ label: t('fields.contractNameField'),
1356
+ placeholder: t('fields.contractNameField'),
1357
+ required: true,
1358
+ },
1359
+ ]}
1360
+ loadOptions={async ({ page, pageSize, search }) => {
1361
+ const params = new URLSearchParams({
1362
+ page: String(page),
1363
+ pageSize: String(pageSize),
1364
+ });
1365
+ if (search.trim()) params.set('search', search.trim());
1366
+ const res = await fetchOperations<
1367
+ PaginatedResponse<OperationsContract>
1368
+ >(request, `/operations/contracts?${params.toString()}`);
1369
+ return {
1370
+ items: res.data ?? [],
1371
+ hasMore: res.page < res.lastPage,
1372
+ };
1373
+ }}
1374
+ getOptionValue={(c) => c.id}
1375
+ getOptionLabel={(c) => (c.name || c.code) as string}
1376
+ getOptionDescription={(c) =>
1377
+ [c.code, c.status].filter(Boolean).join(' • ') || undefined
1378
+ }
1379
+ onCreate={async (values) => {
1380
+ return mutateOperations<OperationsContract>(
1381
+ request,
1382
+ '/operations/contracts',
1383
+ 'POST',
1384
+ {
1385
+ name: values.name,
1386
+ billingModel: 'time_and_material',
1387
+ }
1388
+ );
1389
+ }}
1390
+ />
1391
+ </div>
1293
1392
  </div>
1294
1393
  </div>
1295
1394
  );
@@ -1618,7 +1717,7 @@ export function CollaboratorFormScreen({
1618
1717
  {t('sections.contractDescription')}
1619
1718
  </p>
1620
1719
  </div>
1621
- <div className="grid gap-3 md:grid-cols-2">
1720
+ <div className="grid gap-3 md:grid-cols-3">
1622
1721
  <div className="space-y-2">
1623
1722
  <label className="text-sm font-medium">
1624
1723
  {t('fields.weeklyCapacityHours')}
@@ -1654,36 +1753,17 @@ export function CollaboratorFormScreen({
1654
1753
  }
1655
1754
  />
1656
1755
  </div>
1657
- <div className="space-y-2 md:col-span-2">
1658
- <label className="text-sm font-medium">
1659
- {t('fields.contractDescription')}
1660
- </label>
1661
- <Textarea
1662
- rows={4}
1663
- value={form.contractDescription}
1664
- onChange={(event) =>
1665
- setForm((current) => ({
1666
- ...current,
1667
- contractDescription: event.target.value,
1668
- }))
1669
- }
1670
- />
1671
- </div>
1672
- <div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-2">
1673
- <div>
1674
- <div className="font-medium">
1675
- {t('fields.autoGenerateContractDraft')}
1676
- </div>
1677
- <div className="text-sm text-muted-foreground">
1678
- {t('fields.autoGenerateContractDraftDescription')}
1679
- </div>
1680
- </div>
1681
- <Switch
1682
- checked={form.autoGenerateContractDraft}
1683
- onCheckedChange={(checked) =>
1756
+ <div className="space-y-2">
1757
+ <SupervisorAutocomplete
1758
+ label={commonT('labels.supervisor')}
1759
+ value={form.supervisorCollaboratorId}
1760
+ options={supervisorOptions}
1761
+ placeholder={t('placeholders.supervisor')}
1762
+ emptyLabel={commonT('labels.notAssigned')}
1763
+ onChange={(value) =>
1684
1764
  setForm((current) => ({
1685
1765
  ...current,
1686
- autoGenerateContractDraft: checked,
1766
+ supervisorCollaboratorId: value,
1687
1767
  }))
1688
1768
  }
1689
1769
  />
@@ -2029,21 +2109,54 @@ export function CollaboratorFormScreen({
2029
2109
  {basicInfoSection}
2030
2110
  {employmentInfoSection}
2031
2111
  {!isCreateMode ? equitySection : null}
2032
- {supervisorSection}
2033
2112
  </div>
2034
2113
  );
2035
2114
 
2115
+ const costsSection = (
2116
+ <Accordion
2117
+ type="single"
2118
+ collapsible
2119
+ defaultValue={isCreateMode ? undefined : 'costs'}
2120
+ >
2121
+ <AccordionItem value="costs" className="rounded-xl border px-4">
2122
+ <AccordionTrigger className="py-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground hover:no-underline">
2123
+ {t('sections.costs')}
2124
+ </AccordionTrigger>
2125
+ <AccordionContent className="pb-4">
2126
+ {isCreateMode || !collaborator ? (
2127
+ <p className="text-sm text-muted-foreground">
2128
+ {t('sections.costsSaveBefore')}
2129
+ </p>
2130
+ ) : (
2131
+ <CollaboratorCostsSection
2132
+ collaboratorId={collaborator.id}
2133
+ disabled={!access.isDirector}
2134
+ weeklyCapacity={collaborator.weeklyCapacityHours ?? undefined}
2135
+ remunerationValue={
2136
+ collaborator.compensationAmount != null
2137
+ ? Number(collaborator.compensationAmount)
2138
+ : undefined
2139
+ }
2140
+ />
2141
+ )}
2142
+ </AccordionContent>
2143
+ </AccordionItem>
2144
+ </Accordion>
2145
+ );
2146
+
2036
2147
  const formContent = isSheetMode ? (
2037
2148
  <div className="space-y-4 px-4">
2038
2149
  {profileContent}
2039
- {!isCreateMode ? contractSection : null}
2150
+ {contractSection}
2151
+ {!isCreateMode ? costsSection : null}
2040
2152
  {!isCreateMode ? scheduleSection : null}
2041
2153
  {activitySection}
2042
2154
  </div>
2043
2155
  ) : (
2044
2156
  <div className="space-y-4 px-4">
2045
2157
  {profileContent}
2046
- {!isCreateMode ? contractSection : null}
2158
+ {contractSection}
2159
+ {!isCreateMode ? costsSection : null}
2047
2160
  {!isCreateMode ? scheduleSection : null}
2048
2161
  </div>
2049
2162
  );
@@ -2069,6 +2182,18 @@ export function CollaboratorFormScreen({
2069
2182
  submitLabel={commonT('actions.save')}
2070
2183
  submitSize="lg"
2071
2184
  />
2185
+
2186
+ <PersonFormSheet
2187
+ open={personSheetOpen}
2188
+ person={personToEdit}
2189
+ contactTypes={contactTypes}
2190
+ documentTypes={documentTypes}
2191
+ onOpenChange={(nextOpen) => {
2192
+ setPersonSheetOpen(nextOpen);
2193
+ if (!nextOpen) setPersonToEdit(null);
2194
+ }}
2195
+ onSuccess={() => void refetchCollaborator()}
2196
+ />
2072
2197
  </div>
2073
2198
  );
2074
2199
  }