@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,7 +1,13 @@
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';
4
9
  import { Button } from '@/components/ui/button';
10
+ import { Card, CardContent } from '@/components/ui/card';
5
11
  import { FormActions } from '@/components/ui/form-actions';
6
12
  import { Input } from '@/components/ui/input';
7
13
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
@@ -28,15 +34,19 @@ import {
28
34
  TableRow,
29
35
  } from '@/components/ui/table';
30
36
  import { Textarea } from '@/components/ui/textarea';
37
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
31
38
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
32
- import { Palmtree, Plus } from 'lucide-react';
39
+ import { Eye, LayoutGrid, List, Palmtree, Plus } from 'lucide-react';
33
40
  import { useTranslations } from 'next-intl';
34
41
  import { useMemo, useState } from 'react';
35
42
  import { OperationsHeader } from '../_components/operations-header';
36
43
  import { StatusBadge } from '../_components/status-badge';
37
44
  import { fetchOperations, mutateOperations } from '../_lib/api';
38
45
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
39
- import type { OperationsTimeOffRequest } from '../_lib/types';
46
+ import type {
47
+ OperationsTimeOffRequest,
48
+ PaginatedResponse,
49
+ } from '../_lib/types';
40
50
  import {
41
51
  formatDateRange,
42
52
  formatEnumLabel,
@@ -48,7 +58,6 @@ type TimeOffFormState = {
48
58
  requestType: string;
49
59
  startDate: string;
50
60
  endDate: string;
51
- totalDays: string;
52
61
  reason: string;
53
62
  };
54
63
 
@@ -56,10 +65,39 @@ const emptyForm: TimeOffFormState = {
56
65
  requestType: 'vacation',
57
66
  startDate: '',
58
67
  endDate: '',
59
- totalDays: '',
60
68
  reason: '',
61
69
  };
62
70
 
71
+ function calculateTotalDays(startDate?: string, endDate?: string) {
72
+ if (!startDate || !endDate) return '';
73
+
74
+ const start = new Date(`${startDate}T00:00:00`);
75
+ const end = new Date(`${endDate}T00:00:00`);
76
+
77
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
78
+ return '';
79
+ }
80
+
81
+ const diffInMs = end.getTime() - start.getTime();
82
+ if (diffInMs < 0) return '';
83
+
84
+ return String(Math.floor(diffInMs / 86400000) + 1);
85
+ }
86
+
87
+ function formatDateLabel(value?: string | null) {
88
+ if (!value) return '—';
89
+
90
+ const normalizedValue = value.includes('T') ? value : `${value}T00:00:00`;
91
+ const date = new Date(normalizedValue);
92
+ if (Number.isNaN(date.getTime())) return value;
93
+
94
+ return new Intl.DateTimeFormat(undefined, {
95
+ day: '2-digit',
96
+ month: 'short',
97
+ year: 'numeric',
98
+ }).format(date);
99
+ }
100
+
63
101
  export default function OperationsTimeOffPage() {
64
102
  const t = useTranslations('operations.TimeOffPage');
65
103
  const commonT = useTranslations('operations.Common');
@@ -67,8 +105,22 @@ export default function OperationsTimeOffPage() {
67
105
  const access = useOperationsAccess();
68
106
  const [search, setSearch] = useState('');
69
107
  const [statusFilter, setStatusFilter] = useState('all');
108
+ const [page, setPage] = useState(1);
109
+ const [pageSize, setPageSize] = useState(12);
110
+ const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
111
+ if (typeof window === 'undefined') return 'table';
112
+ const saved = window.localStorage.getItem('operations-time-off-view-mode');
113
+ return saved === 'cards' ? 'cards' : 'table';
114
+ });
70
115
  const [isSheetOpen, setIsSheetOpen] = useState(false);
