@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.
Files changed (39) hide show
  1. package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
  2. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-timesheets.controller.js +12 -0
  4. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  5. package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
  6. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  7. package/dist/dto/update-collaborator-type.dto.js +2 -1
  8. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  9. package/dist/operations.service.d.ts +22 -0
  10. package/dist/operations.service.d.ts.map +1 -1
  11. package/dist/operations.service.js +180 -47
  12. package/dist/operations.service.js.map +1 -1
  13. package/dist/operations.service.spec.js +73 -0
  14. package/dist/operations.service.spec.js.map +1 -1
  15. package/hedhog/data/menu.yaml +26 -26
  16. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  17. package/hedhog/data/route.yaml +13 -0
  18. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  19. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  20. package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
  21. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
  22. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
  23. package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
  24. package/hedhog/frontend/messages/en.json +5 -0
  25. package/hedhog/frontend/messages/pt.json +7 -2
  26. package/hedhog/table/operations_collaborator.yaml +18 -18
  27. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  28. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  29. package/hedhog/table/operations_contract_document.yaml +33 -33
  30. package/package.json +4 -4
  31. package/src/controllers/operations-timesheets.controller.ts +13 -0
  32. package/src/dto/create-collaborator-type.dto.ts +43 -43
  33. package/src/dto/create-collaborator.dto.ts +223 -223
  34. package/src/dto/list-collaborator-types.dto.ts +15 -15
  35. package/src/dto/list-collaborators.dto.ts +30 -30
  36. package/src/dto/update-collaborator-type.dto.ts +4 -3
  37. package/src/dto/update-collaborator.dto.ts +3 -3
  38. package/src/operations.service.spec.ts +96 -0
  39. 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
- () => summarizeScheduleDays(form.days, currentLocaleCode),
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="overflow-x-auto rounded-md border">
287
- <Table>
288
- <TableHeader>
289
- <TableRow>
290
- <TableHead>{commonT('labels.collaborator')}</TableHead>
291
- <TableHead>{commonT('labels.requestScope')}</TableHead>
292
- <TableHead>{commonT('labels.timeline')}</TableHead>
293
- <TableHead>{t('table.currentSchedule')}</TableHead>
294
- <TableHead>{t('table.requestedSchedule')}</TableHead>
295
- <TableHead>{commonT('labels.approver')}</TableHead>
296
- <TableHead>{commonT('labels.status')}</TableHead>
297
- <TableHead>{commonT('labels.notes')}</TableHead>
298
- </TableRow>
299
- </TableHeader>
300
- <TableBody>
301
- {filteredRows.map((requestItem) => (
302
- <TableRow key={requestItem.id}>
303
- <TableCell>{requestItem.collaboratorName}</TableCell>
304
- <TableCell>
305
- {getRequestScopeLabel(requestItem.requestScope)}
306
- </TableCell>
307
- <TableCell>
308
- {formatDateRange(
309
- requestItem.effectiveStartDate,
310
- requestItem.effectiveEndDate
311
- )}
312
- </TableCell>
313
- <TableCell>
314
- {summarizeScheduleDays(
315
- requestItem.currentSchedule ?? [],
316
- currentLocaleCode
317
- )}
318
- </TableCell>
319
- <TableCell>
320
- {summarizeScheduleDays(
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
- currentLocaleCode
324
- )}
325
- </TableCell>
326
- <TableCell>
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
- </TableCell>
329
- <TableCell>
330
- <div className="space-y-1">
331
- <StatusBadge
332
- label={getStatusLabel(requestItem.status)}
333
- className={getStatusBadgeClass(requestItem.status)}
334
- />
335
- {getStatusDescription(requestItem.status) ? (
336
- <p className="max-w-40 text-xs text-muted-foreground">
337
- {getStatusDescription(requestItem.status)}
338
- </p>
339
- ) : null}
340
- </div>
341
- </TableCell>
342
- <TableCell>
343
- {requestItem.approverNote || commonT('labels.noNotes')}
344
- </TableCell>
345
- </TableRow>
346
- ))}
347
- </TableBody>
348
- </Table>
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