@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.
- 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 +52 -4
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +28 -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/dto/update-collaborator-type.dto.d.ts +3 -1
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
- package/dist/dto/update-collaborator-type.dto.js +2 -1
- package/dist/dto/update-collaborator-type.dto.js.map +1 -1
- package/dist/operations.service.d.ts +362 -271
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1195 -1098
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +73 -22
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +19 -55
- package/hedhog/data/operations_collaborator_type.yaml +76 -76
- package/hedhog/data/route.yaml +52 -70
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- 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/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- 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 +843 -151
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +457 -154
- 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 +546 -118
- package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +647 -342
- package/hedhog/frontend/messages/en.json +148 -14
- package/hedhog/frontend/messages/pt.json +199 -56
- package/hedhog/table/operations_collaborator.yaml +18 -18
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
- package/hedhog/table/operations_collaborator_type.yaml +33 -33
- package/hedhog/table/operations_contract.yaml +0 -9
- package/hedhog/table/operations_contract_document.yaml +33 -33
- 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 +30 -8
- package/src/dto/create-collaborator-type.dto.ts +43 -43
- package/src/dto/create-collaborator.dto.ts +223 -223
- package/src/dto/list-approvals.dto.ts +12 -0
- package/src/dto/list-collaborator-types.dto.ts +20 -15
- package/src/dto/list-collaborators.dto.ts +34 -30
- 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/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/operations.service.spec.ts +96 -30
- package/src/operations.service.ts +1738 -1777
- 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,6 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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:
|
|
129
|
-
OperationsScheduleAdjustmentRequest
|
|
279
|
+
const { data: requestsResponse, refetch } = useQuery<
|
|
280
|
+
PaginatedResponse<OperationsScheduleAdjustmentRequest>
|
|
130
281
|
>({
|
|
131
|
-
queryKey: [
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
286
|
-
<div className="
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
}
|