116
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
117
+ const [selectedRequest, setSelectedRequest] =
118
+ useState<OperationsTimeOffRequest | null>(null);
71
119
  const [form, setForm] = useState<TimeOffFormState>(emptyForm);
120
+ const calculatedTotalDays = useMemo(
121
+ () => calculateTotalDays(form.startDate, form.endDate),
122
+ [form.endDate, form.startDate]
123
+ );
72
124
 
73
125
  const getRequestTypeLabel = (value?: string | null) => {
74
126
  if (!value) {
@@ -97,41 +149,42 @@ export default function OperationsTimeOffPage() {
97
149
  return t.has(key) ? t(key) : null;
98
150
  };
99
151
 
100
- const { data: requests = [], refetch } = useQuery<OperationsTimeOffRequest[]>(
101
- {
102
- queryKey: ['operations-time-off', currentLocaleCode],
103
- enabled: access.isCollaborator,
104
- queryFn: () =>
105
- fetchOperations<OperationsTimeOffRequest[]>(
106
- request,
107
- '/operations/time-off'
108
- ),
109
- }
110
- );
152
+ const { data: requestsResponse, refetch } = useQuery<
153
+ PaginatedResponse<OperationsTimeOffRequest>
154
+ >({
155
+ queryKey: [
156
+ 'operations-time-off',
157
+ currentLocaleCode,
158
+ search,
159
+ statusFilter,
160
+ page,
161
+ pageSize,
162
+ ],
163
+ enabled: access.isCollaborator,
164
+ queryFn: () => {
165
+ const params = new URLSearchParams({
166
+ page: String(page),
167
+ pageSize: String(pageSize),
168
+ });
169
+ if (search.trim()) params.set('search', search.trim());
170
+ if (statusFilter !== 'all') params.set('status', statusFilter);
171
+ return fetchOperations<PaginatedResponse<OperationsTimeOffRequest>>(
172
+ request,
173
+ `/operations/time-off?${params.toString()}`
174
+ );
175
+ },
176
+ placeholderData: (previous) => previous,
177
+ });
111
178
 
112
- const filteredRows = useMemo(
113
- () =>
114
- requests.filter((item) => {
115
- const matchesSearch = !search.trim()
116
- ? true
117
- : [
118
- item.collaboratorName,
119
- item.approverName,
120
- item.reason,
121
- item.requestType,
122
- ]
123
- .filter(Boolean)
124
- .some((value) =>
125
- String(value)
126
- .toLowerCase()
127
- .includes(search.trim().toLowerCase())
128
- );
129
- const matchesStatus =
130
- statusFilter === 'all' ? true : item.status === statusFilter;
131
- return matchesSearch && matchesStatus;
132
- }),
133
- [requests, search, statusFilter]
134
- );
179
+ const requests = requestsResponse?.data ?? [];
180
+
181
+ const handleViewModeChange = (value: string) => {
182
+ if (value !== 'table' && value !== 'cards') return;
183
+ setViewMode(value as 'table' | 'cards');
184
+ if (typeof window !== 'undefined') {
185
+ window.localStorage.setItem('operations-time-off-view-mode', value);
186
+ }
187
+ };
135
188
 
136
189
  const cards = [
137
190
  {
@@ -168,7 +221,7 @@ export default function OperationsTimeOffPage() {
168
221
  requestType: form.requestType,
169
222
  startDate: form.startDate,
170
223
  endDate: form.endDate,
171
- totalDays: parseNumberInput(form.totalDays),
224
+ totalDays: parseNumberInput(calculatedTotalDays),
172
225
  reason: trimToNull(form.reason),
173
226
  });
174
227
 
@@ -181,6 +234,11 @@ export default function OperationsTimeOffPage() {
181
234
  }
182
235
  };
183
236
 
237
+ const openDetails = (requestItem: OperationsTimeOffRequest) => {
238
+ setSelectedRequest(requestItem);
239
+ setIsDetailsOpen(true);
240
+ };
241
+
184
242
  return (
185
243
  <Page>
186
244
  <OperationsHeader
@@ -199,87 +257,200 @@ export default function OperationsTimeOffPage() {
199
257
 
200
258
  <KpiCardsGrid items={cards} columns={3} />
201
259
 
202
- <SearchBar
203
- searchQuery={search}
204
- onSearchChange={setSearch}
205
- onSearch={() => undefined}
206
- placeholder={t('searchPlaceholder')}
207
- controls={[
208
- {
209
- id: 'status',
210
- type: 'select',
211
- value: statusFilter,
212
- onChange: setStatusFilter,
213
- placeholder: commonT('labels.status'),
214
- options: [
215
- { value: 'all', label: commonT('filters.allStatuses') },
216
- { value: 'submitted', label: getStatusLabel('submitted') },
217
- { value: 'approved', label: getStatusLabel('approved') },
218
- { value: 'rejected', label: getStatusLabel('rejected') },
219
- ],
220
- },
221
- ]}
222
- />
260
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
261
+ <div className="flex-1">
262
+ <SearchBar
263
+ searchQuery={search}
264
+ onSearchChange={(value) => {
265
+ setSearch(value);
266
+ setPage(1);
267
+ }}
268
+ showSearchButton={false}
269
+ debounceMs={500}
270
+ placeholder={t('searchPlaceholder')}
271
+ controls={[
272
+ {
273
+ id: 'status',
274
+ type: 'select',
275
+ value: statusFilter,
276
+ onChange: (value) => {
277
+ setStatusFilter(value);
278
+ setPage(1);
279
+ },
280
+ placeholder: commonT('labels.status'),
281
+ options: [
282
+ { value: 'all', label: commonT('filters.allStatuses') },
283
+ { value: 'submitted', label: getStatusLabel('submitted') },
284
+ { value: 'approved', label: getStatusLabel('approved') },
285
+ { value: 'rejected', label: getStatusLabel('rejected') },
286
+ ],
287
+ },
288
+ ]}
289
+ />
290
+ </div>
223
291
 
224
- {filteredRows.length > 0 ? (
225
- <div className="overflow-x-auto rounded-md border">
226
- <Table>
227
- <TableHeader>
228
- <TableRow>
229
- <TableHead>{commonT('labels.collaborator')}</TableHead>
230
- <TableHead>{commonT('labels.requestType')}</TableHead>
231
- <TableHead>{commonT('labels.timeline')}</TableHead>
232
- <TableHead>{commonT('labels.approver')}</TableHead>
233
- <TableHead>{commonT('labels.status')}</TableHead>
234
- <TableHead>{commonT('labels.reason')}</TableHead>
235
- <TableHead>{commonT('labels.notes')}</TableHead>
236
- </TableRow>
237
- </TableHeader>
238
- <TableBody>
239
- {filteredRows.map((requestItem) => (
240
- <TableRow key={requestItem.id} className="hover:bg-muted/30">
241
- <TableCell>{requestItem.collaboratorName}</TableCell>
242
- <TableCell>
243
- {getRequestTypeLabel(requestItem.requestType)}
244
- </TableCell>
245
- <TableCell>
246
- <div>
247
- {formatDateRange(
248
- requestItem.startDate,
249
- requestItem.endDate
250
- )}
292
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
293
+ <span className="text-xs font-medium text-muted-foreground">
294
+ {t('viewMode')}
295
+ </span>
296
+ <ToggleGroup
297
+ type="single"
298
+ value={viewMode}
299
+ onValueChange={handleViewModeChange}
300
+ variant="outline"
301
+ size="sm"
302
+ >
303
+ <ToggleGroupItem value="table" className="gap-1.5 px-2.5">
304
+ <List className="h-4 w-4" />
305
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
306
+ </ToggleGroupItem>
307
+ <ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
308
+ <LayoutGrid className="h-4 w-4" />
309
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
310
+ </ToggleGroupItem>
311
+ </ToggleGroup>
312
+ </div>
313
+ </div>
314
+
315
+ {requests.length > 0 ? (
316
+ viewMode === 'cards' ? (
317
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
318
+ {requests.map((requestItem) => (
319
+ <Card
320
+ key={requestItem.id}
321
+ className="overflow-hidden border-border/60 py-0 shadow-sm"
322
+ >
323
+ <CardContent className="space-y-3 p-4">
324
+ <div className="flex items-start justify-between gap-3">
325
+ <div className="min-w-0">
326
+ <div className="truncate font-semibold">
327
+ {requestItem.collaboratorName}
328
+ </div>
329
+ <div className="truncate text-xs text-muted-foreground">
330
+ {getRequestTypeLabel(requestItem.requestType)} &bull;{' '}
331
+ {formatDateRange(
332
+ requestItem.startDate,
333
+ requestItem.endDate
334
+ )}
335
+ </div>
251
336
  </div>
252
- <div className="text-xs text-muted-foreground">
253
- {requestItem.totalDays ?? 0} {commonT('labels.days')}
337
+ <StatusBadge
338
+ label={getStatusLabel(requestItem.status)}
339
+ className={getStatusBadgeClass(requestItem.status)}
340
+ />
341
+ </div>
342
+ <div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
343
+ <div>
344
+ <span className="font-medium text-foreground">
345
+ {commonT('labels.days')}:
346
+ </span>{' '}
347
+ {requestItem.totalDays ?? 0}
254
348
  </div>
255
- </TableCell>
256
- <TableCell>
257
- {requestItem.approverName || commonT('labels.notAssigned')}
258
- </TableCell>
259
- <TableCell>
260
- <div className="space-y-1">
261
- <StatusBadge
262
- label={getStatusLabel(requestItem.status)}
263
- className={getStatusBadgeClass(requestItem.status)}
264
- />
265
- {getStatusDescription(requestItem.status) ? (
266
- <p className="max-w-40 text-xs text-muted-foreground">
267
- {getStatusDescription(requestItem.status)}
268
- </p>
269
- ) : null}
349
+ <div>
350
+ <span className="font-medium text-foreground">
351
+ {commonT('labels.approver')}:
352
+ </span>{' '}
353
+ {requestItem.approverName ||
354
+ commonT('labels.notAssigned')}
270
355
  </div>
271
- </TableCell>
272
- <TableCell>
273
- {requestItem.reason || commonT('labels.noNotes')}
274
- </TableCell>
275
- <TableCell>
276
- {requestItem.approverNote || commonT('labels.noNotes')}
277
- </TableCell>
356
+ </div>
357
+ {requestItem.reason ? (
358
+ <p className="line-clamp-2 text-sm text-muted-foreground">
359
+ {requestItem.reason}
360
+ </p>
361
+ ) : null}
362
+ <div className="flex justify-end border-t border-border/60 pt-3">
363
+ <Button
364
+ variant="outline"
365
+ size="sm"
366
+ className="cursor-pointer"
367
+ onClick={() => openDetails(requestItem)}
368
+ >
369
+ <Eye className="size-4" />
370
+ {t('actions.viewDetails')}
371
+ </Button>
372
+ </div>
373
+ </CardContent>
374
+ </Card>
375
+ ))}
376
+ </div>
377
+ ) : (
378
+ <div className="overflow-x-auto rounded-md border">
379
+ <Table>
380
+ <TableHeader>
381
+ <TableRow>
382
+ <TableHead>{commonT('labels.collaborator')}</TableHead>
383
+ <TableHead>{commonT('labels.requestType')}</TableHead>
384
+ <TableHead>{commonT('labels.timeline')}</TableHead>
385
+ <TableHead>{commonT('labels.approver')}</TableHead>
386
+ <TableHead>{commonT('labels.status')}</TableHead>
387
+ <TableHead>{commonT('labels.reason')}</TableHead>
388
+ <TableHead>{commonT('labels.notes')}</TableHead>
389
+ <TableHead className="text-right">
390
+ {commonT('labels.actions')}
391
+ </TableHead>
278
392
  </TableRow>
279
- ))}
280
- </TableBody>
281
- </Table>
282
- </div>
393
+ </TableHeader>
394
+ <TableBody>
395
+ {requests.map((requestItem) => (
396
+ <TableRow key={requestItem.id} className="hover:bg-muted/30">
397
+ <TableCell>{requestItem.collaboratorName}</TableCell>
398
+ <TableCell>
399
+ {getRequestTypeLabel(requestItem.requestType)}
400
+ </TableCell>
401
+ <TableCell>
402
+ <div>
403
+ {formatDateRange(
404
+ requestItem.startDate,
405
+ requestItem.endDate
406
+ )}
407
+ </div>
408
+ <div className="text-xs text-muted-foreground">
409
+ {requestItem.totalDays ?? 0} {commonT('labels.days')}
410
+ </div>
411
+ </TableCell>
412
+ <TableCell>
413
+ {requestItem.approverName ||
414
+ commonT('labels.notAssigned')}
415
+ </TableCell>
416
+ <TableCell>
417
+ <div className="space-y-1">
418
+ <StatusBadge
419
+ label={getStatusLabel(requestItem.status)}
420
+ className={getStatusBadgeClass(requestItem.status)}
421
+ />
422
+ {getStatusDescription(requestItem.status) ? (
423
+ <p className="max-w-40 text-xs text-muted-foreground">
424
+ {getStatusDescription(requestItem.status)}
425
+ </p>
426
+ ) : null}
427
+ </div>
428
+ </TableCell>
429
+ <TableCell>
430
+ {requestItem.reason || commonT('labels.noNotes')}
431
+ </TableCell>
432
+ <TableCell>
433
+ {requestItem.approverNote || commonT('labels.noNotes')}
434
+ </TableCell>
435
+ <TableCell>
436
+ <div className="flex justify-end">
437
+ <Button
438
+ variant="outline"
439
+ size="sm"
440
+ className="cursor-pointer"
441
+ onClick={() => openDetails(requestItem)}
442
+ >
443
+ <Eye className="size-4" />
444
+ {t('actions.viewDetails')}
445
+ </Button>
446
+ </div>
447
+ </TableCell>
448
+ </TableRow>
449
+ ))}
450
+ </TableBody>
451
+ </Table>
452
+ </div>
453
+ )
283
454
  ) : (
284
455
  <EmptyState
285
456
  icon={<Palmtree className="size-12" />}
@@ -298,6 +469,18 @@ export default function OperationsTimeOffPage() {
298
469
  />
299
470
  )}
300
471
 
472
+ <PaginationFooter
473
+ currentPage={page}
474
+ pageSize={pageSize}
475
+ totalItems={requestsResponse?.total ?? 0}
476
+ pageSizeOptions={[12, 24, 48]}
477
+ onPageChange={setPage}
478
+ onPageSizeChange={(size) => {
479
+ setPageSize(size);
480
+ setPage(1);
481
+ }}
482
+ />
483
+
301
484
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
302
485
  <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
303
486
  <SheetHeader>
@@ -388,13 +571,9 @@ export default function OperationsTimeOffPage() {
388
571
  <Input
389
572
  type="number"
390
573
  step="0.5"
391
- value={form.totalDays}
392
- onChange={(event) =>
393
- setForm((current) => ({
394
- ...current,
395
- totalDays: event.target.value,
396
- }))
397
- }
574
+ value={calculatedTotalDays}
575
+ readOnly
576
+ className="bg-muted/40"
398
577
  />
399
578
  </div>
400
579
 
@@ -425,6 +604,104 @@ export default function OperationsTimeOffPage() {
425
604
  />
426
605
  </SheetContent>
427
606
  </Sheet>
607
+
608
+ <Sheet open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
609
+ <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
610
+ <SheetHeader>
611
+ <SheetTitle>{t('details.title')}</SheetTitle>
612
+ <SheetDescription>{t('details.description')}</SheetDescription>
613
+ </SheetHeader>
614
+
615
+ {selectedRequest ? (
616
+ <div className="mt-6 space-y-5 px-4 pb-8">
617
+ <div className="rounded-lg border bg-muted/20 p-4">
618
+ <div className="text-lg font-semibold">
619
+ {selectedRequest.collaboratorName}
620
+ </div>
621
+ <div className="mt-1 text-sm text-muted-foreground">
622
+ {getRequestTypeLabel(selectedRequest.requestType)}
623
+ </div>
624
+ </div>
625
+
626
+ <div className="grid gap-4 sm:grid-cols-2">
627
+ <div>
628
+ <div className="text-xs font-medium uppercase text-muted-foreground">
629
+ {commonT('labels.startDate')}
630
+ </div>
631
+ <div className="mt-1 text-sm">
632
+ {formatDateLabel(selectedRequest.startDate)}
633
+ </div>
634
+ </div>
635
+ <div>
636
+ <div className="text-xs font-medium uppercase text-muted-foreground">
637
+ {commonT('labels.endDate')}
638
+ </div>
639
+ <div className="mt-1 text-sm">
640
+ {formatDateLabel(selectedRequest.endDate)}
641
+ </div>
642
+ </div>
643
+ <div>
644
+ <div className="text-xs font-medium uppercase text-muted-foreground">
645
+ {commonT('labels.days')}
646
+ </div>
647
+ <div className="mt-1 text-sm">
648
+ {selectedRequest.totalDays ?? 0}
649
+ </div>
650
+ </div>
651
+ <div>
652
+ <div className="text-xs font-medium uppercase text-muted-foreground">
653
+ {commonT('labels.status')}
654
+ </div>
655
+ <div className="mt-1">
656
+ <StatusBadge
657
+ label={getStatusLabel(selectedRequest.status)}
658
+ className={getStatusBadgeClass(selectedRequest.status)}
659
+ />
660
+ </div>
661
+ </div>
662
+ <div>
663
+ <div className="text-xs font-medium uppercase text-muted-foreground">
664
+ {commonT('labels.approver')}
665
+ </div>
666
+ <div className="mt-1 text-sm">
667
+ {selectedRequest.approverName ||
668
+ commonT('labels.notAssigned')}
669
+ </div>
670
+ </div>
671
+ <div>
672
+ <div className="text-xs font-medium uppercase text-muted-foreground">
673
+ {t('details.period')}
674
+ </div>
675
+ <div className="mt-1 text-sm">
676
+ {formatDateRange(
677
+ selectedRequest.startDate,
678
+ selectedRequest.endDate
679
+ )}
680
+ </div>
681
+ </div>
682
+ </div>
683
+
684
+ <div>
685
+ <div className="text-xs font-medium uppercase text-muted-foreground">
686
+ {commonT('labels.reason')}
687
+ </div>
688
+ <div className="mt-1 rounded-lg border p-3 text-sm">
689
+ {selectedRequest.reason || commonT('labels.noNotes')}
690
+ </div>
691
+ </div>
692
+
693
+ <div>
694
+ <div className="text-xs font-medium uppercase text-muted-foreground">
695
+ {commonT('labels.notes')}
696
+ </div>
697
+ <div className="mt-1 rounded-lg border p-3 text-sm">
698
+ {selectedRequest.approverNote || commonT('labels.noNotes')}
699
+ </div>
700
+ </div>
701
+ </div>
702
+ ) : null}
703
+ </SheetContent>
704
+ </Sheet>
428
705
  </Page>
429
706
  );
430
707
  }