@hed-hog/operations 0.0.305 → 0.0.306
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/controllers/operations-timesheets.controller.d.ts +21 -0
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +12 -0
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
- package/dist/dto/update-collaborator-type.dto.js +2 -1
- package/dist/dto/update-collaborator-type.dto.js.map +1 -1
- package/dist/operations.service.d.ts +22 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +180 -47
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +73 -0
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +26 -26
- package/hedhog/data/operations_collaborator_type.yaml +76 -76
- package/hedhog/data/route.yaml +13 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
- package/hedhog/frontend/messages/en.json +5 -0
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/operations_collaborator.yaml +18 -18
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
- package/hedhog/table/operations_collaborator_type.yaml +33 -33
- package/hedhog/table/operations_contract_document.yaml +33 -33
- package/package.json +4 -4
- package/src/controllers/operations-timesheets.controller.ts +13 -0
- package/src/dto/create-collaborator-type.dto.ts +43 -43
- package/src/dto/create-collaborator.dto.ts +223 -223
- package/src/dto/list-collaborator-types.dto.ts +15 -15
- package/src/dto/list-collaborators.dto.ts +30 -30
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/operations.service.spec.ts +96 -0
- package/src/operations.service.ts +257 -47
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page, SearchBar } from '@/components/entity-list';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
5
|
import { Button } from '@/components/ui/button';
|
|
5
6
|
import { FormActions } from '@/components/ui/form-actions';
|
|
6
7
|
import { Input } from '@/components/ui/input';
|
|
@@ -20,17 +21,9 @@ import {
|
|
|
20
21
|
SheetTitle,
|
|
21
22
|
} from '@/components/ui/sheet';
|
|
22
23
|
import { Switch } from '@/components/ui/switch';
|
|
23
|
-
import {
|
|
24
|
-
Table,
|
|
25
|
-
TableBody,
|
|
26
|
-
TableCell,
|
|
27
|
-
TableHead,
|
|
28
|
-
TableHeader,
|
|
29
|
-
TableRow,
|
|
30
|
-
} from '@/components/ui/table';
|
|
31
24
|
import { Textarea } from '@/components/ui/textarea';
|
|
32
25
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
33
|
-
import { CalendarRange, Plus } from 'lucide-react';
|
|
26
|
+
import { CalendarRange, Clock, Plus, User } from 'lucide-react';
|
|
34
27
|
import { useTranslations } from 'next-intl';
|
|
35
28
|
import { useMemo, useState } from 'react';
|
|
36
29
|
import { OperationsHeader } from '../_components/operations-header';
|
|
@@ -46,7 +39,6 @@ import {
|
|
|
46
39
|
formatEnumLabel,
|
|
47
40
|
formatWeekdayLabel,
|
|
48
41
|
getStatusBadgeClass,
|
|
49
|
-
summarizeScheduleDays,
|
|
50
42
|
} from '../_lib/utils/format';
|
|
51
43
|
import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
|
|
52
44
|
|
|
@@ -60,6 +52,130 @@ const weekdays = [
|
|
|
60
52
|
'sunday',
|
|
61
53
|
] as const;
|
|
62
54
|
|
|
55
|
+
type ScheduleDay = {
|
|
56
|
+
weekday: string;
|
|
57
|
+
isWorkingDay: boolean;
|
|
58
|
+
startTime?: string | null;
|
|
59
|
+
endTime?: string | null;
|
|
60
|
+
breakMinutes?: number | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const WEEKDAY_ORDER = [
|
|
64
|
+
'monday',
|
|
65
|
+
'tuesday',
|
|
66
|
+
'wednesday',
|
|
67
|
+
'thursday',
|
|
68
|
+
'friday',
|
|
69
|
+
'saturday',
|
|
70
|
+
'sunday',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function formatTimeValue(t: string) {
|
|
74
|
+
// Handle full ISO datetime (e.g. "1970-01-01T09:00:00.000Z") returned by pg driver
|
|
75
|
+
if (t.includes('T')) {
|
|
76
|
+
return t.split('T')[1]?.slice(0, 5) ?? '--:--';
|
|
77
|
+
}
|
|
78
|
+
return t.slice(0, 5);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function groupScheduleLines(days: ScheduleDay[], locale: string) {
|
|
82
|
+
if (!days.length) return [];
|
|
83
|
+
|
|
84
|
+
// Sort by canonical weekday order before grouping
|
|
85
|
+
const sorted = [...days].sort(
|
|
86
|
+
(a, b) =>
|
|
87
|
+
WEEKDAY_ORDER.indexOf(a.weekday.toLowerCase()) -
|
|
88
|
+
WEEKDAY_ORDER.indexOf(b.weekday.toLowerCase())
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const dayAbbr = (weekday: string) => {
|
|
92
|
+
const full = formatWeekdayLabel(weekday, locale);
|
|
93
|
+
return full.slice(0, 3);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type Group = {
|
|
97
|
+
days: string[];
|
|
98
|
+
time: string | null;
|
|
99
|
+
break: number | null;
|
|
100
|
+
isOff: boolean;
|
|
101
|
+
};
|
|
102
|
+
const groups: Group[] = [];
|
|
103
|
+
|
|
104
|
+
for (const day of sorted) {
|
|
105
|
+
const time =
|
|
106
|
+
day.isWorkingDay && day.startTime && day.endTime
|
|
107
|
+
? `${formatTimeValue(day.startTime)}–${formatTimeValue(day.endTime)}`
|
|
108
|
+
: null;
|
|
109
|
+
const breakMin = day.isWorkingDay ? (day.breakMinutes ?? null) : null;
|
|
110
|
+
const isOff = !day.isWorkingDay;
|
|
111
|
+
|
|
112
|
+
const last = groups[groups.length - 1];
|
|
113
|
+
if (
|
|
114
|
+
last &&
|
|
115
|
+
last.isOff === isOff &&
|
|
116
|
+
last.time === time &&
|
|
117
|
+
last.break === breakMin
|
|
118
|
+
) {
|
|
119
|
+
last.days.push(dayAbbr(day.weekday));
|
|
120
|
+
} else {
|
|
121
|
+
groups.push({
|
|
122
|
+
days: [dayAbbr(day.weekday)],
|
|
123
|
+
time,
|
|
124
|
+
break: breakMin,
|
|
125
|
+
isOff,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return groups.map((g) => {
|
|
131
|
+
const dayLabel =
|
|
132
|
+
g.days.length > 1
|
|
133
|
+
? `${g.days[0]}–${g.days[g.days.length - 1]}`
|
|
134
|
+
: g.days[0];
|
|
135
|
+
return { dayLabel, time: g.time, break: g.break, isOff: g.isOff };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function SchedulePanel({
|
|
140
|
+
days,
|
|
141
|
+
locale,
|
|
142
|
+
emptyLabel,
|
|
143
|
+
}: {
|
|
144
|
+
days: ScheduleDay[];
|
|
145
|
+
locale: string;
|
|
146
|
+
emptyLabel: string;
|
|
147
|
+
}) {
|
|
148
|
+
const lines = groupScheduleLines(days, locale);
|
|
149
|
+
if (!lines.length)
|
|
150
|
+
return <p className="text-xs text-muted-foreground">{emptyLabel}</p>;
|
|
151
|
+
return (
|
|
152
|
+
<div className="space-y-1">
|
|
153
|
+
{lines.map((line, i) => (
|
|
154
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
155
|
+
<span
|
|
156
|
+
className={`inline-block size-2 shrink-0 rounded-full ${line.isOff ? 'bg-muted-foreground/30' : 'bg-emerald-500'}`}
|
|
157
|
+
/>
|
|
158
|
+
<span className="w-16 shrink-0 font-medium text-foreground">
|
|
159
|
+
{line.dayLabel}
|
|
160
|
+
</span>
|
|
161
|
+
{line.isOff ? (
|
|
162
|
+
<span className="text-muted-foreground">Folga</span>
|
|
163
|
+
) : (
|
|
164
|
+
<span className="text-foreground">
|
|
165
|
+
{line.time}
|
|
166
|
+
{line.break ? (
|
|
167
|
+
<span className="ml-1 text-muted-foreground">
|
|
168
|
+
({line.break}min int.)
|
|
169
|
+
</span>
|
|
170
|
+
) : null}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
63
179
|
type ScheduleFormState = {
|
|
64
180
|
requestScope: string;
|
|
65
181
|
effectiveStartDate: string;
|
|
@@ -184,7 +300,19 @@ export default function OperationsScheduleAdjustmentsPage() {
|
|
|
184
300
|
];
|
|
185
301
|
|
|
186
302
|
const requestedScheduleSummary = useMemo(
|
|
187
|
-
() =>
|
|
303
|
+
() =>
|
|
304
|
+
groupScheduleLines(
|
|
305
|
+
form.days.map((day) => ({
|
|
306
|
+
...day,
|
|
307
|
+
breakMinutes: day.isWorkingDay
|
|
308
|
+
? parseNumberInput(day.breakMinutes)
|
|
309
|
+
: null,
|
|
310
|
+
})),
|
|
311
|
+
currentLocaleCode
|
|
312
|
+
)
|
|
313
|
+
.filter((line) => !line.isOff)
|
|
314
|
+
.map((line) => `${line.dayLabel}: ${line.time}`)
|
|
315
|
+
.join(', ') || '-',
|
|
188
316
|
[form.days, currentLocaleCode]
|
|
189
317
|
);
|
|
190
318
|
|
|
@@ -283,69 +411,104 @@ export default function OperationsScheduleAdjustmentsPage() {
|
|
|
283
411
|
<KpiCardsGrid items={cards} columns={3} />
|
|
284
412
|
|
|
285
413
|
{filteredRows.length > 0 ? (
|
|
286
|
-
<div className="
|
|
287
|
-
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
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
|
-
|
|
414
|
+
<div className="space-y-3">
|
|
415
|
+
{filteredRows.map((requestItem) => (
|
|
416
|
+
<div
|
|
417
|
+
key={requestItem.id}
|
|
418
|
+
className="rounded-lg border bg-card shadow-sm"
|
|
419
|
+
>
|
|
420
|
+
{/* Card header */}
|
|
421
|
+
<div className="flex flex-wrap items-start justify-between gap-3 border-b px-4 py-3">
|
|
422
|
+
<div className="space-y-1">
|
|
423
|
+
<div className="flex items-center gap-2">
|
|
424
|
+
<User className="size-4 shrink-0 text-muted-foreground" />
|
|
425
|
+
<span className="font-semibold">
|
|
426
|
+
{requestItem.collaboratorName}
|
|
427
|
+
</span>
|
|
428
|
+
<Badge variant="outline" className="text-xs">
|
|
429
|
+
{getRequestScopeLabel(requestItem.requestScope)}
|
|
430
|
+
</Badge>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
433
|
+
<CalendarRange className="size-3.5 shrink-0" />
|
|
434
|
+
<span>
|
|
435
|
+
{formatDateRange(
|
|
436
|
+
requestItem.effectiveStartDate,
|
|
437
|
+
requestItem.effectiveEndDate
|
|
438
|
+
)}
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<div className="flex flex-col items-end gap-1">
|
|
443
|
+
<StatusBadge
|
|
444
|
+
label={getStatusLabel(requestItem.status)}
|
|
445
|
+
className={getStatusBadgeClass(requestItem.status)}
|
|
446
|
+
/>
|
|
447
|
+
{getStatusDescription(requestItem.status) ? (
|
|
448
|
+
<p className="text-xs text-muted-foreground">
|
|
449
|
+
{getStatusDescription(requestItem.status)}
|
|
450
|
+
</p>
|
|
451
|
+
) : null}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* Schedule comparison */}
|
|
456
|
+
<div className="grid gap-px bg-border sm:grid-cols-2">
|
|
457
|
+
<div className="bg-card px-4 py-3">
|
|
458
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
459
|
+
{t('table.currentSchedule')}
|
|
460
|
+
</p>
|
|
461
|
+
<SchedulePanel
|
|
462
|
+
days={requestItem.currentSchedule ?? []}
|
|
463
|
+
locale={currentLocaleCode}
|
|
464
|
+
emptyLabel={commonT('labels.notAssigned')}
|
|
465
|
+
/>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="bg-card px-4 py-3">
|
|
468
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
469
|
+
{t('table.requestedSchedule')}
|
|
470
|
+
</p>
|
|
471
|
+
<SchedulePanel
|
|
472
|
+
days={
|
|
321
473
|
(requestItem.days ??
|
|
322
|
-
[]) as OperationsScheduleAdjustmentDay[]
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
474
|
+
[]) as OperationsScheduleAdjustmentDay[]
|
|
475
|
+
}
|
|
476
|
+
locale={currentLocaleCode}
|
|
477
|
+
emptyLabel={commonT('labels.notAssigned')}
|
|
478
|
+
/>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
{/* Card footer */}
|
|
483
|
+
<div className="flex flex-wrap items-start justify-between gap-3 border-t bg-muted/20 px-4 py-2.5 text-sm">
|
|
484
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
485
|
+
<Clock className="size-3.5 shrink-0" />
|
|
486
|
+
<span className="font-medium">
|
|
487
|
+
{commonT('labels.approver')}:
|
|
488
|
+
</span>
|
|
489
|
+
<span>
|
|
327
490
|
{requestItem.approverName || commonT('labels.notAssigned')}
|
|
328
|
-
</
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{requestItem.
|
|
344
|
-
</
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
</
|
|
348
|
-
|
|
491
|
+
</span>
|
|
492
|
+
</div>
|
|
493
|
+
{requestItem.approverNote ? (
|
|
494
|
+
<div className="text-xs text-muted-foreground">
|
|
495
|
+
<span className="font-medium">
|
|
496
|
+
{commonT('labels.notes')}:
|
|
497
|
+
</span>{' '}
|
|
498
|
+
{requestItem.approverNote}
|
|
499
|
+
</div>
|
|
500
|
+
) : null}
|
|
501
|
+
{requestItem.reason ? (
|
|
502
|
+
<div className="text-xs text-muted-foreground">
|
|
503
|
+
<span className="font-medium">
|
|
504
|
+
{commonT('labels.reason')}:
|
|
505
|
+
</span>{' '}
|
|
506
|
+
{requestItem.reason}
|
|
507
|
+
</div>
|
|
508
|
+
) : null}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
))}
|
|
349
512
|
</div>
|
|
350
513
|
) : (
|
|
351
514
|
<EmptyState
|