@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,8 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page } from '@/components/entity-list';
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
4
5
|
import { Button } from '@/components/ui/button';
|
|
6
|
+
import {
|
|
7
|
+
Command,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandGroup,
|
|
10
|
+
CommandInput,
|
|
11
|
+
CommandItem,
|
|
12
|
+
CommandList,
|
|
13
|
+
} from '@/components/ui/command';
|
|
14
|
+
import { FormActions } from '@/components/ui/form-actions';
|
|
5
15
|
import { Input } from '@/components/ui/input';
|
|
16
|
+
import { InputMoney } from '@/components/ui/input-money';
|
|
17
|
+
import { Label } from '@/components/ui/label';
|
|
18
|
+
import {
|
|
19
|
+
Popover,
|
|
20
|
+
PopoverContent,
|
|
21
|
+
PopoverTrigger,
|
|
22
|
+
} from '@/components/ui/popover';
|
|
6
23
|
import {
|
|
7
24
|
Select,
|
|
8
25
|
SelectContent,
|
|
@@ -12,23 +29,44 @@ import {
|
|
|
12
29
|
} from '@/components/ui/select';
|
|
13
30
|
import { Switch } from '@/components/ui/switch';
|
|
14
31
|
import { Textarea } from '@/components/ui/textarea';
|
|
32
|
+
import {
|
|
33
|
+
Tooltip,
|
|
34
|
+
TooltipContent,
|
|
35
|
+
TooltipTrigger,
|
|
36
|
+
} from '@/components/ui/tooltip';
|
|
15
37
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
16
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ArrowLeft,
|
|
40
|
+
Check,
|
|
41
|
+
ChevronsUpDown,
|
|
42
|
+
Info,
|
|
43
|
+
Save,
|
|
44
|
+
UserRound,
|
|
45
|
+
} from 'lucide-react';
|
|
46
|
+
import { useTranslations } from 'next-intl';
|
|
17
47
|
import Link from 'next/link';
|
|
18
48
|
import { useRouter } from 'next/navigation';
|
|
19
|
-
import { useEffect, useState } from 'react';
|
|
20
|
-
import { useTranslations } from 'next-intl';
|
|
21
|
-
import { OperationsHeader } from './operations-header';
|
|
22
|
-
import { SectionCard } from './section-card';
|
|
49
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
23
50
|
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
24
51
|
import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
|
|
25
52
|
import type {
|
|
26
53
|
OperationsCollaborator,
|
|
27
54
|
OperationsCollaboratorDetails,
|
|
55
|
+
OperationsDepartment,
|
|
28
56
|
OperationsWeeklyScheduleDay,
|
|
29
57
|
} from '../_lib/types';
|
|
58
|
+
import {
|
|
59
|
+
formatDate,
|
|
60
|
+
formatDateRange,
|
|
61
|
+
formatEnumLabel,
|
|
62
|
+
formatHours,
|
|
63
|
+
getStatusBadgeClass,
|
|
64
|
+
} from '../_lib/utils/format';
|
|
30
65
|
import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
|
|
31
|
-
import {
|
|
66
|
+
import { DepartmentSelectWithCreate } from './department-select-with-create';
|
|
67
|
+
import { OperationsHeader } from './operations-header';
|
|
68
|
+
import { PersonSelectWithCreate } from './person-select-with-create';
|
|
69
|
+
import { StatusBadge } from './status-badge';
|
|
32
70
|
|
|
33
71
|
const weekdays = [
|
|
34
72
|
'monday',
|
|
@@ -40,11 +78,55 @@ const weekdays = [
|
|
|
40
78
|
'sunday',
|
|
41
79
|
] as const;
|
|
42
80
|
|
|
81
|
+
const SUPERVISOR_PAGE_SIZE = 10;
|
|
82
|
+
|
|
83
|
+
function getPersonAvatarUrl(avatarId?: number | null) {
|
|
84
|
+
return typeof avatarId === 'number' && avatarId > 0
|
|
85
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
86
|
+
: '/placeholder.png';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getInitials(value?: string | null) {
|
|
90
|
+
const parts = String(value ?? '')
|
|
91
|
+
.trim()
|
|
92
|
+
.split(/\s+/)
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.slice(0, 2);
|
|
95
|
+
|
|
96
|
+
if (!parts.length) {
|
|
97
|
+
return '??';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeTimeValue(value?: string | null, fallback = '') {
|
|
104
|
+
if (!value) {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const directMatch = String(value).match(/(\d{2}:\d{2})(?::\d{2})?/);
|
|
109
|
+
|
|
110
|
+
if (directMatch?.[1]) {
|
|
111
|
+
return directMatch[1];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parsedDate = new Date(value);
|
|
115
|
+
|
|
116
|
+
if (!Number.isNaN(parsedDate.getTime())) {
|
|
117
|
+
return parsedDate.toISOString().slice(11, 16);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return fallback || String(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
43
123
|
type CollaboratorFormState = {
|
|
44
124
|
userId: string;
|
|
125
|
+
personId: string;
|
|
45
126
|
code: string;
|
|
46
127
|
displayName: string;
|
|
47
128
|
collaboratorType: string;
|
|
129
|
+
departmentId: string;
|
|
48
130
|
department: string;
|
|
49
131
|
title: string;
|
|
50
132
|
levelLabel: string;
|
|
@@ -79,9 +161,11 @@ function defaultSchedule(): CollaboratorFormState['weeklySchedule'] {
|
|
|
79
161
|
function buildEmptyForm(): CollaboratorFormState {
|
|
80
162
|
return {
|
|
81
163
|
userId: '',
|
|
164
|
+
personId: '',
|
|
82
165
|
code: '',
|
|
83
166
|
displayName: '',
|
|
84
167
|
collaboratorType: 'clt',
|
|
168
|
+
departmentId: '',
|
|
85
169
|
department: '',
|
|
86
170
|
title: '',
|
|
87
171
|
levelLabel: '',
|
|
@@ -110,9 +194,10 @@ function normalizeSchedule(
|
|
|
110
194
|
|
|
111
195
|
return {
|
|
112
196
|
weekday,
|
|
113
|
-
isWorkingDay:
|
|
114
|
-
|
|
115
|
-
|
|
197
|
+
isWorkingDay:
|
|
198
|
+
item?.isWorkingDay ?? !['saturday', 'sunday'].includes(weekday),
|
|
199
|
+
startTime: normalizeTimeValue(item?.startTime, '09:00'),
|
|
200
|
+
endTime: normalizeTimeValue(item?.endTime, '18:00'),
|
|
116
201
|
breakMinutes:
|
|
117
202
|
item?.breakMinutes !== null && item?.breakMinutes !== undefined
|
|
118
203
|
? String(item.breakMinutes)
|
|
@@ -126,9 +211,13 @@ function toFormState(
|
|
|
126
211
|
): CollaboratorFormState {
|
|
127
212
|
return {
|
|
128
213
|
userId: collaborator.userId ? String(collaborator.userId) : '',
|
|
214
|
+
personId: collaborator.personId ? String(collaborator.personId) : '',
|
|
129
215
|
code: collaborator.code ?? '',
|
|
130
216
|
displayName: collaborator.displayName ?? '',
|
|
131
217
|
collaboratorType: collaborator.collaboratorType ?? 'other',
|
|
218
|
+
departmentId: collaborator.departmentId
|
|
219
|
+
? String(collaborator.departmentId)
|
|
220
|
+
: '',
|
|
132
221
|
department: collaborator.department ?? '',
|
|
133
222
|
title: collaborator.title ?? '',
|
|
134
223
|
levelLabel: collaborator.levelLabel ?? '',
|
|
@@ -155,23 +244,220 @@ function toFormState(
|
|
|
155
244
|
};
|
|
156
245
|
}
|
|
157
246
|
|
|
247
|
+
type SupervisorAutocompleteProps = {
|
|
248
|
+
label: string;
|
|
249
|
+
value: string;
|
|
250
|
+
options: OperationsCollaborator[];
|
|
251
|
+
placeholder: string;
|
|
252
|
+
emptyLabel: string;
|
|
253
|
+
onChange: (value: string) => void;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
function SupervisorAutocomplete({
|
|
257
|
+
label,
|
|
258
|
+
value,
|
|
259
|
+
options,
|
|
260
|
+
placeholder,
|
|
261
|
+
emptyLabel,
|
|
262
|
+
onChange,
|
|
263
|
+
}: SupervisorAutocompleteProps) {
|
|
264
|
+
const commonT = useTranslations('operations.Common');
|
|
265
|
+
const [open, setOpen] = useState(false);
|
|
266
|
+
const [search, setSearch] = useState('');
|
|
267
|
+
const [visibleCount, setVisibleCount] = useState(SUPERVISOR_PAGE_SIZE);
|
|
268
|
+
|
|
269
|
+
const filteredOptions = useMemo(() => {
|
|
270
|
+
const normalizedSearch = search.trim().toLowerCase();
|
|
271
|
+
|
|
272
|
+
if (!normalizedSearch) {
|
|
273
|
+
return options;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return options.filter((option) =>
|
|
277
|
+
[option.displayName, option.code, option.department, option.title]
|
|
278
|
+
.filter(Boolean)
|
|
279
|
+
.some((field) => String(field).toLowerCase().includes(normalizedSearch))
|
|
280
|
+
);
|
|
281
|
+
}, [options, search]);
|
|
282
|
+
|
|
283
|
+
const selectedOption = options.find((option) => String(option.id) === value);
|
|
284
|
+
const visibleOptions = filteredOptions.slice(0, visibleCount);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className="grid gap-2">
|
|
288
|
+
<Label>{label}</Label>
|
|
289
|
+
|
|
290
|
+
<Popover
|
|
291
|
+
open={open}
|
|
292
|
+
onOpenChange={(nextOpen) => {
|
|
293
|
+
setOpen(nextOpen);
|
|
294
|
+
setVisibleCount(SUPERVISOR_PAGE_SIZE);
|
|
295
|
+
|
|
296
|
+
if (!nextOpen) {
|
|
297
|
+
setSearch('');
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
<PopoverTrigger asChild>
|
|
302
|
+
<Button
|
|
303
|
+
type="button"
|
|
304
|
+
variant="outline"
|
|
305
|
+
role="combobox"
|
|
306
|
+
className="w-full justify-between overflow-hidden"
|
|
307
|
+
>
|
|
308
|
+
<span className="truncate text-left">
|
|
309
|
+
{selectedOption?.displayName ?? emptyLabel}
|
|
310
|
+
</span>
|
|
311
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
312
|
+
</Button>
|
|
313
|
+
</PopoverTrigger>
|
|
314
|
+
<PopoverContent
|
|
315
|
+
className="p-0"
|
|
316
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
317
|
+
>
|
|
318
|
+
<Command shouldFilter={false}>
|
|
319
|
+
<CommandInput
|
|
320
|
+
placeholder={placeholder}
|
|
321
|
+
value={search}
|
|
322
|
+
onValueChange={(value) => {
|
|
323
|
+
setSearch(value);
|
|
324
|
+
setVisibleCount(SUPERVISOR_PAGE_SIZE);
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
<CommandList>
|
|
328
|
+
<CommandEmpty>
|
|
329
|
+
<div className="p-2 text-sm text-muted-foreground">
|
|
330
|
+
{commonT('states.emptyDescription')}
|
|
331
|
+
</div>
|
|
332
|
+
</CommandEmpty>
|
|
333
|
+
|
|
334
|
+
<CommandGroup>
|
|
335
|
+
<CommandItem
|
|
336
|
+
value="none"
|
|
337
|
+
onSelect={() => {
|
|
338
|
+
onChange('none');
|
|
339
|
+
setOpen(false);
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
{value === 'none' ? (
|
|
343
|
+
<Check className="mr-2 h-4 w-4" />
|
|
344
|
+
) : (
|
|
345
|
+
<span className="mr-2 h-4 w-4" />
|
|
346
|
+
)}
|
|
347
|
+
{emptyLabel}
|
|
348
|
+
</CommandItem>
|
|
349
|
+
|
|
350
|
+
{visibleOptions.map((option) => (
|
|
351
|
+
<CommandItem
|
|
352
|
+
key={option.id}
|
|
353
|
+
value={`${option.displayName} ${option.code ?? ''} ${option.department ?? ''} ${option.title ?? ''}`}
|
|
354
|
+
onSelect={() => {
|
|
355
|
+
onChange(String(option.id));
|
|
356
|
+
setOpen(false);
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{String(option.id) === value ? (
|
|
360
|
+
<Check className="mr-2 h-4 w-4" />
|
|
361
|
+
) : (
|
|
362
|
+
<span className="mr-2 h-4 w-4" />
|
|
363
|
+
)}
|
|
364
|
+
<div className="min-w-0">
|
|
365
|
+
<div className="truncate">{option.displayName}</div>
|
|
366
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
367
|
+
{[option.code, option.department, option.title]
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.join(' • ') || commonT('labels.notAvailable')}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</CommandItem>
|
|
373
|
+
))}
|
|
374
|
+
</CommandGroup>
|
|
375
|
+
|
|
376
|
+
{filteredOptions.length > visibleCount ? (
|
|
377
|
+
<div className="border-t p-2">
|
|
378
|
+
<Button
|
|
379
|
+
type="button"
|
|
380
|
+
variant="ghost"
|
|
381
|
+
className="w-full"
|
|
382
|
+
onClick={() =>
|
|
383
|
+
setVisibleCount(
|
|
384
|
+
(current) => current + SUPERVISOR_PAGE_SIZE
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
>
|
|
388
|
+
{commonT('actions.loadMore')}
|
|
389
|
+
</Button>
|
|
390
|
+
</div>
|
|
391
|
+
) : null}
|
|
392
|
+
</CommandList>
|
|
393
|
+
</Command>
|
|
394
|
+
</PopoverContent>
|
|
395
|
+
</Popover>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
158
400
|
type CollaboratorFormScreenProps = {
|
|
159
401
|
collaboratorId?: number;
|
|
402
|
+
onSaved?: (
|
|
403
|
+
collaborator: OperationsCollaboratorDetails
|
|
404
|
+
) => void | Promise<void>;
|
|
405
|
+
onCancel?: () => void;
|
|
160
406
|
};
|
|
161
407
|
|
|
162
408
|
export function CollaboratorFormScreen({
|
|
163
409
|
collaboratorId,
|
|
410
|
+
onSaved,
|
|
411
|
+
onCancel,
|
|
164
412
|
}: CollaboratorFormScreenProps) {
|
|
165
413
|
const t = useTranslations('operations.CollaboratorFormPage');
|
|
166
414
|
const commonT = useTranslations('operations.Common');
|
|
415
|
+
const detailsT = useTranslations('operations.CollaboratorDetailsPage');
|
|
167
416
|
const { request, showToastHandler, currentLocaleCode } = useApp();
|
|
168
417
|
const access = useOperationsAccess();
|
|
169
418
|
const router = useRouter();
|
|
170
419
|
const [form, setForm] = useState<CollaboratorFormState>(buildEmptyForm());
|
|
420
|
+
const isSheetMode = Boolean(onCancel);
|
|
421
|
+
|
|
422
|
+
const getCollaboratorTypeLabel = (value?: string | null) => {
|
|
423
|
+
switch (value) {
|
|
424
|
+
case 'clt':
|
|
425
|
+
return t('options.collaboratorTypes.clt');
|
|
426
|
+
case 'pj':
|
|
427
|
+
return t('options.collaboratorTypes.pj');
|
|
428
|
+
case 'freelancer':
|
|
429
|
+
return t('options.collaboratorTypes.freelancer');
|
|
430
|
+
case 'intern':
|
|
431
|
+
return t('options.collaboratorTypes.intern');
|
|
432
|
+
case 'other':
|
|
433
|
+
return t('options.collaboratorTypes.other');
|
|
434
|
+
default:
|
|
435
|
+
return formatEnumLabel(value);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const getStatusLabel = (value?: string | null) => {
|
|
440
|
+
switch (value) {
|
|
441
|
+
case 'active':
|
|
442
|
+
return t('options.statuses.active');
|
|
443
|
+
case 'on_leave':
|
|
444
|
+
return t('options.statuses.on_leave');
|
|
445
|
+
case 'inactive':
|
|
446
|
+
return t('options.statuses.inactive');
|
|
447
|
+
case 'draft':
|
|
448
|
+
return t('options.statuses.draft');
|
|
449
|
+
default:
|
|
450
|
+
return formatEnumLabel(value);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
171
453
|
|
|
172
454
|
const { data: collaborator, isLoading: isLoadingCollaborator } =
|
|
173
455
|
useQuery<OperationsCollaboratorDetails>({
|
|
174
|
-
queryKey: [
|
|
456
|
+
queryKey: [
|
|
457
|
+
'operations-collaborator-form',
|
|
458
|
+
currentLocaleCode,
|
|
459
|
+
collaboratorId,
|
|
460
|
+
],
|
|
175
461
|
enabled: Boolean(collaboratorId),
|
|
176
462
|
queryFn: () =>
|
|
177
463
|
fetchOperations<OperationsCollaboratorDetails>(
|
|
@@ -184,45 +470,117 @@ export function CollaboratorFormScreen({
|
|
|
184
470
|
queryKey: ['operations-collaborator-form-supervisors', currentLocaleCode],
|
|
185
471
|
enabled: access.isDirector,
|
|
186
472
|
queryFn: () =>
|
|
187
|
-
fetchOperations<OperationsCollaborator[]>(
|
|
473
|
+
fetchOperations<OperationsCollaborator[]>(
|
|
474
|
+
request,
|
|
475
|
+
'/operations/collaborators'
|
|
476
|
+
),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const { data: departments = [] } = useQuery<OperationsDepartment[]>({
|
|
480
|
+
queryKey: ['operations-collaborator-form-departments', currentLocaleCode],
|
|
481
|
+
enabled: access.isDirector,
|
|
482
|
+
queryFn: () =>
|
|
483
|
+
fetchOperations<OperationsDepartment[]>(
|
|
484
|
+
request,
|
|
485
|
+
'/operations/departments'
|
|
486
|
+
),
|
|
188
487
|
});
|
|
189
488
|
|
|
190
489
|
useEffect(() => {
|
|
191
490
|
if (collaborator) {
|
|
491
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
192
492
|
setForm(toFormState(collaborator));
|
|
193
493
|
}
|
|
194
494
|
}, [collaborator]);
|
|
195
495
|
|
|
496
|
+
const departmentOptions = useMemo(() => {
|
|
497
|
+
const selectedDepartment = trimToNull(form.department);
|
|
498
|
+
const options: Array<{
|
|
499
|
+
id?: number | null;
|
|
500
|
+
name: string;
|
|
501
|
+
code?: string | null;
|
|
502
|
+
description?: string | null;
|
|
503
|
+
}> = departments
|
|
504
|
+
.filter(
|
|
505
|
+
(item) => item.status === 'active' || item.name === selectedDepartment
|
|
506
|
+
)
|
|
507
|
+
.map((item) => ({
|
|
508
|
+
id: item.id,
|
|
509
|
+
name: item.name,
|
|
510
|
+
code: item.code ?? null,
|
|
511
|
+
description: item.description ?? null,
|
|
512
|
+
}));
|
|
513
|
+
|
|
514
|
+
if (selectedDepartment) {
|
|
515
|
+
const alreadyIncluded = options.some(
|
|
516
|
+
(item) => item.name === selectedDepartment
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
if (!alreadyIncluded) {
|
|
520
|
+
options.push({
|
|
521
|
+
id: form.departmentId ? Number(form.departmentId) : undefined,
|
|
522
|
+
name: selectedDepartment,
|
|
523
|
+
code: null,
|
|
524
|
+
description: null,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return options.sort((left, right) => left.name.localeCompare(right.name));
|
|
530
|
+
}, [departments, form.department, form.departmentId]);
|
|
531
|
+
|
|
196
532
|
const updateScheduleDay = (
|
|
197
533
|
weekday: string,
|
|
198
534
|
patch: Partial<CollaboratorFormState['weeklySchedule'][number]>
|
|
199
535
|
) => {
|
|
200
536
|
setForm((current) => ({
|
|
201
537
|
...current,
|
|
202
|
-
weeklySchedule: current.weeklySchedule.map((day) =>
|
|
203
|
-
day.weekday
|
|
204
|
-
|
|
538
|
+
weeklySchedule: current.weeklySchedule.map((day) => {
|
|
539
|
+
if (day.weekday !== weekday) {
|
|
540
|
+
return day;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const nextIsWorkingDay = patch.isWorkingDay ?? day.isWorkingDay;
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
...day,
|
|
547
|
+
...patch,
|
|
548
|
+
startTime:
|
|
549
|
+
patch.startTime !== undefined
|
|
550
|
+
? normalizeTimeValue(patch.startTime)
|
|
551
|
+
: nextIsWorkingDay
|
|
552
|
+
? normalizeTimeValue(day.startTime, '09:00')
|
|
553
|
+
: normalizeTimeValue(day.startTime),
|
|
554
|
+
endTime:
|
|
555
|
+
patch.endTime !== undefined
|
|
556
|
+
? normalizeTimeValue(patch.endTime)
|
|
557
|
+
: nextIsWorkingDay
|
|
558
|
+
? normalizeTimeValue(day.endTime, '18:00')
|
|
559
|
+
: normalizeTimeValue(day.endTime),
|
|
560
|
+
};
|
|
561
|
+
}),
|
|
205
562
|
}));
|
|
206
563
|
};
|
|
207
564
|
|
|
208
565
|
const onSubmit = async () => {
|
|
209
566
|
const userId = parseNumberInput(form.userId);
|
|
210
|
-
|
|
211
|
-
showToastHandler?.('error', t('messages.userRequired'));
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
567
|
+
const personId = parseNumberInput(form.personId);
|
|
214
568
|
|
|
215
|
-
if (!
|
|
216
|
-
showToastHandler?.('error', t('messages.
|
|
569
|
+
if (!personId) {
|
|
570
|
+
showToastHandler?.('error', t('messages.personRequired'));
|
|
217
571
|
return;
|
|
218
572
|
}
|
|
219
573
|
|
|
574
|
+
const departmentId = parseNumberInput(form.departmentId);
|
|
575
|
+
|
|
220
576
|
const payload = {
|
|
221
577
|
userId: userId ?? undefined,
|
|
222
|
-
|
|
223
|
-
|
|
578
|
+
personId: personId ?? undefined,
|
|
579
|
+
code: trimToNull(form.code) ?? undefined,
|
|
580
|
+
displayName: trimToNull(form.displayName),
|
|
224
581
|
collaboratorType: form.collaboratorType,
|
|
225
|
-
|
|
582
|
+
departmentId: departmentId ?? undefined,
|
|
583
|
+
department: departmentId ? undefined : trimToNull(form.department),
|
|
226
584
|
title: trimToNull(form.title),
|
|
227
585
|
levelLabel: trimToNull(form.levelLabel),
|
|
228
586
|
weeklyCapacityHours: parseNumberInput(form.weeklyCapacityHours),
|
|
@@ -240,8 +598,12 @@ export function CollaboratorFormScreen({
|
|
|
240
598
|
weeklySchedule: form.weeklySchedule.map((day) => ({
|
|
241
599
|
weekday: day.weekday,
|
|
242
600
|
isWorkingDay: day.isWorkingDay,
|
|
243
|
-
startTime: day.isWorkingDay
|
|
244
|
-
|
|
601
|
+
startTime: day.isWorkingDay
|
|
602
|
+
? trimToNull(normalizeTimeValue(day.startTime, '09:00'))
|
|
603
|
+
: null,
|
|
604
|
+
endTime: day.isWorkingDay
|
|
605
|
+
? trimToNull(normalizeTimeValue(day.endTime, '18:00'))
|
|
606
|
+
: null,
|
|
245
607
|
breakMinutes: day.isWorkingDay ? parseNumberInput(day.breakMinutes) : 0,
|
|
246
608
|
})),
|
|
247
609
|
};
|
|
@@ -263,8 +625,16 @@ export function CollaboratorFormScreen({
|
|
|
263
625
|
|
|
264
626
|
showToastHandler?.(
|
|
265
627
|
'success',
|
|
266
|
-
collaboratorId
|
|
628
|
+
collaboratorId
|
|
629
|
+
? t('messages.updateSuccess')
|
|
630
|
+
: t('messages.createSuccess')
|
|
267
631
|
);
|
|
632
|
+
|
|
633
|
+
if (onSaved) {
|
|
634
|
+
await onSaved(response);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
268
638
|
router.push(`/operations/collaborators/${response.id}`);
|
|
269
639
|
} catch {
|
|
270
640
|
showToastHandler?.(
|
|
@@ -274,7 +644,99 @@ export function CollaboratorFormScreen({
|
|
|
274
644
|
}
|
|
275
645
|
};
|
|
276
646
|
|
|
647
|
+
const noAccessState = (
|
|
648
|
+
<EmptyState
|
|
649
|
+
icon={<UserRound className="size-12" />}
|
|
650
|
+
title={commonT('states.noAccessTitle')}
|
|
651
|
+
description={t('noAccessDescription')}
|
|
652
|
+
actionLabel={commonT('actions.refresh')}
|
|
653
|
+
onAction={() => router.refresh()}
|
|
654
|
+
/>
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const collaboratorSummary =
|
|
658
|
+
collaborator && isSheetMode ? (
|
|
659
|
+
<div className="mx-4 rounded-xl border bg-muted/30 px-4 py-3">
|
|
660
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
661
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
662
|
+
<Avatar className="h-12 w-12 border border-border/60 bg-background">
|
|
663
|
+
<AvatarImage
|
|
664
|
+
src={getPersonAvatarUrl(collaborator.personAvatarId)}
|
|
665
|
+
alt={collaborator.displayName}
|
|
666
|
+
/>
|
|
667
|
+
<AvatarFallback className="bg-muted text-sm font-semibold text-foreground">
|
|
668
|
+
{getInitials(collaborator.displayName)}
|
|
669
|
+
</AvatarFallback>
|
|
670
|
+
</Avatar>
|
|
671
|
+
<div className="min-w-0">
|
|
672
|
+
<div className="truncate text-base font-semibold">
|
|
673
|
+
{collaborator.displayName}
|
|
674
|
+
</div>
|
|
675
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
676
|
+
{[
|
|
677
|
+
collaborator.code,
|
|
678
|
+
collaborator.department,
|
|
679
|
+
collaborator.title,
|
|
680
|
+
]
|
|
681
|
+
.filter(Boolean)
|
|
682
|
+
.join(' • ') || commonT('labels.notAvailable')}
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
687
|
+
<div className="flex flex-wrap gap-2">
|
|
688
|
+
<StatusBadge
|
|
689
|
+
label={getStatusLabel(collaborator.status)}
|
|
690
|
+
className={getStatusBadgeClass(collaborator.status)}
|
|
691
|
+
/>
|
|
692
|
+
<span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
|
|
693
|
+
{getCollaboratorTypeLabel(collaborator.collaboratorType)}
|
|
694
|
+
</span>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
<div className="mt-3 grid gap-2 text-sm md:grid-cols-2 xl:grid-cols-4">
|
|
699
|
+
<div>
|
|
700
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
701
|
+
{commonT('labels.supervisor')}
|
|
702
|
+
</div>
|
|
703
|
+
<div className="font-medium">
|
|
704
|
+
{collaborator.supervisorName || commonT('labels.notAssigned')}
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
<div>
|
|
708
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
709
|
+
{commonT('labels.weeklyCapacity')}
|
|
710
|
+
</div>
|
|
711
|
+
<div className="font-medium">
|
|
712
|
+
{formatHours(collaborator.weeklyCapacityHours)}
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div>
|
|
716
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
717
|
+
{commonT('labels.startDate')}
|
|
718
|
+
</div>
|
|
719
|
+
<div className="font-medium">
|
|
720
|
+
{formatDate(collaborator.joinedAt)}
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
<div>
|
|
724
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
725
|
+
{commonT('labels.projectCount')}
|
|
726
|
+
</div>
|
|
727
|
+
<div className="font-medium">
|
|
728
|
+
{collaborator.assignedProjects.length}
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
) : null;
|
|
734
|
+
|
|
277
735
|
if (!access.isDirector && !access.isLoading) {
|
|
736
|
+
if (isSheetMode) {
|
|
737
|
+
return <div className="pt-4">{noAccessState}</div>;
|
|
738
|
+
}
|
|
739
|
+
|
|
278
740
|
return (
|
|
279
741
|
<Page>
|
|
280
742
|
<OperationsHeader
|
|
@@ -282,334 +744,411 @@ export function CollaboratorFormScreen({
|
|
|
282
744
|
description={t('description')}
|
|
283
745
|
current={t('breadcrumb')}
|
|
284
746
|
/>
|
|
285
|
-
|
|
286
|
-
icon={<UserRound className="size-12" />}
|
|
287
|
-
title={commonT('states.noAccessTitle')}
|
|
288
|
-
description={t('noAccessDescription')}
|
|
289
|
-
actionLabel={commonT('actions.refresh')}
|
|
290
|
-
onAction={() => router.refresh()}
|
|
291
|
-
/>
|
|
747
|
+
{noAccessState}
|
|
292
748
|
</Page>
|
|
293
749
|
);
|
|
294
750
|
}
|
|
295
751
|
|
|
296
|
-
|
|
297
|
-
<
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
752
|
+
const basicInfoSection = (
|
|
753
|
+
<div className="space-y-2">
|
|
754
|
+
<div className="space-y-0.5">
|
|
755
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
756
|
+
{t('sections.basicInfo')}
|
|
757
|
+
</h3>
|
|
758
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
759
|
+
{t('sections.basicInfoDescription')}
|
|
760
|
+
</p>
|
|
761
|
+
</div>
|
|
762
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
763
|
+
<div className="space-y-2 md:col-span-2 xl:col-span-4">
|
|
764
|
+
<PersonSelectWithCreate
|
|
765
|
+
label={t('fields.person')}
|
|
766
|
+
entityLabel={t('fields.personEntityLabel')}
|
|
767
|
+
value={form.personId ? Number(form.personId) : null}
|
|
768
|
+
initialSelectedLabel={form.displayName}
|
|
769
|
+
selectPlaceholder={t('placeholders.person')}
|
|
770
|
+
onChange={(personId, personName) =>
|
|
771
|
+
setForm((current) => ({
|
|
772
|
+
...current,
|
|
773
|
+
personId: personId ? String(personId) : '',
|
|
774
|
+
displayName: personName,
|
|
775
|
+
}))
|
|
776
|
+
}
|
|
777
|
+
/>
|
|
778
|
+
</div>
|
|
779
|
+
<div className="space-y-2">
|
|
780
|
+
<Label>{t('fields.code')}</Label>
|
|
781
|
+
<Input
|
|
782
|
+
className="h-10"
|
|
783
|
+
value={form.code}
|
|
784
|
+
placeholder="COL-001"
|
|
785
|
+
onChange={(event) =>
|
|
786
|
+
setForm((current) => ({
|
|
787
|
+
...current,
|
|
788
|
+
code: event.target.value,
|
|
789
|
+
}))
|
|
790
|
+
}
|
|
791
|
+
/>
|
|
792
|
+
</div>
|
|
793
|
+
<div className="space-y-2">
|
|
794
|
+
<Label>{t('fields.levelLabel')}</Label>
|
|
795
|
+
<Input
|
|
796
|
+
className="h-10"
|
|
797
|
+
value={form.levelLabel}
|
|
798
|
+
onChange={(event) =>
|
|
799
|
+
setForm((current) => ({
|
|
800
|
+
...current,
|
|
801
|
+
levelLabel: event.target.value,
|
|
802
|
+
}))
|
|
803
|
+
}
|
|
804
|
+
/>
|
|
805
|
+
</div>
|
|
806
|
+
<div className="space-y-2 xl:col-span-1">
|
|
807
|
+
<DepartmentSelectWithCreate
|
|
808
|
+
label={t('fields.department')}
|
|
809
|
+
value={form.department}
|
|
810
|
+
options={departmentOptions}
|
|
811
|
+
selectPlaceholder={t('placeholders.department')}
|
|
812
|
+
createDescription={t('fields.departmentDescription')}
|
|
813
|
+
createPlaceholder={t('placeholders.departmentCreate')}
|
|
814
|
+
onChange={(department) =>
|
|
815
|
+
setForm((current) => ({
|
|
816
|
+
...current,
|
|
817
|
+
departmentId: department.id ? String(department.id) : '',
|
|
818
|
+
department: department.name,
|
|
819
|
+
}))
|
|
820
|
+
}
|
|
821
|
+
/>
|
|
822
|
+
</div>
|
|
823
|
+
<div className="space-y-2 xl:col-span-1">
|
|
824
|
+
<Label>{t('fields.title')}</Label>
|
|
825
|
+
<Input
|
|
826
|
+
className="h-10"
|
|
827
|
+
value={form.title}
|
|
828
|
+
onChange={(event) =>
|
|
829
|
+
setForm((current) => ({
|
|
830
|
+
...current,
|
|
831
|
+
title: event.target.value,
|
|
832
|
+
}))
|
|
833
|
+
}
|
|
834
|
+
/>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
const employmentInfoSection = (
|
|
841
|
+
<div className="space-y-2">
|
|
842
|
+
<div className="space-y-0.5">
|
|
843
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
844
|
+
{t('sections.employmentInfo')}
|
|
845
|
+
</h3>
|
|
846
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
847
|
+
{t('sections.employmentInfoDescription')}
|
|
848
|
+
</p>
|
|
849
|
+
</div>
|
|
850
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
851
|
+
<div className="space-y-2">
|
|
852
|
+
<label className="text-sm font-medium">
|
|
853
|
+
{t('fields.collaboratorType')}
|
|
854
|
+
</label>
|
|
855
|
+
<Select
|
|
856
|
+
value={form.collaboratorType}
|
|
857
|
+
onValueChange={(value) =>
|
|
858
|
+
setForm((current) => ({
|
|
859
|
+
...current,
|
|
860
|
+
collaboratorType: value,
|
|
861
|
+
}))
|
|
862
|
+
}
|
|
863
|
+
>
|
|
864
|
+
<SelectTrigger className="w-full">
|
|
865
|
+
<SelectValue />
|
|
866
|
+
</SelectTrigger>
|
|
867
|
+
<SelectContent>
|
|
868
|
+
<SelectItem value="clt">
|
|
869
|
+
{t('options.collaboratorTypes.clt')}
|
|
870
|
+
</SelectItem>
|
|
871
|
+
<SelectItem value="pj">
|
|
872
|
+
{t('options.collaboratorTypes.pj')}
|
|
873
|
+
</SelectItem>
|
|
874
|
+
<SelectItem value="freelancer">
|
|
875
|
+
{t('options.collaboratorTypes.freelancer')}
|
|
876
|
+
</SelectItem>
|
|
877
|
+
<SelectItem value="intern">
|
|
878
|
+
{t('options.collaboratorTypes.intern')}
|
|
879
|
+
</SelectItem>
|
|
880
|
+
<SelectItem value="other">
|
|
881
|
+
{t('options.collaboratorTypes.other')}
|
|
882
|
+
</SelectItem>
|
|
883
|
+
</SelectContent>
|
|
884
|
+
</Select>
|
|
885
|
+
</div>
|
|
886
|
+
<div className="space-y-2">
|
|
887
|
+
<label className="text-sm font-medium">
|
|
888
|
+
{commonT('labels.status')}
|
|
889
|
+
</label>
|
|
890
|
+
<Select
|
|
891
|
+
value={form.status}
|
|
892
|
+
onValueChange={(value) =>
|
|
893
|
+
setForm((current) => ({ ...current, status: value }))
|
|
894
|
+
}
|
|
895
|
+
>
|
|
896
|
+
<SelectTrigger className="w-full">
|
|
897
|
+
<SelectValue />
|
|
898
|
+
</SelectTrigger>
|
|
899
|
+
<SelectContent>
|
|
900
|
+
<SelectItem value="active">
|
|
901
|
+
{t('options.statuses.active')}
|
|
902
|
+
</SelectItem>
|
|
903
|
+
<SelectItem value="on_leave">
|
|
904
|
+
{t('options.statuses.on_leave')}
|
|
905
|
+
</SelectItem>
|
|
906
|
+
<SelectItem value="inactive">
|
|
907
|
+
{t('options.statuses.inactive')}
|
|
908
|
+
</SelectItem>
|
|
909
|
+
<SelectItem value="draft">
|
|
910
|
+
{t('options.statuses.draft')}
|
|
911
|
+
</SelectItem>
|
|
912
|
+
</SelectContent>
|
|
913
|
+
</Select>
|
|
914
|
+
</div>
|
|
915
|
+
<div className="space-y-2">
|
|
916
|
+
<label className="text-sm font-medium">
|
|
917
|
+
{commonT('labels.startDate')}
|
|
918
|
+
</label>
|
|
919
|
+
<Input
|
|
920
|
+
type="date"
|
|
921
|
+
value={form.joinedAt}
|
|
922
|
+
onChange={(event) =>
|
|
923
|
+
setForm((current) => ({
|
|
924
|
+
...current,
|
|
925
|
+
joinedAt: event.target.value,
|
|
926
|
+
}))
|
|
927
|
+
}
|
|
928
|
+
/>
|
|
929
|
+
</div>
|
|
930
|
+
<div className="space-y-2">
|
|
931
|
+
<label className="text-sm font-medium">
|
|
932
|
+
{commonT('labels.endDate')}
|
|
933
|
+
</label>
|
|
934
|
+
<Input
|
|
935
|
+
type="date"
|
|
936
|
+
value={form.leftAt}
|
|
937
|
+
onChange={(event) =>
|
|
938
|
+
setForm((current) => ({
|
|
939
|
+
...current,
|
|
940
|
+
leftAt: event.target.value,
|
|
941
|
+
}))
|
|
942
|
+
}
|
|
943
|
+
/>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
const supervisorSection = (
|
|
950
|
+
<div className="space-y-2">
|
|
951
|
+
<div className="space-y-0.5">
|
|
952
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
953
|
+
{t('sections.supervisor')}
|
|
954
|
+
</h3>
|
|
955
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
956
|
+
{t('sections.supervisorDescription')}
|
|
957
|
+
</p>
|
|
958
|
+
</div>
|
|
959
|
+
<SupervisorAutocomplete
|
|
960
|
+
label={commonT('labels.supervisor')}
|
|
961
|
+
value={form.supervisorCollaboratorId}
|
|
962
|
+
options={collaborators.filter((item) => item.id !== collaboratorId)}
|
|
963
|
+
placeholder={t('placeholders.supervisor')}
|
|
964
|
+
emptyLabel={commonT('labels.notAssigned')}
|
|
965
|
+
onChange={(value) =>
|
|
966
|
+
setForm((current) => ({
|
|
967
|
+
...current,
|
|
968
|
+
supervisorCollaboratorId: value,
|
|
969
|
+
}))
|
|
321
970
|
}
|
|
322
971
|
/>
|
|
972
|
+
</div>
|
|
973
|
+
);
|
|
323
974
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
value
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
...current,
|
|
386
|
-
levelLabel: event.target.value,
|
|
387
|
-
}))
|
|
388
|
-
}
|
|
389
|
-
/>
|
|
390
|
-
</div>
|
|
391
|
-
<div className="space-y-2 md:col-span-2">
|
|
392
|
-
<label className="text-sm font-medium">{t('fields.notes')}</label>
|
|
393
|
-
<Textarea
|
|
394
|
-
rows={4}
|
|
395
|
-
value={form.notes}
|
|
396
|
-
onChange={(event) =>
|
|
397
|
-
setForm((current) => ({ ...current, notes: event.target.value }))
|
|
398
|
-
}
|
|
399
|
-
/>
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
</SectionCard>
|
|
403
|
-
|
|
404
|
-
<SectionCard
|
|
405
|
-
title={t('sections.employmentInfo')}
|
|
406
|
-
description={t('sections.employmentInfoDescription')}
|
|
407
|
-
>
|
|
408
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
409
|
-
<div className="space-y-2">
|
|
410
|
-
<label className="text-sm font-medium">{t('fields.collaboratorType')}</label>
|
|
411
|
-
<Select
|
|
412
|
-
value={form.collaboratorType}
|
|
413
|
-
onValueChange={(value) =>
|
|
414
|
-
setForm((current) => ({ ...current, collaboratorType: value }))
|
|
415
|
-
}
|
|
416
|
-
>
|
|
417
|
-
<SelectTrigger>
|
|
418
|
-
<SelectValue />
|
|
419
|
-
</SelectTrigger>
|
|
420
|
-
<SelectContent>
|
|
421
|
-
<SelectItem value="clt">CLT</SelectItem>
|
|
422
|
-
<SelectItem value="pj">PJ</SelectItem>
|
|
423
|
-
<SelectItem value="freelancer">Freelancer</SelectItem>
|
|
424
|
-
<SelectItem value="intern">Intern</SelectItem>
|
|
425
|
-
<SelectItem value="other">Other</SelectItem>
|
|
426
|
-
</SelectContent>
|
|
427
|
-
</Select>
|
|
428
|
-
</div>
|
|
429
|
-
<div className="space-y-2">
|
|
430
|
-
<label className="text-sm font-medium">{commonT('labels.status')}</label>
|
|
431
|
-
<Select
|
|
432
|
-
value={form.status}
|
|
433
|
-
onValueChange={(value) =>
|
|
434
|
-
setForm((current) => ({ ...current, status: value }))
|
|
435
|
-
}
|
|
436
|
-
>
|
|
437
|
-
<SelectTrigger>
|
|
438
|
-
<SelectValue />
|
|
439
|
-
</SelectTrigger>
|
|
440
|
-
<SelectContent>
|
|
441
|
-
<SelectItem value="active">Active</SelectItem>
|
|
442
|
-
<SelectItem value="on_leave">On Leave</SelectItem>
|
|
443
|
-
<SelectItem value="inactive">Inactive</SelectItem>
|
|
444
|
-
</SelectContent>
|
|
445
|
-
</Select>
|
|
446
|
-
</div>
|
|
447
|
-
<div className="space-y-2">
|
|
448
|
-
<label className="text-sm font-medium">{commonT('labels.startDate')}</label>
|
|
449
|
-
<Input
|
|
450
|
-
type="date"
|
|
451
|
-
value={form.joinedAt}
|
|
452
|
-
onChange={(event) =>
|
|
453
|
-
setForm((current) => ({ ...current, joinedAt: event.target.value }))
|
|
454
|
-
}
|
|
455
|
-
/>
|
|
456
|
-
</div>
|
|
457
|
-
<div className="space-y-2">
|
|
458
|
-
<label className="text-sm font-medium">{commonT('labels.endDate')}</label>
|
|
459
|
-
<Input
|
|
460
|
-
type="date"
|
|
461
|
-
value={form.leftAt}
|
|
462
|
-
onChange={(event) =>
|
|
463
|
-
setForm((current) => ({ ...current, leftAt: event.target.value }))
|
|
464
|
-
}
|
|
465
|
-
/>
|
|
975
|
+
const contractSection = (
|
|
976
|
+
<div className="space-y-2">
|
|
977
|
+
<div className="space-y-0.5">
|
|
978
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
979
|
+
{t('sections.contract')}
|
|
980
|
+
</h3>
|
|
981
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
982
|
+
{t('sections.contractDescription')}
|
|
983
|
+
</p>
|
|
984
|
+
</div>
|
|
985
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
986
|
+
<div className="space-y-2">
|
|
987
|
+
<label className="text-sm font-medium">
|
|
988
|
+
{t('fields.weeklyCapacityHours')}
|
|
989
|
+
</label>
|
|
990
|
+
<Input
|
|
991
|
+
type="number"
|
|
992
|
+
step="0.5"
|
|
993
|
+
value={form.weeklyCapacityHours}
|
|
994
|
+
onChange={(event) =>
|
|
995
|
+
setForm((current) => ({
|
|
996
|
+
...current,
|
|
997
|
+
weeklyCapacityHours: event.target.value,
|
|
998
|
+
}))
|
|
999
|
+
}
|
|
1000
|
+
/>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div className="space-y-2">
|
|
1003
|
+
<label className="text-sm font-medium">
|
|
1004
|
+
{t('fields.compensationAmount')}
|
|
1005
|
+
</label>
|
|
1006
|
+
<InputMoney
|
|
1007
|
+
step="0.01"
|
|
1008
|
+
value={form.compensationAmount}
|
|
1009
|
+
onChange={(event) =>
|
|
1010
|
+
setForm((current) => ({
|
|
1011
|
+
...current,
|
|
1012
|
+
compensationAmount: event.target.value,
|
|
1013
|
+
}))
|
|
1014
|
+
}
|
|
1015
|
+
/>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div className="space-y-2 md:col-span-2">
|
|
1018
|
+
<label className="text-sm font-medium">
|
|
1019
|
+
{t('fields.contractDescription')}
|
|
1020
|
+
</label>
|
|
1021
|
+
<Textarea
|
|
1022
|
+
rows={4}
|
|
1023
|
+
value={form.contractDescription}
|
|
1024
|
+
onChange={(event) =>
|
|
1025
|
+
setForm((current) => ({
|
|
1026
|
+
...current,
|
|
1027
|
+
contractDescription: event.target.value,
|
|
1028
|
+
}))
|
|
1029
|
+
}
|
|
1030
|
+
/>
|
|
1031
|
+
</div>
|
|
1032
|
+
<div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-2">
|
|
1033
|
+
<div>
|
|
1034
|
+
<div className="font-medium">
|
|
1035
|
+
{t('fields.autoGenerateContractDraft')}
|
|
466
1036
|
</div>
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
<SectionCard
|
|
471
|
-
title={t('sections.supervisor')}
|
|
472
|
-
description={t('sections.supervisorDescription')}
|
|
473
|
-
>
|
|
474
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
475
|
-
<div className="space-y-2">
|
|
476
|
-
<label className="text-sm font-medium">{commonT('labels.supervisor')}</label>
|
|
477
|
-
<Select
|
|
478
|
-
value={form.supervisorCollaboratorId}
|
|
479
|
-
onValueChange={(value) =>
|
|
480
|
-
setForm((current) => ({
|
|
481
|
-
...current,
|
|
482
|
-
supervisorCollaboratorId: value,
|
|
483
|
-
}))
|
|
484
|
-
}
|
|
485
|
-
>
|
|
486
|
-
<SelectTrigger>
|
|
487
|
-
<SelectValue />
|
|
488
|
-
</SelectTrigger>
|
|
489
|
-
<SelectContent>
|
|
490
|
-
<SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
|
|
491
|
-
{collaborators
|
|
492
|
-
.filter((item) => item.id !== collaboratorId)
|
|
493
|
-
.map((item) => (
|
|
494
|
-
<SelectItem key={item.id} value={String(item.id)}>
|
|
495
|
-
{item.displayName}
|
|
496
|
-
</SelectItem>
|
|
497
|
-
))}
|
|
498
|
-
</SelectContent>
|
|
499
|
-
</Select>
|
|
1037
|
+
<div className="text-sm text-muted-foreground">
|
|
1038
|
+
{t('fields.autoGenerateContractDraftDescription')}
|
|
500
1039
|
</div>
|
|
501
1040
|
</div>
|
|
502
|
-
|
|
1041
|
+
<Switch
|
|
1042
|
+
checked={form.autoGenerateContractDraft}
|
|
1043
|
+
onCheckedChange={(checked) =>
|
|
1044
|
+
setForm((current) => ({
|
|
1045
|
+
...current,
|
|
1046
|
+
autoGenerateContractDraft: checked,
|
|
1047
|
+
}))
|
|
1048
|
+
}
|
|
1049
|
+
/>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
);
|
|
503
1054
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
<div className="space-y-2">
|
|
524
|
-
<label className="text-sm font-medium">{t('fields.compensationAmount')}</label>
|
|
525
|
-
<Input
|
|
526
|
-
type="number"
|
|
527
|
-
step="0.01"
|
|
528
|
-
value={form.compensationAmount}
|
|
529
|
-
onChange={(event) =>
|
|
530
|
-
setForm((current) => ({
|
|
531
|
-
...current,
|
|
532
|
-
compensationAmount: event.target.value,
|
|
533
|
-
}))
|
|
534
|
-
}
|
|
535
|
-
/>
|
|
536
|
-
</div>
|
|
537
|
-
<div className="space-y-2 md:col-span-2">
|
|
538
|
-
<label className="text-sm font-medium">{t('fields.contractDescription')}</label>
|
|
539
|
-
<Textarea
|
|
540
|
-
rows={4}
|
|
541
|
-
value={form.contractDescription}
|
|
542
|
-
onChange={(event) =>
|
|
543
|
-
setForm((current) => ({
|
|
544
|
-
...current,
|
|
545
|
-
contractDescription: event.target.value,
|
|
546
|
-
}))
|
|
547
|
-
}
|
|
548
|
-
/>
|
|
549
|
-
</div>
|
|
550
|
-
<div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-2">
|
|
551
|
-
<div>
|
|
552
|
-
<div className="font-medium">{t('fields.autoGenerateContractDraft')}</div>
|
|
553
|
-
<div className="text-sm text-muted-foreground">
|
|
554
|
-
{t('fields.autoGenerateContractDraftDescription')}
|
|
555
|
-
</div>
|
|
1055
|
+
const scheduleSection = (
|
|
1056
|
+
<div className="space-y-2">
|
|
1057
|
+
<div className="space-y-0.5">
|
|
1058
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1059
|
+
{t('sections.schedule')}
|
|
1060
|
+
</h3>
|
|
1061
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1062
|
+
{t('sections.scheduleDescription')}
|
|
1063
|
+
</p>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div className="space-y-1.5">
|
|
1066
|
+
{form.weeklySchedule.map((day) => (
|
|
1067
|
+
<div
|
|
1068
|
+
key={day.weekday}
|
|
1069
|
+
className="grid gap-2 rounded-md border px-3 py-2 md:grid-cols-[minmax(108px,0.9fr)_auto_repeat(3,minmax(88px,1fr))] md:items-center"
|
|
1070
|
+
>
|
|
1071
|
+
<div className="space-y-0.5 md:pr-1">
|
|
1072
|
+
<div className="text-sm font-medium leading-none">
|
|
1073
|
+
{formatEnumLabel(day.weekday)}
|
|
556
1074
|
</div>
|
|
1075
|
+
<div className="text-[10px] leading-none text-muted-foreground">
|
|
1076
|
+
{day.isWorkingDay
|
|
1077
|
+
? commonT('labels.workingDay')
|
|
1078
|
+
: commonT('labels.dayOff')}
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div className="flex items-center justify-between gap-2 rounded-md bg-muted/30 px-2 py-1.5 md:justify-center md:bg-transparent md:p-0">
|
|
1082
|
+
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground md:hidden">
|
|
1083
|
+
{commonT('labels.workingDay')}
|
|
1084
|
+
</span>
|
|
557
1085
|
<Switch
|
|
558
|
-
checked={
|
|
1086
|
+
checked={day.isWorkingDay}
|
|
559
1087
|
onCheckedChange={(checked) =>
|
|
560
|
-
|
|
561
|
-
...current,
|
|
562
|
-
autoGenerateContractDraft: checked,
|
|
563
|
-
}))
|
|
1088
|
+
updateScheduleDay(day.weekday, { isWorkingDay: checked })
|
|
564
1089
|
}
|
|
565
1090
|
/>
|
|
566
1091
|
</div>
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<SectionCard
|
|
572
|
-
title={t('sections.schedule')}
|
|
573
|
-
description={t('sections.scheduleDescription')}
|
|
574
|
-
>
|
|
575
|
-
<div className="space-y-3">
|
|
576
|
-
{form.weeklySchedule.map((day) => (
|
|
577
|
-
<div
|
|
578
|
-
key={day.weekday}
|
|
579
|
-
className="grid gap-3 rounded-lg border p-4 lg:grid-cols-[1fr_auto_1fr_1fr_1fr]"
|
|
580
|
-
>
|
|
581
|
-
<div>
|
|
582
|
-
<div className="font-medium">{formatEnumLabel(day.weekday)}</div>
|
|
583
|
-
<div className="text-xs text-muted-foreground">
|
|
584
|
-
{day.isWorkingDay ? commonT('labels.workingDay') : commonT('labels.dayOff')}
|
|
585
|
-
</div>
|
|
586
|
-
</div>
|
|
587
|
-
<div className="flex items-center gap-2">
|
|
588
|
-
<Switch
|
|
589
|
-
checked={day.isWorkingDay}
|
|
590
|
-
onCheckedChange={(checked) =>
|
|
591
|
-
updateScheduleDay(day.weekday, { isWorkingDay: checked })
|
|
592
|
-
}
|
|
593
|
-
/>
|
|
1092
|
+
<div className="space-y-0.5">
|
|
1093
|
+
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1094
|
+
{t('fields.startTime')}
|
|
594
1095
|
</div>
|
|
595
1096
|
<Input
|
|
1097
|
+
className="h-8"
|
|
596
1098
|
type="time"
|
|
597
|
-
value={
|
|
1099
|
+
value={normalizeTimeValue(
|
|
1100
|
+
day.startTime,
|
|
1101
|
+
day.isWorkingDay ? '09:00' : ''
|
|
1102
|
+
)}
|
|
598
1103
|
disabled={!day.isWorkingDay}
|
|
599
1104
|
onChange={(event) =>
|
|
600
|
-
updateScheduleDay(day.weekday, {
|
|
1105
|
+
updateScheduleDay(day.weekday, {
|
|
1106
|
+
startTime: event.target.value,
|
|
1107
|
+
})
|
|
601
1108
|
}
|
|
602
1109
|
/>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div className="space-y-0.5">
|
|
1112
|
+
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1113
|
+
{t('fields.endTime')}
|
|
1114
|
+
</div>
|
|
603
1115
|
<Input
|
|
1116
|
+
className="h-8"
|
|
604
1117
|
type="time"
|
|
605
|
-
value={
|
|
1118
|
+
value={normalizeTimeValue(
|
|
1119
|
+
day.endTime,
|
|
1120
|
+
day.isWorkingDay ? '18:00' : ''
|
|
1121
|
+
)}
|
|
606
1122
|
disabled={!day.isWorkingDay}
|
|
607
1123
|
onChange={(event) =>
|
|
608
|
-
updateScheduleDay(day.weekday, {
|
|
1124
|
+
updateScheduleDay(day.weekday, {
|
|
1125
|
+
endTime: event.target.value,
|
|
1126
|
+
})
|
|
609
1127
|
}
|
|
610
1128
|
/>
|
|
1129
|
+
</div>
|
|
1130
|
+
<div className="space-y-0.5">
|
|
1131
|
+
<div className="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1132
|
+
<span>{t('fields.breakMinutes')}</span>
|
|
1133
|
+
<Tooltip>
|
|
1134
|
+
<TooltipTrigger asChild>
|
|
1135
|
+
<span className="inline-flex cursor-help">
|
|
1136
|
+
<Info className="h-3.5 w-3.5" />
|
|
1137
|
+
</span>
|
|
1138
|
+
</TooltipTrigger>
|
|
1139
|
+
<TooltipContent>
|
|
1140
|
+
<p>{t('fields.breakMinutesDescription')}</p>
|
|
1141
|
+
</TooltipContent>
|
|
1142
|
+
</Tooltip>
|
|
1143
|
+
</div>
|
|
611
1144
|
<Input
|
|
1145
|
+
className="h-8"
|
|
612
1146
|
type="number"
|
|
1147
|
+
min="0"
|
|
1148
|
+
step="5"
|
|
1149
|
+
placeholder="60"
|
|
1150
|
+
title={t('fields.breakMinutesDescription')}
|
|
1151
|
+
aria-label={t('fields.breakMinutes')}
|
|
613
1152
|
value={day.breakMinutes}
|
|
614
1153
|
disabled={!day.isWorkingDay}
|
|
615
1154
|
onChange={(event) =>
|
|
@@ -619,13 +1158,310 @@ export function CollaboratorFormScreen({
|
|
|
619
1158
|
}
|
|
620
1159
|
/>
|
|
621
1160
|
</div>
|
|
622
|
-
|
|
1161
|
+
</div>
|
|
1162
|
+
))}
|
|
1163
|
+
</div>
|
|
1164
|
+
|
|
1165
|
+
<div className="space-y-2 pt-1">
|
|
1166
|
+
<Label>{t('fields.notes')}</Label>
|
|
1167
|
+
<Textarea
|
|
1168
|
+
rows={4}
|
|
1169
|
+
value={form.notes}
|
|
1170
|
+
onChange={(event) =>
|
|
1171
|
+
setForm((current) => ({
|
|
1172
|
+
...current,
|
|
1173
|
+
notes: event.target.value,
|
|
1174
|
+
}))
|
|
1175
|
+
}
|
|
1176
|
+
/>
|
|
1177
|
+
</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
const activitySection = collaborator ? (
|
|
1182
|
+
<div className="space-y-4">
|
|
1183
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
1184
|
+
<div className="rounded-xl border bg-muted/20 px-4 py-3">
|
|
1185
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1186
|
+
{detailsT('cards.timesheets')}
|
|
1187
|
+
</div>
|
|
1188
|
+
<div className="mt-1 text-xl font-semibold">
|
|
1189
|
+
{collaborator.timesheetSummary.totalTimesheets}
|
|
1190
|
+
</div>
|
|
1191
|
+
<p className="text-xs text-muted-foreground">
|
|
1192
|
+
{detailsT('cards.timesheetsDescription', {
|
|
1193
|
+
pending: collaborator.timesheetSummary.pendingTimesheets,
|
|
1194
|
+
})}
|
|
1195
|
+
</p>
|
|
1196
|
+
</div>
|
|
1197
|
+
|
|
1198
|
+
<div className="rounded-xl border bg-muted/20 px-4 py-3">
|
|
1199
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1200
|
+
{detailsT('cards.loggedHours')}
|
|
1201
|
+
</div>
|
|
1202
|
+
<div className="mt-1 text-xl font-semibold">
|
|
1203
|
+
{formatHours(collaborator.timesheetSummary.totalHours)}
|
|
1204
|
+
</div>
|
|
1205
|
+
<p className="text-xs text-muted-foreground">
|
|
1206
|
+
{detailsT('cards.loggedHoursDescription')}
|
|
1207
|
+
</p>
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<div className="rounded-xl border bg-muted/20 px-4 py-3">
|
|
1211
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1212
|
+
{detailsT('cards.timeOff')}
|
|
1213
|
+
</div>
|
|
1214
|
+
<div className="mt-1 text-xl font-semibold">
|
|
1215
|
+
{collaborator.timeOffSummary.totalRequests}
|
|
1216
|
+
</div>
|
|
1217
|
+
<p className="text-xs text-muted-foreground">
|
|
1218
|
+
{detailsT('cards.timeOffDescription', {
|
|
1219
|
+
pending: collaborator.timeOffSummary.pendingRequests,
|
|
1220
|
+
})}
|
|
1221
|
+
</p>
|
|
1222
|
+
</div>
|
|
1223
|
+
|
|
1224
|
+
<div className="rounded-xl border bg-muted/20 px-4 py-3">
|
|
1225
|
+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1226
|
+
{detailsT('sections.scheduleAdjustments')}
|
|
1227
|
+
</div>
|
|
1228
|
+
<div className="mt-1 text-xl font-semibold">
|
|
1229
|
+
{collaborator.scheduleAdjustmentRequests.length}
|
|
1230
|
+
</div>
|
|
1231
|
+
<p className="text-xs text-muted-foreground">
|
|
1232
|
+
{detailsT('sections.scheduleAdjustmentsDescription')}
|
|
1233
|
+
</p>
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
|
|
1237
|
+
<div className="grid gap-4 xl:grid-cols-2">
|
|
1238
|
+
<div className="rounded-xl border px-4 py-3">
|
|
1239
|
+
<div className="mb-3 space-y-0.5">
|
|
1240
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1241
|
+
{detailsT('sections.projects')}
|
|
1242
|
+
</h3>
|
|
1243
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1244
|
+
{detailsT('sections.projectsDescription')}
|
|
1245
|
+
</p>
|
|
1246
|
+
</div>
|
|
1247
|
+
|
|
1248
|
+
{collaborator.assignedProjects.length > 0 ? (
|
|
1249
|
+
<div className="space-y-2">
|
|
1250
|
+
{collaborator.assignedProjects.slice(0, 6).map((project) => (
|
|
1251
|
+
<div
|
|
1252
|
+
key={project.id}
|
|
1253
|
+
className="flex flex-col gap-2 rounded-lg border px-3 py-2.5"
|
|
1254
|
+
>
|
|
1255
|
+
<div className="flex items-start justify-between gap-3">
|
|
1256
|
+
<div className="min-w-0">
|
|
1257
|
+
<div className="truncate font-medium">{project.name}</div>
|
|
1258
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1259
|
+
{[
|
|
1260
|
+
project.code,
|
|
1261
|
+
project.roleLabel || commonT('labels.notAssigned'),
|
|
1262
|
+
project.weeklyHours !== null &&
|
|
1263
|
+
project.weeklyHours !== undefined
|
|
1264
|
+
? formatHours(project.weeklyHours)
|
|
1265
|
+
: null,
|
|
1266
|
+
project.allocationPercent !== null &&
|
|
1267
|
+
project.allocationPercent !== undefined
|
|
1268
|
+
? `${project.allocationPercent}%`
|
|
1269
|
+
: null,
|
|
1270
|
+
]
|
|
1271
|
+
.filter(Boolean)
|
|
1272
|
+
.join(' • ')}
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
<StatusBadge
|
|
1276
|
+
label={formatEnumLabel(project.status)}
|
|
1277
|
+
className={getStatusBadgeClass(project.status)}
|
|
1278
|
+
/>
|
|
1279
|
+
</div>
|
|
1280
|
+
<div className="text-xs text-muted-foreground">
|
|
1281
|
+
{formatDateRange(project.startDate, project.endDate)}
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
))}
|
|
1285
|
+
</div>
|
|
1286
|
+
) : (
|
|
1287
|
+
<p className="text-sm text-muted-foreground">
|
|
1288
|
+
{detailsT('noProjects')}
|
|
1289
|
+
</p>
|
|
1290
|
+
)}
|
|
1291
|
+
</div>
|
|
1292
|
+
|
|
1293
|
+
<div className="space-y-4">
|
|
1294
|
+
<div className="rounded-xl border px-4 py-3">
|
|
1295
|
+
<div className="mb-3 space-y-0.5">
|
|
1296
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1297
|
+
{detailsT('sections.primaryContract')}
|
|
1298
|
+
</h3>
|
|
1299
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1300
|
+
{t('sections.contractDescription')}
|
|
1301
|
+
</p>
|
|
1302
|
+
</div>
|
|
1303
|
+
|
|
1304
|
+
{collaborator.relatedContracts.length > 0 ? (
|
|
1305
|
+
<div className="space-y-2">
|
|
1306
|
+
{collaborator.relatedContracts.slice(0, 3).map((contract) => (
|
|
1307
|
+
<Link
|
|
1308
|
+
key={contract.id}
|
|
1309
|
+
href={`/operations/contracts?edit=${contract.id}`}
|
|
1310
|
+
className="flex cursor-pointer items-center justify-between gap-3 rounded-lg border px-3 py-2.5 transition-colors hover:bg-muted/20"
|
|
1311
|
+
>
|
|
1312
|
+
<div className="min-w-0">
|
|
1313
|
+
<div className="truncate font-medium">
|
|
1314
|
+
{contract.name || contract.code}
|
|
1315
|
+
</div>
|
|
1316
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1317
|
+
{[
|
|
1318
|
+
contract.code,
|
|
1319
|
+
formatEnumLabel(contract.contractCategory),
|
|
1320
|
+
]
|
|
1321
|
+
.filter(Boolean)
|
|
1322
|
+
.join(' • ')}
|
|
1323
|
+
</div>
|
|
1324
|
+
</div>
|
|
1325
|
+
<StatusBadge
|
|
1326
|
+
label={formatEnumLabel(contract.status)}
|
|
1327
|
+
className={getStatusBadgeClass(contract.status)}
|
|
1328
|
+
/>
|
|
1329
|
+
</Link>
|
|
1330
|
+
))}
|
|
1331
|
+
</div>
|
|
1332
|
+
) : (
|
|
1333
|
+
<p className="text-sm text-muted-foreground">
|
|
1334
|
+
{detailsT('noContracts')}
|
|
1335
|
+
</p>
|
|
1336
|
+
)}
|
|
1337
|
+
</div>
|
|
1338
|
+
|
|
1339
|
+
<div className="rounded-xl border px-4 py-3">
|
|
1340
|
+
<div className="mb-3 space-y-0.5">
|
|
1341
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1342
|
+
{detailsT('sections.scheduleAdjustments')}
|
|
1343
|
+
</h3>
|
|
1344
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1345
|
+
{detailsT('sections.scheduleAdjustmentsDescription')}
|
|
1346
|
+
</p>
|
|
1347
|
+
</div>
|
|
1348
|
+
|
|
1349
|
+
{collaborator.scheduleAdjustmentRequests.length > 0 ? (
|
|
1350
|
+
<div className="space-y-2">
|
|
1351
|
+
{collaborator.scheduleAdjustmentRequests
|
|
1352
|
+
.slice(0, 4)
|
|
1353
|
+
.map((request) => (
|
|
1354
|
+
<div
|
|
1355
|
+
key={request.id}
|
|
1356
|
+
className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2.5"
|
|
1357
|
+
>
|
|
1358
|
+
<div className="min-w-0">
|
|
1359
|
+
<div className="font-medium">
|
|
1360
|
+
{formatEnumLabel(request.requestScope)}
|
|
1361
|
+
</div>
|
|
1362
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1363
|
+
{formatDateRange(
|
|
1364
|
+
request.effectiveStartDate,
|
|
1365
|
+
request.effectiveEndDate
|
|
1366
|
+
)}
|
|
1367
|
+
</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
<StatusBadge
|
|
1370
|
+
label={formatEnumLabel(request.status)}
|
|
1371
|
+
className={getStatusBadgeClass(request.status)}
|
|
1372
|
+
/>
|
|
1373
|
+
</div>
|
|
1374
|
+
))}
|
|
1375
|
+
</div>
|
|
1376
|
+
) : (
|
|
1377
|
+
<p className="text-sm text-muted-foreground">
|
|
1378
|
+
{detailsT('noScheduleAdjustments')}
|
|
1379
|
+
</p>
|
|
1380
|
+
)}
|
|
1381
|
+
</div>
|
|
623
1382
|
</div>
|
|
624
|
-
</
|
|
1383
|
+
</div>
|
|
1384
|
+
</div>
|
|
1385
|
+
) : null;
|
|
1386
|
+
|
|
1387
|
+
const profileContent = (
|
|
1388
|
+
<div className="space-y-4">
|
|
1389
|
+
{basicInfoSection}
|
|
1390
|
+
{employmentInfoSection}
|
|
1391
|
+
{supervisorSection}
|
|
1392
|
+
</div>
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
const formContent = isSheetMode ? (
|
|
1396
|
+
<div className="space-y-4 px-4">
|
|
1397
|
+
{profileContent}
|
|
1398
|
+
{contractSection}
|
|
1399
|
+
{scheduleSection}
|
|
1400
|
+
{activitySection}
|
|
1401
|
+
</div>
|
|
1402
|
+
) : (
|
|
1403
|
+
<div className="space-y-4 px-4">
|
|
1404
|
+
{profileContent}
|
|
1405
|
+
{contractSection}
|
|
1406
|
+
{scheduleSection}
|
|
1407
|
+
</div>
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
const loadingState =
|
|
1411
|
+
collaboratorId && isLoadingCollaborator ? (
|
|
1412
|
+
<div className="text-sm text-muted-foreground">{t('loading')}</div>
|
|
1413
|
+
) : null;
|
|
1414
|
+
|
|
1415
|
+
if (isSheetMode) {
|
|
1416
|
+
return (
|
|
1417
|
+
<div className="mt-6 space-y-4 pb-6">
|
|
1418
|
+
{collaboratorSummary}
|
|
1419
|
+
{formContent}
|
|
1420
|
+
{loadingState}
|
|
1421
|
+
|
|
1422
|
+
<FormActions
|
|
1423
|
+
sheet
|
|
1424
|
+
cancelLabel={commonT('actions.cancel')}
|
|
1425
|
+
onCancel={onCancel}
|
|
1426
|
+
onSubmit={() => void onSubmit()}
|
|
1427
|
+
submitIcon={<Save className="size-4" />}
|
|
1428
|
+
submitLabel={commonT('actions.save')}
|
|
1429
|
+
submitSize="lg"
|
|
1430
|
+
/>
|
|
1431
|
+
</div>
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return (
|
|
1436
|
+
<Page>
|
|
1437
|
+
<OperationsHeader
|
|
1438
|
+
title={t(collaboratorId ? 'editTitle' : 'newTitle')}
|
|
1439
|
+
description={t('description')}
|
|
1440
|
+
current={t('breadcrumb')}
|
|
1441
|
+
actions={
|
|
1442
|
+
<div className="flex gap-2">
|
|
1443
|
+
<Button variant="outline" size="sm" asChild>
|
|
1444
|
+
<Link
|
|
1445
|
+
href={
|
|
1446
|
+
collaboratorId
|
|
1447
|
+
? `/operations/collaborators/${collaboratorId}`
|
|
1448
|
+
: '/operations/collaborators'
|
|
1449
|
+
}
|
|
1450
|
+
>
|
|
1451
|
+
<ArrowLeft className="size-4" />
|
|
1452
|
+
{commonT('actions.back')}
|
|
1453
|
+
</Link>
|
|
1454
|
+
</Button>
|
|
1455
|
+
<Button size="sm" onClick={() => void onSubmit()}>
|
|
1456
|
+
<Save className="size-4" />
|
|
1457
|
+
{commonT('actions.save')}
|
|
1458
|
+
</Button>
|
|
1459
|
+
</div>
|
|
1460
|
+
}
|
|
1461
|
+
/>
|
|
625
1462
|
|
|
626
|
-
{
|
|
627
|
-
|
|
628
|
-
) : null}
|
|
1463
|
+
{formContent}
|
|
1464
|
+
{loadingState}
|
|
629
1465
|
</Page>
|
|
630
1466
|
);
|
|
631
1467
|
}
|