@hed-hog/operations 0.0.297 → 0.0.298

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -310
  2. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -631
  3. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -132
  4. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -558
  5. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -291
  6. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -689
  7. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -32
  8. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -44
  9. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -360
  10. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -129
  11. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -14
  12. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -386
  13. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -11
  14. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -11
  15. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -5
  16. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -261
  17. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -11
  18. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -11
  19. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -17
  20. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -262
  21. package/hedhog/frontend/app/page.tsx.ejs +319 -319
  22. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -11
  23. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -11
  24. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -5
  25. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -236
  26. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -418
  27. package/hedhog/frontend/app/team/page.tsx.ejs +339 -339
  28. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -328
  29. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -636
  30. package/hedhog/frontend/messages/en.json +648 -648
  31. package/hedhog/frontend/messages/pt.json +647 -647
  32. package/package.json +4 -4
@@ -1,418 +1,418 @@
1
- 'use client';
2
-
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import { Input } from '@/components/ui/input';
6
- import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
7
- import {
8
- Select,
9
- SelectContent,
10
- SelectItem,
11
- SelectTrigger,
12
- SelectValue,
13
- } from '@/components/ui/select';
14
- import { Switch } from '@/components/ui/switch';
15
- import {
16
- Sheet,
17
- SheetContent,
18
- SheetDescription,
19
- SheetHeader,
20
- SheetTitle,
21
- } from '@/components/ui/sheet';
22
- import {
23
- Table,
24
- TableBody,
25
- TableCell,
26
- TableHead,
27
- TableHeader,
28
- TableRow,
29
- } from '@/components/ui/table';
30
- import { Textarea } from '@/components/ui/textarea';
31
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
32
- import { CalendarRange, Plus } from 'lucide-react';
33
- import { useMemo, useState } from 'react';
34
- import { useTranslations } from 'next-intl';
35
- import { OperationsHeader } from '../_components/operations-header';
36
- import { StatusBadge } from '../_components/status-badge';
37
- import { fetchOperations, mutateOperations } from '../_lib/api';
38
- import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
39
- import type {
40
- OperationsScheduleAdjustmentDay,
41
- OperationsScheduleAdjustmentRequest,
42
- } from '../_lib/types';
43
- import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
44
- import {
45
- formatDateRange,
46
- formatEnumLabel,
47
- getStatusBadgeClass,
48
- summarizeScheduleDays,
49
- } from '../_lib/utils/format';
50
-
51
- const weekdays = [
52
- 'monday',
53
- 'tuesday',
54
- 'wednesday',
55
- 'thursday',
56
- 'friday',
57
- 'saturday',
58
- 'sunday',
59
- ] as const;
60
-
61
- type ScheduleFormState = {
62
- requestScope: string;
63
- effectiveStartDate: string;
64
- effectiveEndDate: string;
65
- reason: string;
66
- days: Array<{
67
- weekday: string;
68
- isWorkingDay: boolean;
69
- startTime: string;
70
- endTime: string;
71
- breakMinutes: string;
72
- }>;
73
- };
74
-
75
- const emptyForm: ScheduleFormState = {
76
- requestScope: 'temporary',
77
- effectiveStartDate: '',
78
- effectiveEndDate: '',
79
- reason: '',
80
- days: weekdays.map((weekday) => ({
81
- weekday,
82
- isWorkingDay: !['saturday', 'sunday'].includes(weekday),
83
- startTime: '09:00',
84
- endTime: '18:00',
85
- breakMinutes: '60',
86
- })),
87
- };
88
-
89
- export default function OperationsScheduleAdjustmentsPage() {
90
- const t = useTranslations('operations.ScheduleAdjustmentsPage');
91
- const commonT = useTranslations('operations.Common');
92
- const { request, showToastHandler, currentLocaleCode } = useApp();
93
- const access = useOperationsAccess();
94
- const [search, setSearch] = useState('');
95
- const [statusFilter, setStatusFilter] = useState('all');
96
- const [isSheetOpen, setIsSheetOpen] = useState(false);
97
- const [form, setForm] = useState<ScheduleFormState>(emptyForm);
98
-
99
- const { data: requests = [], refetch } = useQuery<
100
- OperationsScheduleAdjustmentRequest[]
101
- >({
102
- queryKey: ['operations-schedule-adjustments', currentLocaleCode],
103
- enabled: access.isCollaborator,
104
- queryFn: () =>
105
- fetchOperations<OperationsScheduleAdjustmentRequest[]>(
106
- request,
107
- '/operations/schedule-adjustments'
108
- ),
109
- });
110
-
111
- const filteredRows = useMemo(
112
- () =>
113
- requests.filter((item) => {
114
- const matchesSearch = !search.trim()
115
- ? true
116
- : [
117
- item.collaboratorName,
118
- item.approverName,
119
- item.reason,
120
- item.requestScope,
121
- ]
122
- .filter(Boolean)
123
- .some((value) =>
124
- String(value).toLowerCase().includes(search.trim().toLowerCase())
125
- );
126
- const matchesStatus =
127
- statusFilter === 'all' ? true : item.status === statusFilter;
128
- return matchesSearch && matchesStatus;
129
- }),
130
- [requests, search, statusFilter]
131
- );
132
-
133
- const cards = [
134
- {
135
- key: 'submitted',
136
- title: t('cards.submitted'),
137
- value: requests.filter((item) => item.status === 'submitted').length,
138
- },
139
- {
140
- key: 'approved',
141
- title: t('cards.approved'),
142
- value: requests.filter((item) => item.status === 'approved').length,
143
- },
144
- {
145
- key: 'permanent',
146
- title: t('cards.permanent'),
147
- value: requests.filter((item) => item.requestScope === 'permanent').length,
148
- },
149
- ];
150
-
151
- const updateDay = (
152
- weekday: string,
153
- patch: Partial<ScheduleFormState['days'][number]>
154
- ) => {
155
- setForm((current) => ({
156
- ...current,
157
- days: current.days.map((day) =>
158
- day.weekday === weekday ? { ...day, ...patch } : day
159
- ),
160
- }));
161
- };
162
-
163
- const onSubmit = async () => {
164
- if (!form.effectiveStartDate) {
165
- showToastHandler?.('error', t('messages.requiredFields'));
166
- return;
167
- }
168
-
169
- try {
170
- await mutateOperations(request, '/operations/schedule-adjustments', 'POST', {
171
- requestScope: form.requestScope,
172
- effectiveStartDate: form.effectiveStartDate,
173
- effectiveEndDate: trimToNull(form.effectiveEndDate),
174
- reason: trimToNull(form.reason),
175
- days: form.days.map((day) => ({
176
- weekday: day.weekday,
177
- isWorkingDay: day.isWorkingDay,
178
- startTime: day.isWorkingDay ? trimToNull(day.startTime) : null,
179
- endTime: day.isWorkingDay ? trimToNull(day.endTime) : null,
180
- breakMinutes: day.isWorkingDay ? parseNumberInput(day.breakMinutes) : 0,
181
- })),
182
- });
183
-
184
- showToastHandler?.('success', t('messages.saveSuccess'));
185
- setIsSheetOpen(false);
186
- setForm(emptyForm);
187
- await refetch();
188
- } catch {
189
- showToastHandler?.('error', t('messages.saveError'));
190
- }
191
- };
192
-
193
- return (
194
- <Page>
195
- <OperationsHeader
196
- title={t('title')}
197
- description={t('description')}
198
- current={t('breadcrumb')}
199
- actions={
200
- access.isCollaborator ? (
201
- <Button size="sm" onClick={() => setIsSheetOpen(true)}>
202
- <Plus className="size-4" />
203
- {commonT('actions.create')}
204
- </Button>
205
- ) : undefined
206
- }
207
- />
208
-
209
- <SearchBar
210
- searchQuery={search}
211
- onSearchChange={setSearch}
212
- onSearch={() => undefined}
213
- placeholder={t('searchPlaceholder')}
214
- controls={[
215
- {
216
- id: 'status',
217
- type: 'select',
218
- value: statusFilter,
219
- onChange: setStatusFilter,
220
- placeholder: commonT('labels.status'),
221
- options: [
222
- { value: 'all', label: commonT('filters.allStatuses') },
223
- { value: 'submitted', label: formatEnumLabel('submitted') },
224
- { value: 'approved', label: formatEnumLabel('approved') },
225
- { value: 'rejected', label: formatEnumLabel('rejected') },
226
- ],
227
- },
228
- ]}
229
- />
230
-
231
- <KpiCardsGrid items={cards} columns={3} />
232
-
233
- {filteredRows.length > 0 ? (
234
- <div className="overflow-x-auto rounded-md border">
235
- <Table>
236
- <TableHeader>
237
- <TableRow>
238
- <TableHead>{commonT('labels.collaborator')}</TableHead>
239
- <TableHead>{commonT('labels.requestScope')}</TableHead>
240
- <TableHead>{commonT('labels.timeline')}</TableHead>
241
- <TableHead>{t('table.currentSchedule')}</TableHead>
242
- <TableHead>{commonT('labels.schedule')}</TableHead>
243
- <TableHead>{commonT('labels.approver')}</TableHead>
244
- <TableHead>{commonT('labels.status')}</TableHead>
245
- <TableHead>{commonT('labels.notes')}</TableHead>
246
- </TableRow>
247
- </TableHeader>
248
- <TableBody>
249
- {filteredRows.map((requestItem) => (
250
- <TableRow key={requestItem.id}>
251
- <TableCell>{requestItem.collaboratorName}</TableCell>
252
- <TableCell>{formatEnumLabel(requestItem.requestScope)}</TableCell>
253
- <TableCell>
254
- {formatDateRange(
255
- requestItem.effectiveStartDate,
256
- requestItem.effectiveEndDate
257
- )}
258
- </TableCell>
259
- <TableCell>
260
- {summarizeScheduleDays(requestItem.currentSchedule ?? [])}
261
- </TableCell>
262
- <TableCell>
263
- {summarizeScheduleDays(
264
- (requestItem.days ?? []) as OperationsScheduleAdjustmentDay[]
265
- )}
266
- </TableCell>
267
- <TableCell>
268
- {requestItem.approverName || commonT('labels.notAssigned')}
269
- </TableCell>
270
- <TableCell>
271
- <StatusBadge
272
- label={formatEnumLabel(requestItem.status)}
273
- className={getStatusBadgeClass(requestItem.status)}
274
- />
275
- </TableCell>
276
- <TableCell>
277
- {requestItem.approverNote || commonT('labels.noNotes')}
278
- </TableCell>
279
- </TableRow>
280
- ))}
281
- </TableBody>
282
- </Table>
283
- </div>
284
- ) : (
285
- <EmptyState
286
- icon={<CalendarRange className="size-12" />}
287
- title={commonT('states.emptyTitle')}
288
- description={t('emptyDescription')}
289
- actionLabel={access.isCollaborator ? commonT('actions.create') : commonT('actions.refresh')}
290
- onAction={access.isCollaborator ? () => setIsSheetOpen(true) : () => void refetch()}
291
- />
292
- )}
293
-
294
- <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
295
- <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
296
- <SheetHeader>
297
- <SheetTitle>{t('sheet.title')}</SheetTitle>
298
- <SheetDescription>{t('sheet.description')}</SheetDescription>
299
- </SheetHeader>
300
-
301
- <div className="mt-6 grid gap-4">
302
- <div className="grid gap-4 md:grid-cols-3">
303
- <div className="space-y-2">
304
- <label className="text-sm font-medium">{commonT('labels.requestScope')}</label>
305
- <Select
306
- value={form.requestScope}
307
- onValueChange={(value) =>
308
- setForm((current) => ({ ...current, requestScope: value }))
309
- }
310
- >
311
- <SelectTrigger>
312
- <SelectValue />
313
- </SelectTrigger>
314
- <SelectContent>
315
- <SelectItem value="temporary">Temporary</SelectItem>
316
- <SelectItem value="permanent">Permanent</SelectItem>
317
- </SelectContent>
318
- </Select>
319
- </div>
320
- <div className="space-y-2">
321
- <label className="text-sm font-medium">{commonT('labels.startDate')}</label>
322
- <Input
323
- type="date"
324
- value={form.effectiveStartDate}
325
- onChange={(event) =>
326
- setForm((current) => ({
327
- ...current,
328
- effectiveStartDate: event.target.value,
329
- }))
330
- }
331
- />
332
- </div>
333
- <div className="space-y-2">
334
- <label className="text-sm font-medium">{commonT('labels.endDate')}</label>
335
- <Input
336
- type="date"
337
- value={form.effectiveEndDate}
338
- onChange={(event) =>
339
- setForm((current) => ({
340
- ...current,
341
- effectiveEndDate: event.target.value,
342
- }))
343
- }
344
- />
345
- </div>
346
- </div>
347
-
348
- <div className="space-y-2">
349
- <label className="text-sm font-medium">{commonT('labels.reason')}</label>
350
- <Textarea
351
- rows={3}
352
- value={form.reason}
353
- onChange={(event) =>
354
- setForm((current) => ({ ...current, reason: event.target.value }))
355
- }
356
- />
357
- </div>
358
-
359
- <div className="space-y-3">
360
- <div className="text-sm font-medium">{commonT('labels.weeklySchedule')}</div>
361
- <div className="space-y-3">
362
- {form.days.map((day) => (
363
- <div
364
- key={day.weekday}
365
- className="grid gap-3 rounded-lg border p-4 lg:grid-cols-[1fr_auto_1fr_1fr_1fr]"
366
- >
367
- <div>
368
- <div className="font-medium">{formatEnumLabel(day.weekday)}</div>
369
- <div className="text-xs text-muted-foreground">
370
- {day.isWorkingDay
371
- ? commonT('labels.workingDay')
372
- : commonT('labels.dayOff')}
373
- </div>
374
- </div>
375
- <div className="flex items-center gap-2">
376
- <Switch
377
- checked={day.isWorkingDay}
378
- onCheckedChange={(checked) =>
379
- updateDay(day.weekday, { isWorkingDay: checked })
380
- }
381
- />
382
- </div>
383
- <Input
384
- type="time"
385
- value={day.startTime}
386
- disabled={!day.isWorkingDay}
387
- onChange={(event) =>
388
- updateDay(day.weekday, { startTime: event.target.value })
389
- }
390
- />
391
- <Input
392
- type="time"
393
- value={day.endTime}
394
- disabled={!day.isWorkingDay}
395
- onChange={(event) =>
396
- updateDay(day.weekday, { endTime: event.target.value })
397
- }
398
- />
399
- <Input
400
- type="number"
401
- value={day.breakMinutes}
402
- disabled={!day.isWorkingDay}
403
- onChange={(event) =>
404
- updateDay(day.weekday, { breakMinutes: event.target.value })
405
- }
406
- />
407
- </div>
408
- ))}
409
- </div>
410
- </div>
411
-
412
- <Button onClick={() => void onSubmit()}>{commonT('actions.save')}</Button>
413
- </div>
414
- </SheetContent>
415
- </Sheet>
416
- </Page>
417
- );
418
- }
1
+ 'use client';
2
+
3
+ import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Input } from '@/components/ui/input';
6
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@/components/ui/select';
14
+ import { Switch } from '@/components/ui/switch';
15
+ import {
16
+ Sheet,
17
+ SheetContent,
18
+ SheetDescription,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ } from '@/components/ui/sheet';
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableHead,
27
+ TableHeader,
28
+ TableRow,
29
+ } from '@/components/ui/table';
30
+ import { Textarea } from '@/components/ui/textarea';
31
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
32
+ import { CalendarRange, Plus } from 'lucide-react';
33
+ import { useMemo, useState } from 'react';
34
+ import { useTranslations } from 'next-intl';
35
+ import { OperationsHeader } from '../_components/operations-header';
36
+ import { StatusBadge } from '../_components/status-badge';
37
+ import { fetchOperations, mutateOperations } from '../_lib/api';
38
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
39
+ import type {
40
+ OperationsScheduleAdjustmentDay,
41
+ OperationsScheduleAdjustmentRequest,
42
+ } from '../_lib/types';
43
+ import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
44
+ import {
45
+ formatDateRange,
46
+ formatEnumLabel,
47
+ getStatusBadgeClass,
48
+ summarizeScheduleDays,
49
+ } from '../_lib/utils/format';
50
+
51
+ const weekdays = [
52
+ 'monday',
53
+ 'tuesday',
54
+ 'wednesday',
55
+ 'thursday',
56
+ 'friday',
57
+ 'saturday',
58
+ 'sunday',
59
+ ] as const;
60
+
61
+ type ScheduleFormState = {
62
+ requestScope: string;
63
+ effectiveStartDate: string;
64
+ effectiveEndDate: string;
65
+ reason: string;
66
+ days: Array<{
67
+ weekday: string;
68
+ isWorkingDay: boolean;
69
+ startTime: string;
70
+ endTime: string;
71
+ breakMinutes: string;
72
+ }>;
73
+ };
74
+
75
+ const emptyForm: ScheduleFormState = {
76
+ requestScope: 'temporary',
77
+ effectiveStartDate: '',
78
+ effectiveEndDate: '',
79
+ reason: '',
80
+ days: weekdays.map((weekday) => ({
81
+ weekday,
82
+ isWorkingDay: !['saturday', 'sunday'].includes(weekday),
83
+ startTime: '09:00',
84
+ endTime: '18:00',
85
+ breakMinutes: '60',
86
+ })),
87
+ };
88
+
89
+ export default function OperationsScheduleAdjustmentsPage() {
90
+ const t = useTranslations('operations.ScheduleAdjustmentsPage');
91
+ const commonT = useTranslations('operations.Common');
92
+ const { request, showToastHandler, currentLocaleCode } = useApp();
93
+ const access = useOperationsAccess();
94
+ const [search, setSearch] = useState('');
95
+ const [statusFilter, setStatusFilter] = useState('all');
96
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
97
+ const [form, setForm] = useState<ScheduleFormState>(emptyForm);
98
+
99
+ const { data: requests = [], refetch } = useQuery<
100
+ OperationsScheduleAdjustmentRequest[]
101
+ >({
102
+ queryKey: ['operations-schedule-adjustments', currentLocaleCode],
103
+ enabled: access.isCollaborator,
104
+ queryFn: () =>
105
+ fetchOperations<OperationsScheduleAdjustmentRequest[]>(
106
+ request,
107
+ '/operations/schedule-adjustments'
108
+ ),
109
+ });
110
+
111
+ const filteredRows = useMemo(
112
+ () =>
113
+ requests.filter((item) => {
114
+ const matchesSearch = !search.trim()
115
+ ? true
116
+ : [
117
+ item.collaboratorName,
118
+ item.approverName,
119
+ item.reason,
120
+ item.requestScope,
121
+ ]
122
+ .filter(Boolean)
123
+ .some((value) =>
124
+ String(value).toLowerCase().includes(search.trim().toLowerCase())
125
+ );
126
+ const matchesStatus =
127
+ statusFilter === 'all' ? true : item.status === statusFilter;
128
+ return matchesSearch && matchesStatus;
129
+ }),
130
+ [requests, search, statusFilter]
131
+ );
132
+
133
+ const cards = [
134
+ {
135
+ key: 'submitted',
136
+ title: t('cards.submitted'),
137
+ value: requests.filter((item) => item.status === 'submitted').length,
138
+ },
139
+ {
140
+ key: 'approved',
141
+ title: t('cards.approved'),
142
+ value: requests.filter((item) => item.status === 'approved').length,
143
+ },
144
+ {
145
+ key: 'permanent',
146
+ title: t('cards.permanent'),
147
+ value: requests.filter((item) => item.requestScope === 'permanent').length,
148
+ },
149
+ ];
150
+
151
+ const updateDay = (
152
+ weekday: string,
153
+ patch: Partial<ScheduleFormState['days'][number]>
154
+ ) => {
155
+ setForm((current) => ({
156
+ ...current,
157
+ days: current.days.map((day) =>
158
+ day.weekday === weekday ? { ...day, ...patch } : day
159
+ ),
160
+ }));
161
+ };
162
+
163
+ const onSubmit = async () => {
164
+ if (!form.effectiveStartDate) {
165
+ showToastHandler?.('error', t('messages.requiredFields'));
166
+ return;
167
+ }
168
+
169
+ try {
170
+ await mutateOperations(request, '/operations/schedule-adjustments', 'POST', {
171
+ requestScope: form.requestScope,
172
+ effectiveStartDate: form.effectiveStartDate,
173
+ effectiveEndDate: trimToNull(form.effectiveEndDate),
174
+ reason: trimToNull(form.reason),
175
+ days: form.days.map((day) => ({
176
+ weekday: day.weekday,
177
+ isWorkingDay: day.isWorkingDay,
178
+ startTime: day.isWorkingDay ? trimToNull(day.startTime) : null,
179
+ endTime: day.isWorkingDay ? trimToNull(day.endTime) : null,
180
+ breakMinutes: day.isWorkingDay ? parseNumberInput(day.breakMinutes) : 0,
181
+ })),
182
+ });
183
+
184
+ showToastHandler?.('success', t('messages.saveSuccess'));
185
+ setIsSheetOpen(false);
186
+ setForm(emptyForm);
187
+ await refetch();
188
+ } catch {
189
+ showToastHandler?.('error', t('messages.saveError'));
190
+ }
191
+ };
192
+
193
+ return (
194
+ <Page>
195
+ <OperationsHeader
196
+ title={t('title')}
197
+ description={t('description')}
198
+ current={t('breadcrumb')}
199
+ actions={
200
+ access.isCollaborator ? (
201
+ <Button size="sm" onClick={() => setIsSheetOpen(true)}>
202
+ <Plus className="size-4" />
203
+ {commonT('actions.create')}
204
+ </Button>
205
+ ) : undefined
206
+ }
207
+ />
208
+
209
+ <SearchBar
210
+ searchQuery={search}
211
+ onSearchChange={setSearch}
212
+ onSearch={() => undefined}
213
+ placeholder={t('searchPlaceholder')}
214
+ controls={[
215
+ {
216
+ id: 'status',
217
+ type: 'select',
218
+ value: statusFilter,
219
+ onChange: setStatusFilter,
220
+ placeholder: commonT('labels.status'),
221
+ options: [
222
+ { value: 'all', label: commonT('filters.allStatuses') },
223
+ { value: 'submitted', label: formatEnumLabel('submitted') },
224
+ { value: 'approved', label: formatEnumLabel('approved') },
225
+ { value: 'rejected', label: formatEnumLabel('rejected') },
226
+ ],
227
+ },
228
+ ]}
229
+ />
230
+
231
+ <KpiCardsGrid items={cards} columns={3} />
232
+
233
+ {filteredRows.length > 0 ? (
234
+ <div className="overflow-x-auto rounded-md border">
235
+ <Table>
236
+ <TableHeader>
237
+ <TableRow>
238
+ <TableHead>{commonT('labels.collaborator')}</TableHead>
239
+ <TableHead>{commonT('labels.requestScope')}</TableHead>
240
+ <TableHead>{commonT('labels.timeline')}</TableHead>
241
+ <TableHead>{t('table.currentSchedule')}</TableHead>
242
+ <TableHead>{commonT('labels.schedule')}</TableHead>
243
+ <TableHead>{commonT('labels.approver')}</TableHead>
244
+ <TableHead>{commonT('labels.status')}</TableHead>
245
+ <TableHead>{commonT('labels.notes')}</TableHead>
246
+ </TableRow>
247
+ </TableHeader>
248
+ <TableBody>
249
+ {filteredRows.map((requestItem) => (
250
+ <TableRow key={requestItem.id}>
251
+ <TableCell>{requestItem.collaboratorName}</TableCell>
252
+ <TableCell>{formatEnumLabel(requestItem.requestScope)}</TableCell>
253
+ <TableCell>
254
+ {formatDateRange(
255
+ requestItem.effectiveStartDate,
256
+ requestItem.effectiveEndDate
257
+ )}
258
+ </TableCell>
259
+ <TableCell>
260
+ {summarizeScheduleDays(requestItem.currentSchedule ?? [])}
261
+ </TableCell>
262
+ <TableCell>
263
+ {summarizeScheduleDays(
264
+ (requestItem.days ?? []) as OperationsScheduleAdjustmentDay[]
265
+ )}
266
+ </TableCell>
267
+ <TableCell>
268
+ {requestItem.approverName || commonT('labels.notAssigned')}
269
+ </TableCell>
270
+ <TableCell>
271
+ <StatusBadge
272
+ label={formatEnumLabel(requestItem.status)}
273
+ className={getStatusBadgeClass(requestItem.status)}
274
+ />
275
+ </TableCell>
276
+ <TableCell>
277
+ {requestItem.approverNote || commonT('labels.noNotes')}
278
+ </TableCell>
279
+ </TableRow>
280
+ ))}
281
+ </TableBody>
282
+ </Table>
283
+ </div>
284
+ ) : (
285
+ <EmptyState
286
+ icon={<CalendarRange className="size-12" />}
287
+ title={commonT('states.emptyTitle')}
288
+ description={t('emptyDescription')}
289
+ actionLabel={access.isCollaborator ? commonT('actions.create') : commonT('actions.refresh')}
290
+ onAction={access.isCollaborator ? () => setIsSheetOpen(true) : () => void refetch()}
291
+ />
292
+ )}
293
+
294
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
295
+ <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
296
+ <SheetHeader>
297
+ <SheetTitle>{t('sheet.title')}</SheetTitle>
298
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
299
+ </SheetHeader>
300
+
301
+ <div className="mt-6 grid gap-4">
302
+ <div className="grid gap-4 md:grid-cols-3">
303
+ <div className="space-y-2">
304
+ <label className="text-sm font-medium">{commonT('labels.requestScope')}</label>
305
+ <Select
306
+ value={form.requestScope}
307
+ onValueChange={(value) =>
308
+ setForm((current) => ({ ...current, requestScope: value }))
309
+ }
310
+ >
311
+ <SelectTrigger>
312
+ <SelectValue />
313
+ </SelectTrigger>
314
+ <SelectContent>
315
+ <SelectItem value="temporary">Temporary</SelectItem>
316
+ <SelectItem value="permanent">Permanent</SelectItem>
317
+ </SelectContent>
318
+ </Select>
319
+ </div>
320
+ <div className="space-y-2">
321
+ <label className="text-sm font-medium">{commonT('labels.startDate')}</label>
322
+ <Input
323
+ type="date"
324
+ value={form.effectiveStartDate}
325
+ onChange={(event) =>
326
+ setForm((current) => ({
327
+ ...current,
328
+ effectiveStartDate: event.target.value,
329
+ }))
330
+ }
331
+ />
332
+ </div>
333
+ <div className="space-y-2">
334
+ <label className="text-sm font-medium">{commonT('labels.endDate')}</label>
335
+ <Input
336
+ type="date"
337
+ value={form.effectiveEndDate}
338
+ onChange={(event) =>
339
+ setForm((current) => ({
340
+ ...current,
341
+ effectiveEndDate: event.target.value,
342
+ }))
343
+ }
344
+ />
345
+ </div>
346
+ </div>
347
+
348
+ <div className="space-y-2">
349
+ <label className="text-sm font-medium">{commonT('labels.reason')}</label>
350
+ <Textarea
351
+ rows={3}
352
+ value={form.reason}
353
+ onChange={(event) =>
354
+ setForm((current) => ({ ...current, reason: event.target.value }))
355
+ }
356
+ />
357
+ </div>
358
+
359
+ <div className="space-y-3">
360
+ <div className="text-sm font-medium">{commonT('labels.weeklySchedule')}</div>
361
+ <div className="space-y-3">
362
+ {form.days.map((day) => (
363
+ <div
364
+ key={day.weekday}
365
+ className="grid gap-3 rounded-lg border p-4 lg:grid-cols-[1fr_auto_1fr_1fr_1fr]"
366
+ >
367
+ <div>
368
+ <div className="font-medium">{formatEnumLabel(day.weekday)}</div>
369
+ <div className="text-xs text-muted-foreground">
370
+ {day.isWorkingDay
371
+ ? commonT('labels.workingDay')
372
+ : commonT('labels.dayOff')}
373
+ </div>
374
+ </div>
375
+ <div className="flex items-center gap-2">
376
+ <Switch
377
+ checked={day.isWorkingDay}
378
+ onCheckedChange={(checked) =>
379
+ updateDay(day.weekday, { isWorkingDay: checked })
380
+ }
381
+ />
382
+ </div>
383
+ <Input
384
+ type="time"
385
+ value={day.startTime}
386
+ disabled={!day.isWorkingDay}
387
+ onChange={(event) =>
388
+ updateDay(day.weekday, { startTime: event.target.value })
389
+ }
390
+ />
391
+ <Input
392
+ type="time"
393
+ value={day.endTime}
394
+ disabled={!day.isWorkingDay}
395
+ onChange={(event) =>
396
+ updateDay(day.weekday, { endTime: event.target.value })
397
+ }
398
+ />
399
+ <Input
400
+ type="number"
401
+ value={day.breakMinutes}
402
+ disabled={!day.isWorkingDay}
403
+ onChange={(event) =>
404
+ updateDay(day.weekday, { breakMinutes: event.target.value })
405
+ }
406
+ />
407
+ </div>
408
+ ))}
409
+ </div>
410
+ </div>
411
+
412
+ <Button onClick={() => void onSubmit()}>{commonT('actions.save')}</Button>
413
+ </div>
414
+ </SheetContent>
415
+ </Sheet>
416
+ </Page>
417
+ );
418
+ }