@hed-hog/operations 0.0.305 → 0.0.309

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 (138) hide show
  1. package/dist/controllers/operations-approvals.controller.d.ts +114 -1
  2. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-approvals.controller.js +16 -3
  4. package/dist/controllers/operations-approvals.controller.js.map +1 -1
  5. package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +16 -3
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +14 -453
  10. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-contracts.controller.js +11 -112
  12. package/dist/controllers/operations-contracts.controller.js.map +1 -1
  13. package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
  14. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
  15. package/dist/controllers/operations-org-structure.controller.js +18 -5
  16. package/dist/controllers/operations-org-structure.controller.js.map +1 -1
  17. package/dist/controllers/operations-projects.controller.d.ts +28 -4
  18. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-projects.controller.js +17 -5
  20. package/dist/controllers/operations-projects.controller.js.map +1 -1
  21. package/dist/controllers/operations-timesheets.controller.d.ts +52 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +28 -11
  24. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  25. package/dist/dto/list-approvals.dto.d.ts +6 -0
  26. package/dist/dto/list-approvals.dto.d.ts.map +1 -0
  27. package/dist/dto/list-approvals.dto.js +28 -0
  28. package/dist/dto/list-approvals.dto.js.map +1 -0
  29. package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
  30. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
  31. package/dist/dto/list-collaborator-types.dto.js +7 -1
  32. package/dist/dto/list-collaborator-types.dto.js.map +1 -1
  33. package/dist/dto/list-collaborators.dto.d.ts +1 -0
  34. package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
  35. package/dist/dto/list-collaborators.dto.js +5 -0
  36. package/dist/dto/list-collaborators.dto.js.map +1 -1
  37. package/dist/dto/list-contracts.dto.d.ts +8 -0
  38. package/dist/dto/list-contracts.dto.d.ts.map +1 -0
  39. package/dist/dto/list-contracts.dto.js +38 -0
  40. package/dist/dto/list-contracts.dto.js.map +1 -0
  41. package/dist/dto/list-departments.dto.d.ts +5 -0
  42. package/dist/dto/list-departments.dto.d.ts.map +1 -0
  43. package/dist/dto/list-departments.dto.js +23 -0
  44. package/dist/dto/list-departments.dto.js.map +1 -0
  45. package/dist/dto/list-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-projects.dto.js +23 -0
  48. package/dist/dto/list-projects.dto.js.map +1 -0
  49. package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
  50. package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
  51. package/dist/dto/list-schedule-adjustments.dto.js +23 -0
  52. package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
  53. package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
  54. package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
  55. package/dist/dto/list-time-off-requests.dto.js +23 -0
  56. package/dist/dto/list-time-off-requests.dto.js.map +1 -0
  57. package/dist/dto/list-timesheets.dto.d.ts +5 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
  59. package/dist/dto/list-timesheets.dto.js +23 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -0
  61. package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
  62. package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
  63. package/dist/dto/reorder-collaborator-types.dto.js +25 -0
  64. package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
  65. package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
  66. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  67. package/dist/dto/update-collaborator-type.dto.js +2 -1
  68. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  69. package/dist/operations.service.d.ts +362 -271
  70. package/dist/operations.service.d.ts.map +1 -1
  71. package/dist/operations.service.js +1195 -1098
  72. package/dist/operations.service.js.map +1 -1
  73. package/dist/operations.service.spec.js +73 -22
  74. package/dist/operations.service.spec.js.map +1 -1
  75. package/hedhog/data/menu.yaml +19 -55
  76. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  77. package/hedhog/data/route.yaml +52 -70
  78. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  79. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  80. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  81. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  82. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  83. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  84. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  85. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  86. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  87. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  88. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  89. package/hedhog/frontend/app/approvals/page.tsx.ejs +843 -151
  90. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +457 -154
  91. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  92. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  93. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  94. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  95. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  96. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +546 -118
  97. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  98. package/hedhog/frontend/app/timesheets/page.tsx.ejs +647 -342
  99. package/hedhog/frontend/messages/en.json +148 -14
  100. package/hedhog/frontend/messages/pt.json +199 -56
  101. package/hedhog/table/operations_collaborator.yaml +18 -18
  102. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  103. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  104. package/hedhog/table/operations_contract.yaml +0 -9
  105. package/hedhog/table/operations_contract_document.yaml +33 -33
  106. package/package.json +4 -4
  107. package/src/controllers/operations-approvals.controller.ts +9 -3
  108. package/src/controllers/operations-collaborators.controller.ts +15 -2
  109. package/src/controllers/operations-contracts.controller.ts +8 -92
  110. package/src/controllers/operations-org-structure.controller.ts +17 -4
  111. package/src/controllers/operations-projects.controller.ts +10 -4
  112. package/src/controllers/operations-timesheets.controller.ts +30 -8
  113. package/src/dto/create-collaborator-type.dto.ts +43 -43
  114. package/src/dto/create-collaborator.dto.ts +223 -223
  115. package/src/dto/list-approvals.dto.ts +12 -0
  116. package/src/dto/list-collaborator-types.dto.ts +20 -15
  117. package/src/dto/list-collaborators.dto.ts +34 -30
  118. package/src/dto/list-contracts.dto.ts +20 -0
  119. package/src/dto/list-departments.dto.ts +8 -0
  120. package/src/dto/list-projects.dto.ts +8 -0
  121. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  122. package/src/dto/list-time-off-requests.dto.ts +8 -0
  123. package/src/dto/list-timesheets.dto.ts +8 -0
  124. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  125. package/src/dto/update-collaborator-type.dto.ts +4 -3
  126. package/src/dto/update-collaborator.dto.ts +3 -3
  127. package/src/operations.service.spec.ts +96 -30
  128. package/src/operations.service.ts +1738 -1777
  129. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  130. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  131. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  132. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  133. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  134. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  135. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  136. package/hedhog/table/operations_contract_revision.yaml +0 -38
  137. package/hedhog/table/operations_contract_signature.yaml +0 -38
  138. package/hedhog/table/operations_contract_template.yaml +0 -58
