@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,7 +1,17 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
4
5
  import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent } from '@/components/ui/card';
7
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
8
+ import {
9
+ Sheet,
10
+ SheetContent,
11
+ SheetDescription,
12
+ SheetHeader,
13
+ SheetTitle,
14
+ } from '@/components/ui/sheet';
5
15
  import {
6
16
  Table,
7
17
  TableBody,
@@ -10,11 +20,25 @@ import {
10
20
  TableHeader,
11
21
  TableRow,
12
22
  } from '@/components/ui/table';
23
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
13
24
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
14
- import { Eye, FileText, Pencil, UserRound } from 'lucide-react';
25
+ import {
26
+ Building2,
27
+ CalendarDays,
28
+ FileText,
29
+ LayoutGrid,
30
+ List,
31
+ Pencil,
32
+ Power,
33
+ UserCheck,
34
+ UserRound,
35
+ Users,
36
+ } from 'lucide-react';
37
+ import { useTranslations } from 'next-intl';
15
38
  import Link from 'next/link';
39
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
16
40
  import { useMemo, useState } from 'react';
17
- import { useTranslations } from 'next-intl';
41
+ import { CollaboratorFormScreen } from '../_components/collaborator-form-screen';
18
42
  import { OperationsHeader } from '../_components/operations-header';
19
43
  import { StatusBadge } from '../_components/status-badge';
20
44
  import { fetchOperations, mutateOperations } from '../_lib/api';
@@ -27,20 +51,133 @@ import {
27
51
  getStatusBadgeClass,
28
52
  } from '../_lib/utils/format';
29
53
 
54
+ const COLLABORATOR_VIEW_STORAGE_KEY = 'operations-collaborators-view-mode';
55
+
56
+ type CollaboratorViewMode = 'table' | 'cards';
57
+
58
+ function getPersonAvatarUrl(avatarId?: number | null) {
59
+ return typeof avatarId === 'number' && avatarId > 0
60
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
61
+ : '/placeholder.png';
62
+ }
63
+
64
+ function getInitials(value?: string | null) {
65
+ const parts = String(value ?? '')
66
+ .trim()
67
+ .split(/\s+/)
68
+ .filter(Boolean)
69
+ .slice(0, 2);
70
+
71
+ if (!parts.length) {
72
+ return '??';
73
+ }
74
+
75
+ return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
76
+ }
77
+
30
78
  export default function OperationsCollaboratorsPage() {
31
79
  const t = useTranslations('operations.CollaboratorsPage');
80
+ const formT = useTranslations('operations.CollaboratorFormPage');
32
81
  const commonT = useTranslations('operations.Common');
33
- const { request, showToastHandler, currentLocaleCode } = useApp();
82
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
83
+ useApp();
34
84
  const access = useOperationsAccess();
85
+ const router = useRouter();
86
+ const pathname = usePathname();
87
+ const searchParams = useSearchParams();
35
88
  const [search, setSearch] = useState('');
36
89
  const [statusFilter, setStatusFilter] = useState('all');
37
90
  const [typeFilter, setTypeFilter] = useState('all');
91
+ const [viewMode, setViewMode] = useState<CollaboratorViewMode>(() => {
92
+ if (typeof window === 'undefined') {
93
+ return 'table';
94
+ }
95
+
96
+ const savedViewMode = window.localStorage.getItem(
97
+ COLLABORATOR_VIEW_STORAGE_KEY
98
+ );
99
+
100
+ return savedViewMode === 'cards' ? 'cards' : 'table';
101
+ });
102
+ const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false);
103
+
104
+ const editParam = searchParams.get('edit');
105
+ const editingCollaboratorId =
106
+ editParam && /^\d+$/.test(editParam) ? Number(editParam) : null;
107
+
108
+ const updateSheetQuery = (collaboratorId?: number | null) => {
109
+ const params = new URLSearchParams(searchParams.toString());
110
+
111
+ if (collaboratorId && collaboratorId > 0) {
112
+ params.set('edit', String(collaboratorId));
113
+ } else {
114
+ params.delete('edit');
115
+ }
116
+
117
+ const nextUrl = params.size ? `${pathname}?${params.toString()}` : pathname;
118
+ router.replace(nextUrl, { scroll: false });
119
+ };
38
120
 
39
- const { data: collaborators = [], refetch } = useQuery<OperationsCollaborator[]>({
121
+ const openCreateSheet = () => {
122
+ setIsCreateSheetOpen(true);
123
+ updateSheetQuery(null);
124
+ };
125
+
126
+ const openEditSheet = (collaboratorId: number) => {
127
+ setIsCreateSheetOpen(false);
128
+ updateSheetQuery(collaboratorId);
129
+ };
130
+
131
+ const closeFormSheet = () => {
132
+ setIsCreateSheetOpen(false);
133
+
134
+ if (editingCollaboratorId !== null) {
135
+ updateSheetQuery(null);
136
+ }
137
+ };
138
+
139
+ const getCollaboratorTypeLabel = (value?: string | null) => {
140
+ switch (value) {
141
+ case 'clt':
142
+ return formT('options.collaboratorTypes.clt');
143
+ case 'pj':
144
+ return formT('options.collaboratorTypes.pj');
145
+ case 'freelancer':
146
+ return formT('options.collaboratorTypes.freelancer');
147
+ case 'intern':
148
+ return formT('options.collaboratorTypes.intern');
149
+ case 'other':
150
+ return formT('options.collaboratorTypes.other');
151
+ default:
152
+ return formatEnumLabel(value);
153
+ }
154
+ };
155
+
156
+ const getStatusLabel = (value?: string | null) => {
157
+ switch (value) {
158
+ case 'active':
159
+ return formT('options.statuses.active');
160
+ case 'on_leave':
161
+ return formT('options.statuses.on_leave');
162
+ case 'inactive':
163
+ return formT('options.statuses.inactive');
164
+ case 'draft':
165
+ return formT('options.statuses.draft');
166
+ default:
167
+ return formatEnumLabel(value);
168
+ }
169
+ };
170
+
171
+ const { data: collaborators = [], refetch } = useQuery<
172
+ OperationsCollaborator[]
173
+ >({
40
174
  queryKey: ['operations-collaborators-list', currentLocaleCode],
41
175
  enabled: access.isCollaborator,
42
176
  queryFn: () =>
43
- fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
177
+ fetchOperations<OperationsCollaborator[]>(
178
+ request,
179
+ '/operations/collaborators'
180
+ ),
44
181
  });
45
182
 
46
183
  const filteredRows = useMemo(
@@ -57,7 +194,9 @@ export default function OperationsCollaboratorsPage() {
57
194
  ]
58
195
  .filter(Boolean)
59
196
  .some((value) =>
60
- String(value).toLowerCase().includes(search.trim().toLowerCase())
197
+ String(value)
198
+ .toLowerCase()
199
+ .includes(search.trim().toLowerCase())
61
200
  );
62
201
  const matchesStatus =
63
202
  statusFilter === 'all' ? true : item.status === statusFilter;
@@ -68,8 +207,52 @@ export default function OperationsCollaboratorsPage() {
68
207
  [collaborators, search, statusFilter, typeFilter]
69
208
  );
70
209
 
210
+ const statsCards = useMemo(
211
+ () => [
212
+ {
213
+ key: 'total',
214
+ title: t('cards.total'),
215
+ value: collaborators.length,
216
+ icon: Users,
217
+ },
218
+ {
219
+ key: 'active',
220
+ title: t('cards.active'),
221
+ value: collaborators.filter((item) => item.status === 'active').length,
222
+ icon: UserCheck,
223
+ },
224
+ {
225
+ key: 'onLeave',
226
+ title: t('cards.onLeave'),
227
+ value: collaborators.filter((item) => item.status === 'on_leave')
228
+ .length,
229
+ icon: CalendarDays,
230
+ },
231
+ {
232
+ key: 'withContracts',
233
+ title: t('cards.withContracts'),
234
+ value: collaborators.filter((item) => Boolean(item.contractId)).length,
235
+ icon: FileText,
236
+ },
237
+ ],
238
+ [collaborators, t]
239
+ );
240
+
241
+ const handleViewModeChange = (value: string) => {
242
+ if (value !== 'table' && value !== 'cards') {
243
+ return;
244
+ }
245
+
246
+ setViewMode(value);
247
+
248
+ if (typeof window !== 'undefined') {
249
+ window.localStorage.setItem(COLLABORATOR_VIEW_STORAGE_KEY, value);
250
+ }
251
+ };
252
+
71
253
  const toggleStatus = async (collaborator: OperationsCollaborator) => {
72
- const nextStatus = collaborator.status === 'inactive' ? 'active' : 'inactive';
254
+ const nextStatus =
255
+ collaborator.status === 'inactive' ? 'active' : 'inactive';
73
256
 
74
257
  try {
75
258
  await mutateOperations(
@@ -93,169 +276,443 @@ export default function OperationsCollaboratorsPage() {
93
276
  current={t('breadcrumb')}
94
277
  actions={
95
278
  access.isDirector ? (
96
- <Button size="sm" asChild>
97
- <Link href="/operations/collaborators/new">
279
+ <div className="flex flex-wrap gap-2">
280
+ <Button variant="outline" size="sm" asChild>
281
+ <Link href="/operations/departments">
282
+ <Building2 className="size-4" />
283
+ {commonT('actions.manageDepartments')}
284
+ </Link>
285
+ </Button>
286
+ <Button size="sm" onClick={openCreateSheet}>
98
287
  {commonT('actions.create')}
99
- </Link>
100
- </Button>
288
+ </Button>
289
+ </div>
101
290
  ) : undefined
102
291
  }
103
292
  />
104
293
 
105
- <SearchBar
106
- searchQuery={search}
107
- onSearchChange={setSearch}
108
- onSearch={() => undefined}
109
- placeholder={t('searchPlaceholder')}
110
- controls={[
111
- {
112
- id: 'type',
113
- type: 'select',
114
- value: typeFilter,
115
- onChange: setTypeFilter,
116
- placeholder: commonT('labels.collaboratorType'),
117
- options: [
118
- { value: 'all', label: commonT('filters.allTypes') },
119
- { value: 'clt', label: 'CLT' },
120
- { value: 'pj', label: 'PJ' },
121
- { value: 'freelancer', label: 'Freelancer' },
122
- { value: 'intern', label: 'Intern' },
123
- { value: 'other', label: 'Other' }
124
- ]
125
- },
126
- {
127
- id: 'status',
128
- type: 'select',
129
- value: statusFilter,
130
- onChange: setStatusFilter,
131
- placeholder: commonT('labels.status'),
132
- options: [
133
- { value: 'all', label: commonT('filters.allStatuses') },
134
- { value: 'active', label: formatEnumLabel('active') },
135
- { value: 'on_leave', label: formatEnumLabel('on_leave') },
136
- { value: 'inactive', label: formatEnumLabel('inactive') }
137
- ]
138
- }
139
- ]}
140
- />
294
+ <KpiCardsGrid items={statsCards} columns={4} />
295
+
296
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
297
+ <div className="flex-1">
298
+ <SearchBar
299
+ searchQuery={search}
300
+ onSearchChange={setSearch}
301
+ onSearch={() => undefined}
302
+ placeholder={t('searchPlaceholder')}
303
+ controls={[
304
+ {
305
+ id: 'type',
306
+ type: 'select',
307
+ value: typeFilter,
308
+ onChange: setTypeFilter,
309
+ placeholder: commonT('labels.collaboratorType'),
310
+ options: [
311
+ { value: 'all', label: commonT('filters.allTypes') },
312
+ { value: 'clt', label: getCollaboratorTypeLabel('clt') },
313
+ { value: 'pj', label: getCollaboratorTypeLabel('pj') },
314
+ {
315
+ value: 'freelancer',
316
+ label: getCollaboratorTypeLabel('freelancer'),
317
+ },
318
+ {
319
+ value: 'intern',
320
+ label: getCollaboratorTypeLabel('intern'),
321
+ },
322
+ { value: 'other', label: getCollaboratorTypeLabel('other') },
323
+ ],
324
+ },
325
+ {
326
+ id: 'status',
327
+ type: 'select',
328
+ value: statusFilter,
329
+ onChange: setStatusFilter,
330
+ placeholder: commonT('labels.status'),
331
+ options: [
332
+ { value: 'all', label: commonT('filters.allStatuses') },
333
+ { value: 'active', label: getStatusLabel('active') },
334
+ { value: 'on_leave', label: getStatusLabel('on_leave') },
335
+ { value: 'inactive', label: getStatusLabel('inactive') },
336
+ ],
337
+ },
338
+ ]}
339
+ />
340
+ </div>
341
+
342
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
343
+ <span className="text-xs font-medium text-muted-foreground">
344
+ {t('viewMode')}
345
+ </span>
346
+ <ToggleGroup
347
+ type="single"
348
+ value={viewMode}
349
+ onValueChange={handleViewModeChange}
350
+ variant="outline"
351
+ size="sm"
352
+ aria-label={t('viewMode')}
353
+ >
354
+ <ToggleGroupItem
355
+ value="table"
356
+ className="gap-1.5 px-2.5"
357
+ aria-label={t('viewModeTable')}
358
+ >
359
+ <List className="h-4 w-4" />
360
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
361
+ </ToggleGroupItem>
362
+ <ToggleGroupItem
363
+ value="cards"
364
+ className="gap-1.5 px-2.5"
365
+ aria-label={t('viewModeCards')}
366
+ >
367
+ <LayoutGrid className="h-4 w-4" />
368
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
369
+ </ToggleGroupItem>
370
+ </ToggleGroup>
371
+ </div>
372
+ </div>
141
373
 
142
374
  {filteredRows.length > 0 ? (
143
- <div className="overflow-x-auto rounded-md border">
144
- <Table>
145
- <TableHeader>
146
- <TableRow>
147
- <TableHead>{commonT('labels.collaborator')}</TableHead>
148
- <TableHead>{commonT('labels.collaboratorType')}</TableHead>
149
- <TableHead>{commonT('labels.title')}</TableHead>
150
- <TableHead>{commonT('labels.status')}</TableHead>
151
- <TableHead>{commonT('labels.supervisor')}</TableHead>
152
- <TableHead>{commonT('labels.weeklyCapacity')}</TableHead>
153
- <TableHead>{commonT('labels.contractStatus')}</TableHead>
154
- <TableHead>{commonT('labels.startDate')}</TableHead>
155
- <TableHead className="w-[260px] text-right">
156
- {commonT('labels.actions')}
157
- </TableHead>
158
- </TableRow>
159
- </TableHeader>
160
- <TableBody>
161
- {filteredRows.map((collaborator) => (
162
- <TableRow key={collaborator.id}>
163
- <TableCell>
164
- <div className="font-medium">{collaborator.displayName}</div>
165
- <div className="text-xs text-muted-foreground">
166
- {collaborator.code}
375
+ viewMode === 'cards' ? (
376
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
377
+ {filteredRows.map((collaborator) => (
378
+ <Card
379
+ key={collaborator.id}
380
+ className="overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
381
+ >
382
+ <CardContent className="space-y-4 p-4">
383
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
384
+ <div className="flex min-w-0 items-center gap-3">
385
+ <Avatar className="h-12 w-12 border border-border/60 bg-muted">
386
+ <AvatarImage
387
+ src={getPersonAvatarUrl(collaborator.personAvatarId)}
388
+ alt={collaborator.displayName}
389
+ />
390
+ <AvatarFallback className="bg-muted text-sm font-semibold text-foreground">
391
+ {getInitials(collaborator.displayName)}
392
+ </AvatarFallback>
393
+ </Avatar>
394
+ <div className="min-w-0">
395
+ <div className="truncate font-semibold">
396
+ {collaborator.displayName}
397
+ </div>
398
+ <div className="truncate text-xs text-muted-foreground">
399
+ {[collaborator.code, collaborator.title]
400
+ .filter(Boolean)
401
+ .join(' • ') || commonT('labels.notAvailable')}
402
+ </div>
403
+ </div>
167
404
  </div>
168
- </TableCell>
169
- <TableCell>{formatEnumLabel(collaborator.collaboratorType)}</TableCell>
170
- <TableCell>
171
- {[collaborator.department, collaborator.title]
172
- .filter(Boolean)
173
- .join(' • ') || commonT('labels.notAvailable')}
174
- </TableCell>
175
- <TableCell>
176
405
  <StatusBadge
177
- label={formatEnumLabel(collaborator.status)}
406
+ label={getStatusLabel(collaborator.status)}
178
407
  className={getStatusBadgeClass(collaborator.status)}
179
408
  />
180
- </TableCell>
181
- <TableCell>
182
- {collaborator.supervisorName || commonT('labels.notAssigned')}
183
- </TableCell>
184
- <TableCell>{formatHours(collaborator.weeklyCapacityHours)}</TableCell>
185
- <TableCell>
186
- {collaborator.contractStatus ? (
187
- <StatusBadge
188
- label={formatEnumLabel(collaborator.contractStatus)}
189
- className={getStatusBadgeClass(collaborator.contractStatus)}
190
- />
191
- ) : (
192
- commonT('labels.notAssigned')
193
- )}
194
- </TableCell>
195
- <TableCell>{formatDate(collaborator.joinedAt)}</TableCell>
196
- <TableCell>
197
- <div className="flex justify-end gap-2">
198
- <Button variant="outline" size="icon" asChild>
199
- <Link href={`/operations/collaborators/${collaborator.id}`}>
200
- <Eye className="size-4" />
201
- </Link>
202
- </Button>
203
- {access.isDirector ? (
204
- <Button variant="outline" size="icon" asChild>
205
- <Link href={`/operations/collaborators/${collaborator.id}/edit`}>
206
- <Pencil className="size-4" />
207
- </Link>
208
- </Button>
209
- ) : null}
409
+ </div>
410
+
411
+ <div className="flex flex-wrap gap-2">
412
+ <span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
413
+ {getCollaboratorTypeLabel(collaborator.collaboratorType)}
414
+ </span>
415
+ {collaborator.department ? (
416
+ <span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
417
+ {collaborator.department}
418
+ </span>
419
+ ) : null}
420
+ </div>
421
+
422
+ <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
423
+ <div>
424
+ <span className="font-medium text-foreground">
425
+ {commonT('labels.supervisor')}:
426
+ </span>{' '}
427
+ {collaborator.supervisorName ||
428
+ commonT('labels.notAssigned')}
429
+ </div>
430
+ <div>
431
+ <span className="font-medium text-foreground">
432
+ {commonT('labels.weeklyCapacity')}:
433
+ </span>{' '}
434
+ {formatHours(collaborator.weeklyCapacityHours)}
435
+ </div>
436
+ <div>
437
+ <span className="font-medium text-foreground">
438
+ {commonT('labels.startDate')}:
439
+ </span>{' '}
440
+ {formatDate(
441
+ collaborator.joinedAt,
442
+ getSettingValue,
443
+ currentLocaleCode
444
+ )}
445
+ </div>
446
+ <div className="flex items-center gap-2">
447
+ <span className="font-medium text-foreground">
448
+ {commonT('labels.contractStatus')}:
449
+ </span>
450
+ {collaborator.contractStatus ? (
451
+ <StatusBadge
452
+ label={getStatusLabel(collaborator.contractStatus)}
453
+ className={getStatusBadgeClass(
454
+ collaborator.contractStatus
455
+ )}
456
+ />
457
+ ) : (
458
+ <span>{commonT('labels.notAssigned')}</span>
459
+ )}
460
+ </div>
461
+ </div>
462
+
463
+ <div className="flex flex-wrap justify-end gap-2 border-t border-border/60 pt-3">
464
+ {access.isDirector ? (
210
465
  <Button
211
466
  variant="outline"
212
467
  size="icon"
213
- asChild={Boolean(collaborator.contractId)}
214
- disabled={!collaborator.contractId}
468
+ onClick={() => openEditSheet(collaborator.id)}
215
469
  >
216
- {collaborator.contractId ? (
217
- <Link href={`/operations/contracts?edit=${collaborator.contractId}`}>
218
- <FileText className="size-4" />
219
- </Link>
220
- ) : (
221
- <span>
222
- <FileText className="size-4" />
223
- </span>
224
- )}
470
+ <Pencil className="size-4" />
225
471
  </Button>
226
- {access.isDirector ? (
227
- <Button
228
- variant="outline"
229
- size="sm"
230
- onClick={() => void toggleStatus(collaborator)}
472
+ ) : null}
473
+ <Button
474
+ variant="outline"
475
+ size="icon"
476
+ asChild={Boolean(collaborator.contractId)}
477
+ disabled={!collaborator.contractId}
478
+ >
479
+ {collaborator.contractId ? (
480
+ <Link
481
+ href={`/operations/contracts?edit=${collaborator.contractId}`}
231
482
  >
483
+ <FileText className="size-4" />
484
+ </Link>
485
+ ) : (
486
+ <span>
487
+ <FileText className="size-4" />
488
+ </span>
489
+ )}
490
+ </Button>
491
+ {access.isDirector ? (
492
+ <Button
493
+ variant="outline"
494
+ size="sm"
495
+ className="gap-1.5 px-2 sm:px-3"
496
+ onClick={() => void toggleStatus(collaborator)}
497
+ >
498
+ <Power className="size-4" />
499
+ <span className="hidden sm:inline">
232
500
  {collaborator.status === 'inactive'
233
501
  ? commonT('actions.activate')
234
502
  : commonT('actions.deactivate')}
235
- </Button>
236
- ) : null}
237
- </div>
238
- </TableCell>
503
+ </span>
504
+ </Button>
505
+ ) : null}
506
+ </div>
507
+ </CardContent>
508
+ </Card>
509
+ ))}
510
+ </div>
511
+ ) : (
512
+ <div className="overflow-hidden rounded-xl border bg-card shadow-sm">
513
+ <Table className="table-fixed">
514
+ <TableHeader>
515
+ <TableRow>
516
+ <TableHead className="w-[34%]">
517
+ {commonT('labels.collaborator')}
518
+ </TableHead>
519
+ <TableHead className="hidden md:table-cell">
520
+ {commonT('labels.collaboratorType')}
521
+ </TableHead>
522
+ <TableHead className="hidden sm:table-cell">
523
+ {commonT('labels.title')}
524
+ </TableHead>
525
+ <TableHead>{commonT('labels.status')}</TableHead>
526
+ <TableHead className="hidden lg:table-cell">
527
+ {commonT('labels.supervisor')}
528
+ </TableHead>
529
+ <TableHead className="hidden xl:table-cell">
530
+ {commonT('labels.weeklyCapacity')}
531
+ </TableHead>
532
+ <TableHead className="hidden 2xl:table-cell">
533
+ {commonT('labels.contractStatus')}
534
+ </TableHead>
535
+ <TableHead className="hidden 2xl:table-cell">
536
+ {commonT('labels.startDate')}
537
+ </TableHead>
538
+ <TableHead className="w-30 text-right sm:w-42.5">
539
+ {commonT('labels.actions')}
540
+ </TableHead>
239
541
  </TableRow>
240
- ))}
241
- </TableBody>
242
- </Table>
243
- </div>
542
+ </TableHeader>
543
+ <TableBody>
544
+ {filteredRows.map((collaborator) => (
545
+ <TableRow key={collaborator.id} className="hover:bg-muted/30">
546
+ <TableCell>
547
+ <div className="flex items-center gap-3">
548
+ <Avatar className="h-9 w-9 border border-border/60 bg-muted">
549
+ <AvatarImage
550
+ src={getPersonAvatarUrl(
551
+ collaborator.personAvatarId
552
+ )}
553
+ alt={collaborator.displayName}
554
+ />
555
+ <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
556
+ {getInitials(collaborator.displayName)}
557
+ </AvatarFallback>
558
+ </Avatar>
559
+ <div className="min-w-0">
560
+ <div className="truncate font-medium">
561
+ {collaborator.displayName}
562
+ </div>
563
+ <div className="truncate text-xs text-muted-foreground">
564
+ {[
565
+ collaborator.code,
566
+ collaborator.department,
567
+ collaborator.title,
568
+ ]
569
+ .filter(Boolean)
570
+ .join(' • ')}
571
+ </div>
572
+ </div>
573
+ </div>
574
+ </TableCell>
575
+ <TableCell className="hidden md:table-cell">
576
+ {getCollaboratorTypeLabel(collaborator.collaboratorType)}
577
+ </TableCell>
578
+ <TableCell className="hidden max-w-55 sm:table-cell">
579
+ <div className="truncate">
580
+ {[collaborator.department, collaborator.title]
581
+ .filter(Boolean)
582
+ .join(' • ') || commonT('labels.notAvailable')}
583
+ </div>
584
+ </TableCell>
585
+ <TableCell>
586
+ <StatusBadge
587
+ label={getStatusLabel(collaborator.status)}
588
+ className={getStatusBadgeClass(collaborator.status)}
589
+ />
590
+ </TableCell>
591
+ <TableCell className="hidden lg:table-cell">
592
+ <div className="truncate">
593
+ {collaborator.supervisorName ||
594
+ commonT('labels.notAssigned')}
595
+ </div>
596
+ </TableCell>
597
+ <TableCell className="hidden xl:table-cell">
598
+ {formatHours(collaborator.weeklyCapacityHours)}
599
+ </TableCell>
600
+ <TableCell className="hidden 2xl:table-cell">
601
+ {collaborator.contractStatus ? (
602
+ <StatusBadge
603
+ label={getStatusLabel(collaborator.contractStatus)}
604
+ className={getStatusBadgeClass(
605
+ collaborator.contractStatus
606
+ )}
607
+ />
608
+ ) : (
609
+ commonT('labels.notAssigned')
610
+ )}
611
+ </TableCell>
612
+ <TableCell className="hidden 2xl:table-cell">
613
+ {formatDate(
614
+ collaborator.joinedAt,
615
+ getSettingValue,
616
+ currentLocaleCode
617
+ )}
618
+ </TableCell>
619
+ <TableCell>
620
+ <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
621
+ {access.isDirector ? (
622
+ <Button
623
+ variant="outline"
624
+ size="icon"
625
+ onClick={() => openEditSheet(collaborator.id)}
626
+ >
627
+ <Pencil className="size-4" />
628
+ </Button>
629
+ ) : null}
630
+ <Button
631
+ variant="outline"
632
+ size="icon"
633
+ asChild={Boolean(collaborator.contractId)}
634
+ disabled={!collaborator.contractId}
635
+ >
636
+ {collaborator.contractId ? (
637
+ <Link
638
+ href={`/operations/contracts?edit=${collaborator.contractId}`}
639
+ >
640
+ <FileText className="size-4" />
641
+ </Link>
642
+ ) : (
643
+ <span>
644
+ <FileText className="size-4" />
645
+ </span>
646
+ )}
647
+ </Button>
648
+ {access.isDirector ? (
649
+ <Button
650
+ variant="outline"
651
+ size="sm"
652
+ className="gap-1.5 px-2 sm:px-3"
653
+ onClick={() => void toggleStatus(collaborator)}
654
+ >
655
+ <Power className="size-4" />
656
+ <span className="hidden sm:inline">
657
+ {collaborator.status === 'inactive'
658
+ ? commonT('actions.activate')
659
+ : commonT('actions.deactivate')}
660
+ </span>
661
+ </Button>
662
+ ) : null}
663
+ </div>
664
+ </TableCell>
665
+ </TableRow>
666
+ ))}
667
+ </TableBody>
668
+ </Table>
669
+ </div>
670
+ )
244
671
  ) : (
245
672
  <EmptyState
246
673
  icon={<UserRound className="size-12" />}
247
674
  title={commonT('states.emptyTitle')}
248
675
  description={t('emptyDescription')}
249
- actionLabel={access.isDirector ? commonT('actions.create') : commonT('actions.refresh')}
250
- onAction={
676
+ actionLabel={
251
677
  access.isDirector
252
- ? () => {
253
- window.location.href = '/operations/collaborators/new';
254
- }
255
- : () => void refetch()
678
+ ? commonT('actions.create')
679
+ : commonT('actions.refresh')
256
680
  }
681
+ onAction={access.isDirector ? openCreateSheet : () => void refetch()}
257
682
  />
258
683
  )}
684
+
685
+ <Sheet
686
+ open={isCreateSheetOpen || editingCollaboratorId !== null}
687
+ onOpenChange={(open) => {
688
+ if (!open) {
689
+ closeFormSheet();
690
+ }
691
+ }}
692
+ >
693
+ <SheetContent className="w-full overflow-y-auto sm:max-w-6xl">
694
+ <SheetHeader>
695
+ <SheetTitle>
696
+ {editingCollaboratorId
697
+ ? t('sheet.editTitle')
698
+ : t('sheet.createTitle')}
699
+ </SheetTitle>
700
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
701
+ </SheetHeader>
702
+
703
+ <CollaboratorFormScreen
704
+ key={
705
+ editingCollaboratorId ? `edit-${editingCollaboratorId}` : 'create'
706
+ }
707
+ collaboratorId={editingCollaboratorId ?? undefined}
708
+ onCancel={closeFormSheet}
709
+ onSaved={async () => {
710
+ closeFormSheet();
711
+ await refetch();
712
+ }}
713
+ />
714
+ </SheetContent>
715
+ </Sheet>
259
716
  </Page>
260
717
  );
261
718
  }