@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.
- package/dist/operations.controller.d.ts +713 -31
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.controller.js +157 -0
- package/dist/operations.controller.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +5 -1
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.proposal.subscriber.d.ts +11 -0
- package/dist/operations.proposal.subscriber.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.js +80 -0
- package/dist/operations.proposal.subscriber.js.map +1 -0
- package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
- package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.spec.js +88 -0
- package/dist/operations.proposal.subscriber.spec.js.map +1 -0
- package/dist/operations.service.d.ts +490 -46
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +3590 -1267
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.d.ts +2 -0
- package/dist/operations.service.spec.d.ts.map +1 -0
- package/dist/operations.service.spec.js +159 -0
- package/dist/operations.service.spec.js.map +1 -0
- package/hedhog/data/menu.yaml +232 -198
- package/hedhog/data/role.yaml +23 -23
- package/hedhog/data/role_route.yaml +39 -0
- package/hedhog/data/route.yaml +447 -317
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
- package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
- package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
- package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
- package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
- package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
- package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
- package/hedhog/frontend/app/page.tsx.ejs +36 -12
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
- package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
- package/hedhog/frontend/messages/en.json +473 -12
- package/hedhog/frontend/messages/pt.json +528 -66
- package/hedhog/table/operations_approval.yaml +49 -49
- package/hedhog/table/operations_approval_history.yaml +29 -29
- package/hedhog/table/operations_collaborator.yaml +87 -67
- package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
- package/hedhog/table/operations_contract.yaml +121 -100
- package/hedhog/table/operations_contract_document.yaml +40 -23
- package/hedhog/table/operations_contract_financial_term.yaml +40 -40
- package/hedhog/table/operations_contract_history.yaml +27 -27
- package/hedhog/table/operations_contract_party.yaml +46 -46
- package/hedhog/table/operations_contract_revision.yaml +38 -38
- package/hedhog/table/operations_contract_signature.yaml +38 -38
- package/hedhog/table/operations_contract_template.yaml +58 -0
- package/hedhog/table/operations_department.yaml +24 -0
- package/hedhog/table/operations_project.yaml +54 -54
- package/hedhog/table/operations_project_assignment.yaml +55 -55
- package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
- package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
- package/hedhog/table/operations_time_off_request.yaml +57 -57
- package/hedhog/table/operations_timesheet.yaml +41 -41
- package/hedhog/table/operations_timesheet_entry.yaml +40 -40
- package/package.json +5 -3
- package/src/operations.controller.ts +304 -182
- package/src/operations.module.ts +26 -22
- package/src/operations.proposal.subscriber.spec.ts +121 -0
- package/src/operations.proposal.subscriber.ts +86 -0
- package/src/operations.service.spec.ts +210 -0
- package/src/operations.service.ts +7317 -3595
- package/dist/operations-data.controller.d.ts +0 -139
- package/dist/operations-data.controller.d.ts.map +0 -1
- package/dist/operations-data.controller.js +0 -113
- package/dist/operations-data.controller.js.map +0 -1
- package/dist/operations-growth.controller.d.ts +0 -48
- package/dist/operations-growth.controller.d.ts.map +0 -1
- package/dist/operations-growth.controller.js +0 -90
- 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 {
|
|
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 {
|
|
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 } =
|
|
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
|
|
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[]>(
|
|
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)
|
|
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 =
|
|
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
|
-
<
|
|
97
|
-
<
|
|
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
|
-
</
|
|
100
|
-
</
|
|
288
|
+
</Button>
|
|
289
|
+
</div>
|
|
101
290
|
) : undefined
|
|
102
291
|
}
|
|
103
292
|
/>
|
|
104
293
|
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
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
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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={
|
|
406
|
+
label={getStatusLabel(collaborator.status)}
|
|
178
407
|
className={getStatusBadgeClass(collaborator.status)}
|
|
179
408
|
/>
|
|
180
|
-
</
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{collaborator.
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
disabled={!collaborator.contractId}
|
|
468
|
+
onClick={() => openEditSheet(collaborator.id)}
|
|
215
469
|
>
|
|
216
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
</
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
</
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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={
|
|
250
|
-
onAction={
|
|
676
|
+
actionLabel={
|
|
251
677
|
access.isDirector
|
|
252
|
-
? ()
|
|
253
|
-
|
|
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
|
}
|