@@ -1,6 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import { Badge } from '@/components/ui/badge';
4
10
  import { Button } from '@/components/ui/button';
5
11
  import { FormActions } from '@/components/ui/form-actions';
6
12
  import { Input } from '@/components/ui/input';
@@ -29,8 +35,17 @@ import {
29
35
  TableRow,
30
36
  } from '@/components/ui/table';
31
37
  import { Textarea } from '@/components/ui/textarea';
38
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
32
39
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
33
- import { CalendarRange, Plus } from 'lucide-react';
40
+ import {
41
+ CalendarRange,
42
+ Clock,
43
+ Eye,
44
+ LayoutGrid,
45
+ List,
46
+ Plus,
47
+ User,
48
+ } from 'lucide-react';
34
49
  import { useTranslations } from 'next-intl';
35
50
  import { useMemo, useState } from 'react';
36
51
  import { OperationsHeader } from '../_components/operations-header';
@@ -40,13 +55,13 @@ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
40
55
  import type {
41
56
  OperationsScheduleAdjustmentDay,
42
57
  OperationsScheduleAdjustmentRequest,
58
+ PaginatedResponse,
43
59
  } from '../_lib/types';
44
60
  import {
45
61
  formatDateRange,
46
62
  formatEnumLabel,
47
63
  formatWeekdayLabel,
48
64
  getStatusBadgeClass,
49
- summarizeScheduleDays,
50
65
  } from '../_lib/utils/format';
51
66
  import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
52
67
 
@@ -60,6 +75,130 @@ const weekdays = [
60
75
  'sunday',
61
76
  ] as const;
62
77
 
