@hed-hog/operations 0.0.306 → 0.0.310
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-approvals.controller.d.ts +114 -1
- package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
- package/dist/controllers/operations-approvals.controller.js +16 -3
- package/dist/controllers/operations-approvals.controller.js.map +1 -1
- package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +16 -3
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +14 -453
- package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
- package/dist/controllers/operations-contracts.controller.js +11 -112
- package/dist/controllers/operations-contracts.controller.js.map +1 -1
- package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
- package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
- package/dist/controllers/operations-org-structure.controller.js +18 -5
- package/dist/controllers/operations-org-structure.controller.js.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +28 -4
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +17 -5
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +16 -11
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/list-approvals.dto.d.ts +6 -0
- package/dist/dto/list-approvals.dto.d.ts.map +1 -0
- package/dist/dto/list-approvals.dto.js +28 -0
- package/dist/dto/list-approvals.dto.js.map +1 -0
- package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
- package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborator-types.dto.js +7 -1
- package/dist/dto/list-collaborator-types.dto.js.map +1 -1
- package/dist/dto/list-collaborators.dto.d.ts +1 -0
- package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborators.dto.js +5 -0
- package/dist/dto/list-collaborators.dto.js.map +1 -1
- package/dist/dto/list-contracts.dto.d.ts +8 -0
- package/dist/dto/list-contracts.dto.d.ts.map +1 -0
- package/dist/dto/list-contracts.dto.js +38 -0
- package/dist/dto/list-contracts.dto.js.map +1 -0
- package/dist/dto/list-departments.dto.d.ts +5 -0
- package/dist/dto/list-departments.dto.d.ts.map +1 -0
- package/dist/dto/list-departments.dto.js +23 -0
- package/dist/dto/list-departments.dto.js.map +1 -0
- package/dist/dto/list-projects.dto.d.ts +5 -0
- package/dist/dto/list-projects.dto.d.ts.map +1 -0
- package/dist/dto/list-projects.dto.js +23 -0
- package/dist/dto/list-projects.dto.js.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.js +23 -0
- package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
- package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
- package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
- package/dist/dto/list-time-off-requests.dto.js +23 -0
- package/dist/dto/list-time-off-requests.dto.js.map +1 -0
- package/dist/dto/list-timesheets.dto.d.ts +5 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
- package/dist/dto/list-timesheets.dto.js +23 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.js +25 -0
- package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
- package/dist/operations.service.d.ts +340 -271
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1007 -1043
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +0 -22
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +0 -36
- package/hedhog/data/route.yaml +42 -73
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
- package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
- package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
- package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
- package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
- package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
- package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
- package/hedhog/frontend/messages/en.json +143 -14
- package/hedhog/frontend/messages/pt.json +192 -54
- package/hedhog/table/operations_contract.yaml +0 -9
- package/package.json +4 -4
- package/src/controllers/operations-approvals.controller.ts +9 -3
- package/src/controllers/operations-collaborators.controller.ts +15 -2
- package/src/controllers/operations-contracts.controller.ts +8 -92
- package/src/controllers/operations-org-structure.controller.ts +17 -4
- package/src/controllers/operations-projects.controller.ts +10 -4
- package/src/controllers/operations-timesheets.controller.ts +17 -8
- package/src/dto/list-approvals.dto.ts +12 -0
- package/src/dto/list-collaborator-types.dto.ts +7 -2
- package/src/dto/list-collaborators.dto.ts +4 -0
- package/src/dto/list-contracts.dto.ts +20 -0
- package/src/dto/list-departments.dto.ts +8 -0
- package/src/dto/list-projects.dto.ts +8 -0
- package/src/dto/list-schedule-adjustments.dto.ts +8 -0
- package/src/dto/list-time-off-requests.dto.ts +8 -0
- package/src/dto/list-timesheets.dto.ts +8 -0
- package/src/dto/reorder-collaborator-types.dto.ts +10 -0
- package/src/operations.service.spec.ts +0 -30
- package/src/operations.service.ts +1557 -1806
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
- package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
- package/hedhog/table/operations_contract_financial_term.yaml +0 -40
- package/hedhog/table/operations_contract_revision.yaml +0 -38
- package/hedhog/table/operations_contract_signature.yaml +0 -38
- package/hedhog/table/operations_contract_template.yaml +0 -58
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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)} •{' '}
|
|
331
|
+
{formatDateRange(
|
|
332
|
+
requestItem.startDate,
|
|
333
|
+
requestItem.endDate
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
251
336
|
</div>
|
|
252
|
-
<
|
|
253
|
-
{requestItem.
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
</
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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={
|
|
392
|
-
|
|
393
|
-
|
|
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
|
}
|