78
+ type ScheduleDay = {
79
+ weekday: string;
80
+ isWorkingDay: boolean;
81
+ startTime?: string | null;
82
+ endTime?: string | null;
83
+ breakMinutes?: number | null;
84
+ };
85
+
86
+ const WEEKDAY_ORDER = [
87
+ 'monday',
88
+ 'tuesday',
89
+ 'wednesday',
90
+ 'thursday',
91
+ 'friday',
92
+ 'saturday',
93
+ 'sunday',
94
+ ];
95
+
96
+ function formatTimeValue(t: string) {
97
+ // Handle full ISO datetime (e.g. "1970-01-01T09:00:00.000Z") returned by pg driver
98
+ if (t.includes('T')) {
99
+ return t.split('T')[1]?.slice(0, 5) ?? '--:--';
100
+ }
101
+ return t.slice(0, 5);
102
+ }
103
+
104
+ function groupScheduleLines(days: ScheduleDay[], locale: string) {
105
+ if (!days.length) return [];
106
+
107
+ // Sort by canonical weekday order before grouping
108
+ const sorted = [...days].sort(
109
+ (a, b) =>
110
+ WEEKDAY_ORDER.indexOf(a.weekday.toLowerCase()) -
111
+ WEEKDAY_ORDER.indexOf(b.weekday.toLowerCase())
112
+ );
113
+
114
+ const dayAbbr = (weekday: string) => {
115
+ const full = formatWeekdayLabel(weekday, locale);
116
+ return full.slice(0, 3);
117
+ };
118
+
119
+ type Group = {
120
+ days: string[];
121
+ time: string | null;
122
+ break: number | null;
123
+ isOff: boolean;
124
+ };
125
+ const groups: Group[] = [];
126
+
127
+ for (const day of sorted) {
128
+ const time =
129
+ day.isWorkingDay && day.startTime && day.endTime
130
+ ? `${formatTimeValue(day.startTime)}–${formatTimeValue(day.endTime)}`
131
+ : null;
132
+ const breakMin = day.isWorkingDay ? (day.breakMinutes ?? null) : null;
133
+ const isOff = !day.isWorkingDay;
134
+
135
+ const last = groups[groups.length - 1];
136
+ if (
137
+ last &&
138
+ last.isOff === isOff &&
139
+ last.time === time &&
140
+ last.break === breakMin
141
+ ) {
142
+ last.days.push(dayAbbr(day.weekday));
143
+ } else {
144
+ groups.push({
145
+ days: [dayAbbr(day.weekday)],
146
+ time,
147
+ break: breakMin,
148
+ isOff,
149
+ });
150
+ }
151
+ }
152
+
153
+ return groups.map((g) => {
154
+ const dayLabel =
155
+ g.days.length > 1
156
+ ? `${g.days[0]}–${g.days[g.days.length - 1]}`
157
+ : g.days[0];
158
+ return { dayLabel, time: g.time, break: g.break, isOff: g.isOff };
159
+ });
160
+ }
161
+
162
+ function SchedulePanel({
163
+ days,
164
+ locale,
165
+ emptyLabel,
166
+ }: {
167
+ days: ScheduleDay[];
168
+ locale: string;
169
+ emptyLabel: string;
170
+ }) {
171
+ const lines = groupScheduleLines(days, locale);
172
+ if (!lines.length)
173
+ return <p className="text-xs text-muted-foreground">{emptyLabel}</p>;
174
+ return (
175
+ <div className="space-y-1">
176
+ {lines.map((line, i) => (
177
+ <div key={i} className="flex items-center gap-2 text-xs">
178
+ <span
179
+ className={`inline-block size-2 shrink-0 rounded-full ${line.isOff ? 'bg-muted-foreground/30' : 'bg-emerald-500'}`}
180
+ />
181
+ <span className="w-16 shrink-0 font-medium text-foreground">
182
+ {line.dayLabel}
183
+ </span>
184
+ {line.isOff ? (
185
+ <span className="text-muted-foreground">Folga</span>
186
+ ) : (
187
+ <span className="text-foreground">
188
+ {line.time}
189
+ {line.break ? (
190
+ <span className="ml-1 text-muted-foreground">
191
+ ({line.break}min int.)
192
+ </span>
193
+ ) : null}
194
+ </span>
195
+ )}
196
+ </div>
197
+ ))}
198
+ </div>
199
+ );
200
+ }
201
+
63
202
  type ScheduleFormState = {
64
203
  requestScope: string;
65
204
  effectiveStartDate: string;
@@ -95,7 +234,19 @@ export default function OperationsScheduleAdjustmentsPage() {
95
234
  const access = useOperationsAccess();
96
235
  const [search, setSearch] = useState('');
97
236
  const [statusFilter, setStatusFilter] = useState('all');
237
+ const [page, setPage] = useState(1);
238
+ const [pageSize, setPageSize] = useState(12);
239
+ const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
240
+ if (typeof window === 'undefined') return 'cards';
241
+ const saved = window.localStorage.getItem(
242
+ 'operations-schedule-adjustments-view-mode'
243
+ );
244
+ return saved === 'table' ? 'table' : 'cards';
245
+ });
98
246
  const [isSheetOpen, setIsSheetOpen] = useState(false);
247
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
248
+ const [selectedRequest, setSelectedRequest] =
249
+ useState<OperationsScheduleAdjustmentRequest | null>(null);
99
250
  const [form, setForm] = useState<ScheduleFormState>(emptyForm);
100
251
 
101
252
  const getRequestScopeLabel = (value?: string | null) => {
@@ -125,41 +276,44 @@ export default function OperationsScheduleAdjustmentsPage() {
125
276
  return t.has(key) ? t(key) : null;
126
277
  };
127
278
 
128
- const { data: requests = [], refetch } = useQuery<
129
- OperationsScheduleAdjustmentRequest[]
279
+ const { data: requestsResponse, refetch } = useQuery<
280
+ PaginatedResponse<OperationsScheduleAdjustmentRequest>
130
281
  >({
131
- queryKey: ['operations-schedule-adjustments', currentLocaleCode],
282
+ queryKey: [
283
+ 'operations-schedule-adjustments',
284
+ currentLocaleCode,
285
+ search,
286
+ statusFilter,
287
+ page,
288
+ pageSize,
289
+ ],
132
290
  enabled: access.isCollaborator,
133
- queryFn: () =>
134
- fetchOperations<OperationsScheduleAdjustmentRequest[]>(
135
- request,
136
- '/operations/schedule-adjustments'
137
- ),
291
+ queryFn: () => {
292
+ const params = new URLSearchParams({
293
+ page: String(page),
294
+ pageSize: String(pageSize),
295
+ });
296
+ if (search.trim()) params.set('search', search.trim());
297
+ if (statusFilter !== 'all') params.set('status', statusFilter);
298
+ return fetchOperations<
299
+ PaginatedResponse<OperationsScheduleAdjustmentRequest>
300
+ >(request, `/operations/schedule-adjustments?${params.toString()}`);
301
+ },
302
+ placeholderData: (previous) => previous,
138
303
  });
139
304
 
140
- const filteredRows = useMemo(
141
- () =>
142
- requests.filter((item) => {
143
- const matchesSearch = !search.trim()
144
- ? true
145
- : [
146
- item.collaboratorName,
147
- item.approverName,
148
- item.reason,
149
- item.requestScope,
150
- ]
151
- .filter(Boolean)
152
- .some((value) =>
153
- String(value)
154
- .toLowerCase()
155
- .includes(search.trim().toLowerCase())
156
- );
157
- const matchesStatus =
158
- statusFilter === 'all' ? true : item.status === statusFilter;
159
- return matchesSearch && matchesStatus;
160
- }),
161
- [requests, search, statusFilter]
162
- );
305
+ const requests = requestsResponse?.data ?? [];
306
+
307
+ const handleViewModeChange = (value: string) => {
308
+ if (value !== 'table' && value !== 'cards') return;
309
+ setViewMode(value as 'table' | 'cards');
310
+ if (typeof window !== 'undefined') {
311
+ window.localStorage.setItem(
312
+ 'operations-schedule-adjustments-view-mode',
313
+ value
314
+ );
315
+ }
316
+ };
163
317
 
164
318
  const cards = [
165
319
  {
@@ -184,7 +338,19 @@ export default function OperationsScheduleAdjustmentsPage() {
184
338
  ];
185
339
 
186
340
  const requestedScheduleSummary = useMemo(
187
- () => summarizeScheduleDays(form.days, currentLocaleCode),
341
+ () =>
342
+ groupScheduleLines(
343
+ form.days.map((day) => ({
344
+ ...day,
345
+ breakMinutes: day.isWorkingDay
346
+ ? parseNumberInput(day.breakMinutes)
347
+ : null,
348
+ })),
349
+ currentLocaleCode
350
+ )
351
+ .filter((line) => !line.isOff)
352
+ .map((line) => `${line.dayLabel}: ${line.time}`)
353
+ .join(', ') || '-',
188
354
  [form.days, currentLocaleCode]
189
355
  );
190
356
 
@@ -242,6 +408,11 @@ export default function OperationsScheduleAdjustmentsPage() {
242
408
  }
243
409
  };
244
410
 
411
+ const openDetails = (requestItem: OperationsScheduleAdjustmentRequest) => {
412
+ setSelectedRequest(requestItem);
413
+ setIsDetailsOpen(true);
414
+ };
415
+
245
416
  return (
246
417
  <Page>
247
418
  <OperationsHeader
@@ -258,95 +429,239 @@ export default function OperationsScheduleAdjustmentsPage() {
258
429
  }
259
430
  />
260
431
 
261
- <SearchBar
262
- searchQuery={search}
263
- onSearchChange={setSearch}
264
- onSearch={() => undefined}
265
- placeholder={t('searchPlaceholder')}
266
- controls={[
267
- {
268
- id: 'status',
269
- type: 'select',
270
- value: statusFilter,
271
- onChange: setStatusFilter,
272
- placeholder: commonT('labels.status'),
273
- options: [
274
- { value: 'all', label: commonT('filters.allStatuses') },
275
- { value: 'submitted', label: getStatusLabel('submitted') },
276
- { value: 'approved', label: getStatusLabel('approved') },
277
- { value: 'rejected', label: getStatusLabel('rejected') },
278
- ],
279
- },
280
- ]}
281
- />
282
-
283
432
  <KpiCardsGrid items={cards} columns={3} />
284
433
 
285
- {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(
321
- (requestItem.days ??
322
- []) as OperationsScheduleAdjustmentDay[],
323
- currentLocaleCode
324
- )}
325
- </TableCell>
326
- <TableCell>
327
- {requestItem.approverName || commonT('labels.notAssigned')}
328
- </TableCell>
329
- <TableCell>
330
- <div className="space-y-1">
434
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
435
+ <div className="flex-1">
436
+ <SearchBar
437
+ searchQuery={search}
438
+ onSearchChange={(value) => {
439
+ setSearch(value);
440
+ setPage(1);
441
+ }}
442
+ showSearchButton={false}
443
+ debounceMs={500}
444
+ placeholder={t('searchPlaceholder')}
445
+ controls={[
446
+ {
447
+ id: 'status',
448
+ type: 'select',
449
+ value: statusFilter,
450
+ onChange: (value) => {
451
+ setStatusFilter(value);
452
+ setPage(1);
453
+ },
454
+ placeholder: commonT('labels.status'),
455
+ options: [
456
+ { value: 'all', label: commonT('filters.allStatuses') },
457
+ { value: 'submitted', label: getStatusLabel('submitted') },
458
+ { value: 'approved', label: getStatusLabel('approved') },
459
+ { value: 'rejected', label: getStatusLabel('rejected') },
460
+ ],
461
+ },
462
+ ]}
463
+ />
464
+ </div>
465
+
466
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
467
+ <span className="text-xs font-medium text-muted-foreground">
468
+ {t('viewMode')}
469
+ </span>
470
+ <ToggleGroup
471
+ type="single"
472
+ value={viewMode}
473
+ onValueChange={handleViewModeChange}
474
+ variant="outline"
475
+ size="sm"
476
+ >
477
+ <ToggleGroupItem value="table" className="gap-1.5 px-2.5">
478
+ <List className="h-4 w-4" />
479
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
480
+ </ToggleGroupItem>
481
+ <ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
482
+ <LayoutGrid className="h-4 w-4" />
483
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
484
+ </ToggleGroupItem>
485
+ </ToggleGroup>
486
+ </div>
487
+ </div>
488
+
489
+ {requests.length > 0 ? (
490
+ viewMode === 'cards' ? (
491
+ <div className="space-y-3">
492
+ {requests.map((requestItem) => (
493
+ <div
494
+ key={requestItem.id}
495
+ className="rounded-lg border bg-card shadow-sm"
496
+ >
497
+ {/* Card header */}
498
+ <div className="flex flex-wrap items-start justify-between gap-3 border-b px-4 py-3">
499
+ <div className="space-y-1">
500
+ <div className="flex items-center gap-2">
501
+ <User className="size-4 shrink-0 text-muted-foreground" />
502
+ <span className="font-semibold">
503
+ {requestItem.collaboratorName}
504
+ </span>
505
+ <Badge variant="outline" className="text-xs">
506
+ {getRequestScopeLabel(requestItem.requestScope)}
507
+ </Badge>
508
+ </div>
509
+ <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
510
+ <CalendarRange className="size-3.5 shrink-0" />
511
+ <span>
512
+ {formatDateRange(
513
+ requestItem.effectiveStartDate,
514
+ requestItem.effectiveEndDate
515
+ )}
516
+ </span>
517
+ </div>
518
+ </div>
519
+ <div className="flex flex-col items-end gap-1">
520
+ <StatusBadge
521
+ label={getStatusLabel(requestItem.status)}
522
+ className={getStatusBadgeClass(requestItem.status)}
523
+ />
524
+ {getStatusDescription(requestItem.status) ? (
525
+ <p className="text-xs text-muted-foreground">
526
+ {getStatusDescription(requestItem.status)}
527
+ </p>
528
+ ) : null}
529
+ </div>
530
+ </div>
531
+
532
+ {/* Schedule comparison */}
533
+ <div className="grid gap-px bg-border sm:grid-cols-2">
534
+ <div className="bg-card px-4 py-3">
535
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
536
+ {t('table.currentSchedule')}
537
+ </p>
538
+ <SchedulePanel
539
+ days={requestItem.currentSchedule ?? []}
540
+ locale={currentLocaleCode}
541
+ emptyLabel={commonT('labels.notAssigned')}
542
+ />
543
+ </div>
544
+ <div className="bg-card px-4 py-3">
545
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
546
+ {t('table.requestedSchedule')}
547
+ </p>
548
+ <SchedulePanel
549
+ days={
550
+ (requestItem.days ??
551
+ []) as OperationsScheduleAdjustmentDay[]
552
+ }
553
+ locale={currentLocaleCode}
554
+ emptyLabel={commonT('labels.notAssigned')}
555
+ />
556
+ </div>
557
+ </div>
558
+
559
+ {/* Card footer */}
560
+ <div className="flex flex-wrap items-start justify-between gap-3 border-t bg-muted/20 px-4 py-2.5 text-sm">
561
+ <div className="space-y-2">
562
+ <div className="flex items-center gap-1.5 text-muted-foreground">
563
+ <Clock className="size-3.5 shrink-0" />
564
+ <span className="font-medium">
565
+ {commonT('labels.approver')}:
566
+ </span>
567
+ <span>
568
+ {requestItem.approverName ||
569
+ commonT('labels.notAssigned')}
570
+ </span>
571
+ </div>
572
+ <div>
573
+ <Button
574
+ variant="outline"
575
+ size="sm"
576
+ className="cursor-pointer"
577
+ onClick={() => openDetails(requestItem)}
578
+ >
579
+ <Eye className="size-4" />
580
+ {t('actions.viewDetails')}
581
+ </Button>
582
+ </div>
583
+ </div>
584
+ {requestItem.approverNote ? (
585
+ <div className="text-xs text-muted-foreground">
586
+ <span className="font-medium">
587
+ {commonT('labels.notes')}:
588
+ </span>{' '}
589
+ {requestItem.approverNote}
590
+ </div>
591
+ ) : null}
592
+ {requestItem.reason ? (
593
+ <div className="text-xs text-muted-foreground">
594
+ <span className="font-medium">
595
+ {commonT('labels.reason')}:
596
+ </span>{' '}
597
+ {requestItem.reason}
598
+ </div>
599
+ ) : null}
600
+ </div>
601
+ </div>
602
+ ))}
603
+ </div>
604
+ ) : (
605
+ <div className="overflow-x-auto rounded-md border">
606
+ <Table>
607
+ <TableHeader>
608
+ <TableRow>
609
+ <TableHead>{commonT('labels.collaborator')}</TableHead>
610
+ <TableHead>{t('table.scope')}</TableHead>
611
+ <TableHead>{commonT('labels.timeline')}</TableHead>
612
+ <TableHead>{commonT('labels.approver')}</TableHead>
613
+ <TableHead>{commonT('labels.status')}</TableHead>
614
+ <TableHead className="text-right">
615
+ {commonT('labels.actions')}
616
+ </TableHead>
617
+ </TableRow>
618
+ </TableHeader>
619
+ <TableBody>
620
+ {requests.map((requestItem) => (
621
+ <TableRow key={requestItem.id} className="hover:bg-muted/30">
622
+ <TableCell className="font-medium">
623
+ {requestItem.collaboratorName}
624
+ </TableCell>
625
+ <TableCell>
626
+ <Badge variant="outline" className="text-xs">
627
+ {getRequestScopeLabel(requestItem.requestScope)}
628
+ </Badge>
629
+ </TableCell>
630
+ <TableCell>
631
+ {formatDateRange(
632
+ requestItem.effectiveStartDate,
633
+ requestItem.effectiveEndDate
634
+ )}
635
+ </TableCell>
636
+ <TableCell>
637
+ {requestItem.approverName ||
638
+ commonT('labels.notAssigned')}
639
+ </TableCell>
640
+ <TableCell>
331
641
  <StatusBadge
332
642
  label={getStatusLabel(requestItem.status)}
333
643
  className={getStatusBadgeClass(requestItem.status)}
334
644
  />
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>
349
- </div>
645
+ </TableCell>
646
+ <TableCell>
647
+ <div className="flex justify-end">
648
+ <Button
649
+ variant="outline"
650
+ size="sm"
651
+ className="cursor-pointer"
652
+ onClick={() => openDetails(requestItem)}
653
+ >
654
+ <Eye className="size-4" />
655
+ {t('actions.viewDetails')}
656
+ </Button>
657
+ </div>
658
+ </TableCell>
659
+ </TableRow>
660
+ ))}
661
+ </TableBody>
662
+ </Table>
663
+ </div>
664
+ )
350
665
  ) : (
351
666
  <EmptyState
352
667
  icon={<CalendarRange className="size-12" />}
@@ -365,6 +680,18 @@ export default function OperationsScheduleAdjustmentsPage() {
365
680
  />
366
681
  )}
367
682
 
683
+ <PaginationFooter
684
+ currentPage={page}
685
+ pageSize={pageSize}
686
+ totalItems={requestsResponse?.total ?? 0}
687
+ pageSizeOptions={[12, 24, 48]}
688
+ onPageChange={setPage}
689
+ onPageSizeChange={(size) => {
690
+ setPageSize(size);
691
+ setPage(1);
692
+ }}
693
+ />
694
+
368
695
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
369
696
  <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
370
697
  <SheetHeader>
@@ -559,6 +886,107 @@ export default function OperationsScheduleAdjustmentsPage() {
559
886
  />
560
887
  </SheetContent>
561
888
  </Sheet>
889
+
890
+ <Sheet open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
891
+ <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
892
+ <SheetHeader>
893
+ <SheetTitle>{t('details.title')}</SheetTitle>
894
+ <SheetDescription>{t('details.description')}</SheetDescription>
895
+ </SheetHeader>
896
+
897
+ {selectedRequest ? (
898
+ <div className="mt-6 space-y-5 px-4 pb-8">
899
+ <div className="rounded-lg border bg-muted/20 p-4">
900
+ <div className="flex flex-wrap items-start justify-between gap-3">
901
+ <div>
902
+ <div className="text-lg font-semibold">
903
+ {selectedRequest.collaboratorName}
904
+ </div>
905
+ <div className="mt-1 text-sm text-muted-foreground">
906
+ {getRequestScopeLabel(selectedRequest.requestScope)}
907
+ </div>
908
+ </div>
909
+ <StatusBadge
910
+ label={getStatusLabel(selectedRequest.status)}
911
+ className={getStatusBadgeClass(selectedRequest.status)}
912
+ />
913
+ </div>
914
+ </div>
915
+
916
+ <div className="grid gap-4 sm:grid-cols-2">
917
+ <div>
918
+ <div className="text-xs font-medium uppercase text-muted-foreground">
919
+ {commonT('labels.timeline')}
920
+ </div>
921
+ <div className="mt-1 text-sm">
922
+ {formatDateRange(
923
+ selectedRequest.effectiveStartDate,
924
+ selectedRequest.effectiveEndDate
925
+ )}
926
+ </div>
927
+ </div>
928
+ <div>
929
+ <div className="text-xs font-medium uppercase text-muted-foreground">
930
+ {commonT('labels.approver')}
931
+ </div>
932
+ <div className="mt-1 text-sm">
933
+ {selectedRequest.approverName ||
934
+ commonT('labels.notAssigned')}
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <div className="grid gap-4 sm:grid-cols-2">
940
+ <div>
941
+ <div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
942
+ {t('table.currentSchedule')}
943
+ </div>
944
+ <div className="rounded-lg border p-3">
945
+ <SchedulePanel
946
+ days={selectedRequest.currentSchedule ?? []}
947
+ locale={currentLocaleCode}
948
+ emptyLabel={commonT('labels.notAssigned')}
949
+ />
950
+ </div>
951
+ </div>
952
+ <div>
953
+ <div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
954
+ {t('table.requestedSchedule')}
955
+ </div>
956
+ <div className="rounded-lg border p-3">
957
+ <SchedulePanel
958
+ days={
959
+ (selectedRequest.days ??
960
+ []) as OperationsScheduleAdjustmentDay[]
961
+ }
962
+ locale={currentLocaleCode}
963
+ emptyLabel={commonT('labels.notAssigned')}
964
+ />
965
+ </div>
966
+ </div>
967
+ </div>
968
+
969
+ <div>
970
+ <div className="text-xs font-medium uppercase text-muted-foreground">
971
+ {commonT('labels.reason')}
972
+ </div>
973
+ <div className="mt-1 rounded-lg border p-3 text-sm">
974
+ {selectedRequest.reason || commonT('labels.noNotes')}
975
+ </div>
976
+ </div>
977
+
978
+ <div>
979
+ <div className="text-xs font-medium uppercase text-muted-foreground">
980
+ {commonT('labels.notes')}
981
+ </div>
982
+ <div className="mt-1 rounded-lg border p-3 text-sm">
983
+ {selectedRequest.approverNote || commonT('labels.noNotes')}
984
+ </div>
985
+ </div>
986
+ </div>
987
+ ) : null}
988
+ </SheetContent>
989
+ </Sheet>
562
990
  </Page>
563
991
  );
564
992
  }