@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,11 @@
|
|
|
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 {
|
|
5
10
|
AlertDialog,
|
|
6
11
|
AlertDialogAction,
|
|
@@ -12,13 +17,6 @@ import {
|
|
|
12
17
|
AlertDialogTitle,
|
|
13
18
|
} from '@/components/ui/alert-dialog';
|
|
14
19
|
import { Button } from '@/components/ui/button';
|
|
15
|
-
import {
|
|
16
|
-
Card,
|
|
17
|
-
CardContent,
|
|
18
|
-
CardDescription,
|
|
19
|
-
CardHeader,
|
|
20
|
-
CardTitle,
|
|
21
|
-
} from '@/components/ui/card';
|
|
22
20
|
import {
|
|
23
21
|
Form,
|
|
24
22
|
FormControl,
|
|
@@ -53,14 +51,15 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
53
51
|
import {
|
|
54
52
|
ClipboardList,
|
|
55
53
|
Clock3,
|
|
54
|
+
Eye,
|
|
55
|
+
LayoutGrid,
|
|
56
|
+
List,
|
|
56
57
|
Loader2,
|
|
57
58
|
Plus,
|
|
58
59
|
Send,
|
|
59
|
-
Sparkles,
|
|
60
|
-
Trash2,
|
|
61
60
|
} from 'lucide-react';
|
|
62
61
|
import { useTranslations } from 'next-intl';
|
|
63
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
62
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
64
63
|
import { useForm, useWatch } from 'react-hook-form';
|
|
65
64
|
import { z } from 'zod';
|
|
66
65
|
|
|
@@ -99,7 +98,10 @@ type QuickEntryFormValues = {
|
|
|
99
98
|
description: string;
|
|
100
99
|
};
|
|
101
100
|
|
|
102
|
-
const getTodayDate = () =>
|
|
101
|
+
const getTodayDate = () => {
|
|
102
|
+
const now = new Date();
|
|
103
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
104
|
+
};
|
|
103
105
|
|
|
104
106
|
const buildDefaultFormValues = (): QuickEntryFormValues => ({
|
|
105
107
|
projectId: undefined,
|
|
@@ -145,7 +147,13 @@ function formatDateLabel(value?: string | null) {
|
|
|
145
147
|
return '—';
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
// Always extract just the YYYY-MM-DD part to avoid UTC offset shifting the date
|
|
151
|
+
const match = String(value).match(/(\d{4}-\d{2}-\d{2})/);
|
|
152
|
+
if (!match) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const date = new Date(`${match[1]}T12:00:00`);
|
|
149
157
|
|
|
150
158
|
if (Number.isNaN(date.getTime())) {
|
|
151
159
|
return value;
|
|
@@ -202,11 +210,25 @@ export default function OperationsTimesheetsPage() {
|
|
|
202
210
|
const access = useOperationsAccess();
|
|
203
211
|
const [search, setSearch] = useState('');
|
|
204
212
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
213
|
+
const [page, setPage] = useState(1);
|
|
214
|
+
const [pageSize, setPageSize] = useState(12);
|
|
215
|
+
const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
|
|
216
|
+
if (typeof window === 'undefined') return 'table';
|
|
217
|
+
const saved = window.localStorage.getItem(
|
|
218
|
+
'operations-timesheets-view-mode'
|
|
219
|
+
);
|
|
220
|
+
return saved === 'cards' ? 'cards' : 'table';
|
|
221
|
+
});
|
|
205
222
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
223
|
+
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
|
206
224
|
const [isTaskCreateSheetOpen, setIsTaskCreateSheetOpen] = useState(false);
|
|
207
225
|
const [keepContextOnSave, setKeepContextOnSave] = useState(true);
|
|
226
|
+
const [editingEntry, setEditingEntry] =
|
|
227
|
+
useState<OperationsTimesheetEntry | null>(null);
|
|
208
228
|
const [entryToDelete, setEntryToDelete] =
|
|
209
229
|
useState<OperationsTimesheetEntry | null>(null);
|
|
230
|
+
const [selectedTimesheet, setSelectedTimesheet] =
|
|
231
|
+
useState<OperationsTimesheet | null>(null);
|
|
210
232
|
const [isDeletingEntry, setIsDeletingEntry] = useState(false);
|
|
211
233
|
const [projectQuery, setProjectQuery] = useState({ search: '', page: 1 });
|
|
212
234
|
const [projectOptions, setProjectOptions] = useState<
|
|
@@ -215,6 +237,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
215
237
|
const [taskQuery, setTaskQuery] = useState({ search: '', page: 1 });
|
|
216
238
|
const [taskRefreshKey, setTaskRefreshKey] = useState(0);
|
|
217
239
|
const [taskOptions, setTaskOptions] = useState<OperationsTaskOption[]>([]);
|
|
240
|
+
const previousProjectIdRef = useRef<number | undefined>(undefined);
|
|
218
241
|
|
|
219
242
|
const quickEntrySchema = useMemo(
|
|
220
243
|
() =>
|
|
@@ -254,13 +277,42 @@ export default function OperationsTimesheetsPage() {
|
|
|
254
277
|
name: 'unit',
|
|
255
278
|
});
|
|
256
279
|
|
|
257
|
-
const { data:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
const { data: timesheetsResponse, refetch } = useQuery<
|
|
281
|
+
PaginatedResponse<OperationsTimesheet>
|
|
282
|
+
>({
|
|
283
|
+
queryKey: [
|
|
284
|
+
'operations-timesheets',
|
|
285
|
+
currentLocaleCode,
|
|
286
|
+
search,
|
|
287
|
+
statusFilter,
|
|
288
|
+
page,
|
|
289
|
+
pageSize,
|
|
290
|
+
],
|
|
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<PaginatedResponse<OperationsTimesheet>>(
|
|
299
|
+
request,
|
|
300
|
+
`/operations/timesheets?${params.toString()}`
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
placeholderData: (previous) => previous,
|
|
262
304
|
});
|
|
263
305
|
|
|
306
|
+
const timesheets = timesheetsResponse?.data ?? [];
|
|
307
|
+
|
|
308
|
+
const handleViewModeChange = (value: string) => {
|
|
309
|
+
if (value !== 'table' && value !== 'cards') return;
|
|
310
|
+
setViewMode(value as 'table' | 'cards');
|
|
311
|
+
if (typeof window !== 'undefined') {
|
|
312
|
+
window.localStorage.setItem('operations-timesheets-view-mode', value);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
264
316
|
const { data: me } = useQuery<OperationsCollaborator>({
|
|
265
317
|
queryKey: ['operations-timesheets-me', currentLocaleCode],
|
|
266
318
|
enabled: access.isCollaborator,
|
|
@@ -271,11 +323,9 @@ export default function OperationsTimesheetsPage() {
|
|
|
271
323
|
),
|
|
272
324
|
});
|
|
273
325
|
|
|
274
|
-
const {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
refetch: refetchRecentEntries,
|
|
278
|
-
} = useQuery<PaginatedResponse<OperationsTimesheetEntry>>({
|
|
326
|
+
const { refetch: refetchRecentEntries } = useQuery<
|
|
327
|
+
PaginatedResponse<OperationsTimesheetEntry>
|
|
328
|
+
>({
|
|
279
329
|
queryKey: [
|
|
280
330
|
'operations-timesheet-recent-entries',
|
|
281
331
|
currentLocaleCode,
|
|
@@ -307,66 +357,73 @@ export default function OperationsTimesheetsPage() {
|
|
|
307
357
|
placeholderData: (previous) => previous,
|
|
308
358
|
});
|
|
309
359
|
|
|
310
|
-
const {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
360
|
+
const {
|
|
361
|
+
data: projectOptionsResponse,
|
|
362
|
+
isLoading: isProjectOptionsLoading,
|
|
363
|
+
error: projectOptionsError,
|
|
364
|
+
} = useQuery<PaginatedResponse<OperationsProjectOption>>({
|
|
365
|
+
queryKey: [
|
|
366
|
+
'operations-timesheet-project-options',
|
|
367
|
+
currentLocaleCode,
|
|
368
|
+
projectQuery.search,
|
|
369
|
+
projectQuery.page,
|
|
370
|
+
],
|
|
371
|
+
enabled: access.isCollaborator && isSheetOpen,
|
|
372
|
+
queryFn: () => {
|
|
373
|
+
const params = new URLSearchParams({
|
|
374
|
+
page: String(projectQuery.page),
|
|
375
|
+
pageSize: '20',
|
|
376
|
+
});
|
|
328
377
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
);
|
|
333
|
-
},
|
|
334
|
-
placeholderData: (previous) => previous,
|
|
335
|
-
});
|
|
378
|
+
if (projectQuery.search.trim()) {
|
|
379
|
+
params.set('search', projectQuery.search.trim());
|
|
380
|
+
}
|
|
336
381
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
taskQuery.page,
|
|
345
|
-
taskRefreshKey,
|
|
346
|
-
],
|
|
347
|
-
enabled:
|
|
348
|
-
access.isCollaborator && isSheetOpen && Boolean(selectedProjectId),
|
|
349
|
-
queryFn: () => {
|
|
350
|
-
const params = new URLSearchParams({
|
|
351
|
-
page: String(taskQuery.page),
|
|
352
|
-
pageSize: '20',
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
if (taskQuery.search.trim()) {
|
|
356
|
-
params.set('search', taskQuery.search.trim());
|
|
357
|
-
}
|
|
382
|
+
return fetchOperations<PaginatedResponse<OperationsProjectOption>>(
|
|
383
|
+
request,
|
|
384
|
+
`/operations/projects/options?${params.toString()}`
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
placeholderData: (previous) => previous,
|
|
388
|
+
});
|
|
358
389
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
390
|
+
const selectedProject =
|
|
391
|
+
projectOptions.find((option) => option.id === selectedProjectId) ?? null;
|
|
362
392
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
393
|
+
const {
|
|
394
|
+
data: taskOptionsResponse,
|
|
395
|
+
isLoading: isTaskOptionsLoading,
|
|
396
|
+
error: taskOptionsError,
|
|
397
|
+
} = useQuery<PaginatedResponse<OperationsTaskOption>>({
|
|
398
|
+
queryKey: [
|
|
399
|
+
'operations-timesheet-task-options',
|
|
400
|
+
currentLocaleCode,
|
|
401
|
+
selectedProjectId,
|
|
402
|
+
taskQuery.search,
|
|
403
|
+
taskQuery.page,
|
|
404
|
+
taskRefreshKey,
|
|
405
|
+
],
|
|
406
|
+
enabled: access.isCollaborator && isSheetOpen && Boolean(selectedProjectId),
|
|
407
|
+
queryFn: () => {
|
|
408
|
+
const params = new URLSearchParams({
|
|
409
|
+
page: String(taskQuery.page),
|
|
410
|
+
pageSize: '20',
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (taskQuery.search.trim()) {
|
|
414
|
+
params.set('search', taskQuery.search.trim());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (selectedProjectId) {
|
|
418
|
+
params.set('projectId', String(selectedProjectId));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return fetchOperations<PaginatedResponse<OperationsTaskOption>>(
|
|
422
|
+
request,
|
|
423
|
+
`/operations/tasks?${params.toString()}`
|
|
424
|
+
);
|
|
425
|
+
},
|
|
426
|
+
});
|
|
370
427
|
|
|
371
428
|
useEffect(() => {
|
|
372
429
|
if (!projectOptionsResponse) {
|
|
@@ -375,10 +432,12 @@ export default function OperationsTimesheetsPage() {
|
|
|
375
432
|
|
|
376
433
|
setProjectOptions((current) =>
|
|
377
434
|
projectQuery.page === 1
|
|
378
|
-
?
|
|
435
|
+
? editingEntry
|
|
436
|
+
? appendUniqueById(current, projectOptionsResponse.data ?? [])
|
|
437
|
+
: (projectOptionsResponse.data ?? [])
|
|
379
438
|
: appendUniqueById(current, projectOptionsResponse.data ?? [])
|
|
380
439
|
);
|
|
381
|
-
}, [projectOptionsResponse, projectQuery.page]);
|
|
440
|
+
}, [editingEntry, projectOptionsResponse, projectQuery.page]);
|
|
382
441
|
|
|
383
442
|
useEffect(() => {
|
|
384
443
|
if (!selectedProjectId) {
|
|
@@ -392,45 +451,78 @@ export default function OperationsTimesheetsPage() {
|
|
|
392
451
|
|
|
393
452
|
setTaskOptions((current) =>
|
|
394
453
|
taskQuery.page === 1
|
|
395
|
-
?
|
|
396
|
-
|
|
454
|
+
? editingEntry
|
|
455
|
+
? appendUniqueById(
|
|
456
|
+
current,
|
|
457
|
+
(taskOptionsResponse.data ?? []).filter(
|
|
458
|
+
(option) => option.projectId === selectedProjectId
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
: (taskOptionsResponse.data ?? []).filter(
|
|
462
|
+
(option) => option.projectId === selectedProjectId
|
|
463
|
+
)
|
|
464
|
+
: appendUniqueById(
|
|
465
|
+
current,
|
|
466
|
+
(taskOptionsResponse.data ?? []).filter(
|
|
467
|
+
(option) => option.projectId === selectedProjectId
|
|
468
|
+
)
|
|
469
|
+
)
|
|
397
470
|
);
|
|
398
|
-
}, [selectedProjectId, taskOptionsResponse, taskQuery.page]);
|
|
471
|
+
}, [editingEntry, selectedProjectId, taskOptionsResponse, taskQuery.page]);
|
|
399
472
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
const previousProjectId = previousProjectIdRef.current;
|
|
475
|
+
previousProjectIdRef.current = selectedProjectId;
|
|
476
|
+
|
|
477
|
+
if (
|
|
478
|
+
previousProjectId === undefined ||
|
|
479
|
+
previousProjectId === selectedProjectId
|
|
480
|
+
) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setTaskOptions([]);
|
|
485
|
+
setTaskQuery({ search: '', page: 1 });
|
|
486
|
+
form.setValue('taskId', undefined, {
|
|
487
|
+
shouldDirty: true,
|
|
488
|
+
shouldTouch: true,
|
|
489
|
+
shouldValidate: false,
|
|
490
|
+
});
|
|
491
|
+
}, [form, selectedProjectId]);
|
|
492
|
+
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (!projectOptionsError) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
showToastHandler?.(
|
|
499
|
+
'error',
|
|
500
|
+
getOperationsErrorMessage(
|
|
501
|
+
projectOptionsError,
|
|
502
|
+
t('messages.projectLoadError')
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
}, [projectOptionsError, showToastHandler, t]);
|
|
506
|
+
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (!taskOptionsError) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
showToastHandler?.(
|
|
513
|
+
'error',
|
|
514
|
+
getOperationsErrorMessage(taskOptionsError, t('messages.taskLoadError'))
|
|
515
|
+
);
|
|
516
|
+
}, [showToastHandler, t, taskOptionsError]);
|
|
517
|
+
|
|
518
|
+
const filteredTaskOptions = selectedProjectId
|
|
519
|
+
? taskOptions.filter((option) => option.projectId === selectedProjectId)
|
|
520
|
+
: [];
|
|
403
521
|
const selectedTask =
|
|
404
|
-
|
|
522
|
+
filteredTaskOptions.find((option) => option.id === selectedTaskId) ?? null;
|
|
523
|
+
const isEditingEntry = Boolean(editingEntry);
|
|
405
524
|
|
|
406
|
-
const filteredRows =
|
|
407
|
-
() =>
|
|
408
|
-
timesheets.filter((item) => {
|
|
409
|
-
const matchesSearch = !search.trim()
|
|
410
|
-
? true
|
|
411
|
-
: [
|
|
412
|
-
item.collaboratorName,
|
|
413
|
-
item.approverName,
|
|
414
|
-
item.notes,
|
|
415
|
-
...((item.entries ?? []).flatMap((entry) => [
|
|
416
|
-
entry.projectName,
|
|
417
|
-
entry.taskName,
|
|
418
|
-
entry.activityLabel,
|
|
419
|
-
entry.description,
|
|
420
|
-
]) as Array<string | undefined>),
|
|
421
|
-
]
|
|
422
|
-
.filter(Boolean)
|
|
423
|
-
.some((value) =>
|
|
424
|
-
String(value)
|
|
425
|
-
.toLowerCase()
|
|
426
|
-
.includes(search.trim().toLowerCase())
|
|
427
|
-
);
|
|
428
|
-
const matchesStatus =
|
|
429
|
-
statusFilter === 'all' ? true : item.status === statusFilter;
|
|
430
|
-
return matchesSearch && matchesStatus;
|
|
431
|
-
}),
|
|
432
|
-
[timesheets, search, statusFilter]
|
|
433
|
-
);
|
|
525
|
+
const filteredRows = timesheets;
|
|
434
526
|
|
|
435
527
|
const cards = [
|
|
436
528
|
{
|
|
@@ -459,9 +551,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
459
551
|
];
|
|
460
552
|
|
|
461
553
|
const openCreate = () => {
|
|
554
|
+
setEditingEntry(null);
|
|
462
555
|
form.reset(buildDefaultFormValues());
|
|
463
556
|
setProjectQuery({ search: '', page: 1 });
|
|
464
557
|
setTaskQuery({ search: '', page: 1 });
|
|
558
|
+
setProjectOptions([]);
|
|
465
559
|
setTaskOptions([]);
|
|
466
560
|
setIsTaskCreateSheetOpen(false);
|
|
467
561
|
setIsSheetOpen(true);
|
|
@@ -469,13 +563,20 @@ export default function OperationsTimesheetsPage() {
|
|
|
469
563
|
|
|
470
564
|
const closeCreateSheet = () => {
|
|
471
565
|
setIsSheetOpen(false);
|
|
566
|
+
setEditingEntry(null);
|
|
472
567
|
setIsTaskCreateSheetOpen(false);
|
|
473
568
|
form.reset(buildDefaultFormValues());
|
|
474
569
|
setProjectQuery({ search: '', page: 1 });
|
|
475
570
|
setTaskQuery({ search: '', page: 1 });
|
|
571
|
+
setProjectOptions([]);
|
|
476
572
|
setTaskOptions([]);
|
|
477
573
|
};
|
|
478
574
|
|
|
575
|
+
const openDetails = (timesheet: OperationsTimesheet) => {
|
|
576
|
+
setSelectedTimesheet(timesheet);
|
|
577
|
+
setIsDetailsOpen(true);
|
|
578
|
+
};
|
|
579
|
+
|
|
479
580
|
const canManageRow = (timesheet: OperationsTimesheet) => {
|
|
480
581
|
return Boolean(
|
|
481
582
|
me?.id &&
|
|
@@ -484,28 +585,39 @@ export default function OperationsTimesheetsPage() {
|
|
|
484
585
|
);
|
|
485
586
|
};
|
|
486
587
|
|
|
487
|
-
const canManageEntry = (entry: OperationsTimesheetEntry) => {
|
|
488
|
-
return Boolean(
|
|
489
|
-
me?.id &&
|
|
490
|
-
entry.collaboratorId === me.id &&
|
|
491
|
-
['draft', 'rejected'].includes(entry.status ?? '')
|
|
492
|
-
);
|
|
493
|
-
};
|
|
494
|
-
|
|
495
588
|
const handleQuickEntrySubmit = async (values: QuickEntryFormValues) => {
|
|
589
|
+
const payload = {
|
|
590
|
+
projectId: values.projectId,
|
|
591
|
+
taskId: values.taskId,
|
|
592
|
+
workDate: values.workDate,
|
|
593
|
+
duration: values.duration,
|
|
594
|
+
unit: values.unit,
|
|
595
|
+
description: trimToNull(values.description),
|
|
596
|
+
};
|
|
597
|
+
|
|
496
598
|
try {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
599
|
+
if (editingEntry?.id) {
|
|
600
|
+
await mutateOperations(
|
|
601
|
+
request,
|
|
602
|
+
`/operations/timesheet-entries/${editingEntry.id}`,
|
|
603
|
+
'PATCH',
|
|
604
|
+
payload
|
|
605
|
+
);
|
|
606
|
+
} else {
|
|
607
|
+
await mutateOperations(
|
|
608
|
+
request,
|
|
609
|
+
'/operations/timesheet-entries',
|
|
610
|
+
'POST',
|
|
611
|
+
payload
|
|
612
|
+
);
|
|
613
|
+
}
|
|
505
614
|
|
|
506
|
-
showToastHandler?.(
|
|
615
|
+
showToastHandler?.(
|
|
616
|
+
'success',
|
|
617
|
+
editingEntry ? t('messages.updateSuccess') : t('messages.saveSuccess')
|
|
618
|
+
);
|
|
507
619
|
|
|
508
|
-
if (keepContextOnSave) {
|
|
620
|
+
if (!editingEntry && keepContextOnSave) {
|
|
509
621
|
form.reset(buildFollowUpFormValues(values, true));
|
|
510
622
|
setProjectQuery({ search: '', page: 1 });
|
|
511
623
|
setTaskQuery({ search: '', page: 1 });
|
|
@@ -519,7 +631,10 @@ export default function OperationsTimesheetsPage() {
|
|
|
519
631
|
} catch (error) {
|
|
520
632
|
showToastHandler?.(
|
|
521
633
|
'error',
|
|
522
|
-
getOperationsErrorMessage(
|
|
634
|
+
getOperationsErrorMessage(
|
|
635
|
+
error,
|
|
636
|
+
editingEntry ? t('messages.updateError') : t('messages.saveError')
|
|
637
|
+
)
|
|
523
638
|
);
|
|
524
639
|
}
|
|
525
640
|
};
|
|
@@ -604,222 +719,260 @@ export default function OperationsTimesheetsPage() {
|
|
|
604
719
|
}
|
|
605
720
|
/>
|
|
606
721
|
|
|
607
|
-
<
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
722
|
+
<div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
|
|
723
|
+
<div className="flex-1">
|
|
724
|
+
<SearchBar
|
|
725
|
+
searchQuery={search}
|
|
726
|
+
onSearchChange={(value) => {
|
|
727
|
+
setSearch(value);
|
|
728
|
+
setPage(1);
|
|
729
|
+
}}
|
|
730
|
+
showSearchButton={false}
|
|
731
|
+
debounceMs={500}
|
|
732
|
+
placeholder={t('searchPlaceholder')}
|
|
733
|
+
controls={[
|
|
734
|
+
{
|
|
735
|
+
id: 'status',
|
|
736
|
+
type: 'select',
|
|
737
|
+
value: statusFilter,
|
|
738
|
+
onChange: (value) => {
|
|
739
|
+
setStatusFilter(value);
|
|
740
|
+
setPage(1);
|
|
741
|
+
},
|
|
742
|
+
placeholder: commonT('labels.status'),
|
|
743
|
+
options: [
|
|
744
|
+
{ value: 'all', label: commonT('filters.allStatuses') },
|
|
745
|
+
{ value: 'draft', label: formatEnumLabel('draft') },
|
|
746
|
+
{ value: 'submitted', label: formatEnumLabel('submitted') },
|
|
747
|
+
{ value: 'approved', label: formatEnumLabel('approved') },
|
|
748
|
+
{ value: 'rejected', label: formatEnumLabel('rejected') },
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
]}
|
|
752
|
+
/>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div className="flex items-center justify-between gap-3 xl:justify-end">
|
|
756
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
757
|
+
{t('viewMode')}
|
|
758
|
+
</span>
|
|
759
|
+
<ToggleGroup
|
|
760
|
+
type="single"
|
|
761
|
+
value={viewMode}
|
|
762
|
+
onValueChange={handleViewModeChange}
|
|
763
|
+
variant="outline"
|
|
764
|
+
size="sm"
|
|
765
|
+
>
|
|
766
|
+
<ToggleGroupItem value="table" className="gap-1.5 px-2.5">
|
|
767
|
+
<List className="h-4 w-4" />
|
|
768
|
+
<span className="hidden sm:inline">{t('viewModeTable')}</span>
|
|
769
|
+
</ToggleGroupItem>
|
|
770
|
+
<ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
|
|
771
|
+
<LayoutGrid className="h-4 w-4" />
|
|
772
|
+
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
773
|
+
</ToggleGroupItem>
|
|
774
|
+
</ToggleGroup>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
629
777
|
|
|
630
778
|
<KpiCardsGrid items={cards} columns={3} />
|
|
631
779
|
|
|
632
|
-
{
|
|
633
|
-
|
|
634
|
-
<
|
|
635
|
-
|
|
636
|
-
<div
|
|
637
|
-
|
|
638
|
-
|
|
780
|
+
{timesheets.length > 0 ? (
|
|
781
|
+
viewMode === 'cards' ? (
|
|
782
|
+
<div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
|
783
|
+
{timesheets.map((timesheet) => (
|
|
784
|
+
<div
|
|
785
|
+
key={timesheet.id}
|
|
786
|
+
className="rounded-lg border bg-card shadow-sm p-4 space-y-3"
|
|
787
|
+
>
|
|
788
|
+
<div className="flex items-start justify-between gap-3">
|
|
789
|
+
<div className="min-w-0">
|
|
790
|
+
<div className="truncate font-semibold">
|
|
791
|
+
{timesheet.collaboratorName}
|
|
792
|
+
</div>
|
|
793
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
794
|
+
{formatDateRange(
|
|
795
|
+
timesheet.weekStartDate,
|
|
796
|
+
timesheet.weekEndDate
|
|
797
|
+
)}
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
<StatusBadge
|
|
801
|
+
label={
|
|
802
|
+
t.has(`statuses.${timesheet.status}`)
|
|
803
|
+
? t(`statuses.${timesheet.status}`)
|
|
804
|
+
: formatEnumLabel(timesheet.status)
|
|
805
|
+
}
|
|
806
|
+
className={getStatusBadgeClass(timesheet.status)}
|
|
807
|
+
/>
|
|
639
808
|
</div>
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
{
|
|
645
|
-
|
|
809
|
+
<div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
|
|
810
|
+
<div>
|
|
811
|
+
<span className="font-medium text-foreground">
|
|
812
|
+
{commonT('labels.totalHours')}:
|
|
813
|
+
</span>{' '}
|
|
814
|
+
{formatHours(timesheet.totalHours)}
|
|
815
|
+
</div>
|
|
816
|
+
<div>
|
|
817
|
+
<span className="font-medium text-foreground">
|
|
818
|
+
{commonT('labels.entries')}:
|
|
819
|
+
</span>{' '}
|
|
820
|
+
{timesheet.entries?.length ?? 0}
|
|
821
|
+
</div>
|
|
646
822
|
</div>
|
|
823
|
+
{timesheet.approverName ? (
|
|
824
|
+
<div className="text-sm text-muted-foreground">
|
|
825
|
+
<span className="font-medium text-foreground">
|
|
826
|
+
{commonT('labels.approver')}:
|
|
827
|
+
</span>{' '}
|
|
828
|
+
{timesheet.approverName}
|
|
829
|
+
</div>
|
|
830
|
+
) : null}
|
|
831
|
+
{canManageRow(timesheet) ? (
|
|
832
|
+
<div className="flex justify-end gap-2 border-t border-border/60 pt-3">
|
|
833
|
+
<Button
|
|
834
|
+
variant="outline"
|
|
835
|
+
size="sm"
|
|
836
|
+
className="cursor-pointer"
|
|
837
|
+
onClick={() => openDetails(timesheet)}
|
|
838
|
+
>
|
|
839
|
+
<Eye className="size-4" />
|
|
840
|
+
{t('actions.viewDetails')}
|
|
841
|
+
</Button>
|
|
842
|
+
<Button
|
|
843
|
+
size="sm"
|
|
844
|
+
onClick={() => void submitTimesheet(timesheet.id)}
|
|
845
|
+
>
|
|
846
|
+
<Send className="size-4" />
|
|
847
|
+
{commonT('actions.submit')}
|
|
848
|
+
</Button>
|
|
849
|
+
</div>
|
|
850
|
+
) : (
|
|
851
|
+
<div className="flex justify-end border-t border-border/60 pt-3">
|
|
852
|
+
<Button
|
|
853
|
+
variant="outline"
|
|
854
|
+
size="sm"
|
|
855
|
+
className="cursor-pointer"
|
|
856
|
+
onClick={() => openDetails(timesheet)}
|
|
857
|
+
>
|
|
858
|
+
<Eye className="size-4" />
|
|
859
|
+
{t('actions.viewDetails')}
|
|
860
|
+
</Button>
|
|
861
|
+
</div>
|
|
862
|
+
)}
|
|
647
863
|
</div>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
>
|
|
668
|
-
<
|
|
669
|
-
<div className="
|
|
670
|
-
|
|
671
|
-
{[entry.projectCode, entry.projectName]
|
|
672
|
-
.filter(Boolean)
|
|
673
|
-
.join(' • ') || commonT('labels.unassigned')}
|
|
674
|
-
</span>
|
|
675
|
-
<span className="text-muted-foreground">•</span>
|
|
676
|
-
<span className="text-sm text-muted-foreground">
|
|
677
|
-
{entry.taskName ||
|
|
678
|
-
entry.activityLabel ||
|
|
679
|
-
commonT('labels.noNotes')}
|
|
680
|
-
</span>
|
|
864
|
+
))}
|
|
865
|
+
</div>
|
|
866
|
+
) : (
|
|
867
|
+
<div className="overflow-x-auto rounded-md border">
|
|
868
|
+
<Table>
|
|
869
|
+
<TableHeader>
|
|
870
|
+
<TableRow>
|
|
871
|
+
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
872
|
+
<TableHead>{commonT('labels.week')}</TableHead>
|
|
873
|
+
<TableHead>{commonT('labels.entries')}</TableHead>
|
|
874
|
+
<TableHead>{commonT('labels.totalHours')}</TableHead>
|
|
875
|
+
<TableHead>{commonT('labels.approver')}</TableHead>
|
|
876
|
+
<TableHead>{commonT('labels.decisionNote')}</TableHead>
|
|
877
|
+
<TableHead>{commonT('labels.status')}</TableHead>
|
|
878
|
+
<TableHead>{commonT('labels.actions')}</TableHead>
|
|
879
|
+
</TableRow>
|
|
880
|
+
</TableHeader>
|
|
881
|
+
<TableBody>
|
|
882
|
+
{filteredRows.map((timesheet) => (
|
|
883
|
+
<TableRow key={timesheet.id} className="hover:bg-muted/30">
|
|
884
|
+
<TableCell>
|
|
885
|
+
<div className="font-medium">
|
|
886
|
+
{timesheet.collaboratorName}
|
|
681
887
|
</div>
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
888
|
+
<div className="text-xs text-muted-foreground">
|
|
889
|
+
{timesheet.notes || commonT('labels.noNotes')}
|
|
890
|
+
</div>
|
|
891
|
+
</TableCell>
|
|
892
|
+
<TableCell>
|
|
893
|
+
{formatDateRange(
|
|
894
|
+
timesheet.weekStartDate,
|
|
895
|
+
timesheet.weekEndDate
|
|
896
|
+
)}
|
|
897
|
+
</TableCell>
|
|
898
|
+
<TableCell>
|
|
899
|
+
<div className="font-medium">
|
|
900
|
+
{timesheet.entries?.length ?? 0}{' '}
|
|
901
|
+
{commonT('labels.lines')}
|
|
902
|
+
</div>
|
|
903
|
+
<div className="text-xs text-muted-foreground">
|
|
904
|
+
{(timesheet.entries ?? [])
|
|
905
|
+
.slice(0, 2)
|
|
906
|
+
.map(
|
|
907
|
+
(entry) =>
|
|
908
|
+
[
|
|
909
|
+
entry.projectName,
|
|
910
|
+
entry.taskName || entry.activityLabel,
|
|
911
|
+
]
|
|
912
|
+
.filter(Boolean)
|
|
913
|
+
.join(' • ') || commonT('labels.unassigned')
|
|
914
|
+
)
|
|
915
|
+
.join(', ') || commonT('labels.unassigned')}
|
|
916
|
+
</div>
|
|
917
|
+
</TableCell>
|
|
918
|
+
<TableCell>{formatHours(timesheet.totalHours)}</TableCell>
|
|
919
|
+
<TableCell>
|
|
920
|
+
{timesheet.approverName || commonT('labels.notAssigned')}
|
|
921
|
+
</TableCell>
|
|
922
|
+
<TableCell>
|
|
923
|
+
<div className="text-sm text-muted-foreground">
|
|
924
|
+
{timesheet.decisionNote || commonT('labels.noNotes')}
|
|
925
|
+
</div>
|
|
926
|
+
</TableCell>
|
|
927
|
+
<TableCell>
|
|
928
|
+
<StatusBadge
|
|
929
|
+
label={
|
|
930
|
+
t.has(`statuses.${timesheet.status}`)
|
|
931
|
+
? t(`statuses.${timesheet.status}`)
|
|
932
|
+
: formatEnumLabel(timesheet.status)
|
|
933
|
+
}
|
|
934
|
+
className={getStatusBadgeClass(timesheet.status)}
|
|
935
|
+
/>
|
|
936
|
+
</TableCell>
|
|
937
|
+
<TableCell>
|
|
938
|
+
<div className="flex justify-end gap-2">
|
|
939
|
+
{canManageRow(timesheet) ? (
|
|
686
940
|
<>
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
941
|
+
<Button
|
|
942
|
+
variant="outline"
|
|
943
|
+
size="sm"
|
|
944
|
+
className="cursor-pointer"
|
|
945
|
+
onClick={() => openDetails(timesheet)}
|
|
946
|
+
>
|
|
947
|
+
<Eye className="size-4" />
|
|
948
|
+
{t('actions.viewDetails')}
|
|
949
|
+
</Button>
|
|
950
|
+
<Button
|
|
951
|
+
size="icon"
|
|
952
|
+
onClick={() => void submitTimesheet(timesheet.id)}
|
|
953
|
+
>
|
|
954
|
+
<Send className="size-4" />
|
|
955
|
+
</Button>
|
|
691
956
|
</>
|
|
692
|
-
) :
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
) : null}
|
|
703
|
-
|
|
704
|
-
<span className="inline-flex items-center rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-foreground">
|
|
705
|
-
{formatEntryDuration(
|
|
706
|
-
entry.durationMinutes,
|
|
707
|
-
entry.hours
|
|
957
|
+
) : (
|
|
958
|
+
<Button
|
|
959
|
+
variant="outline"
|
|
960
|
+
size="sm"
|
|
961
|
+
className="cursor-pointer"
|
|
962
|
+
onClick={() => openDetails(timesheet)}
|
|
963
|
+
>
|
|
964
|
+
<Eye className="size-4" />
|
|
965
|
+
{t('actions.viewDetails')}
|
|
966
|
+
</Button>
|
|
708
967
|
)}
|
|
709
|
-
</
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<Button
|
|
713
|
-
type="button"
|
|
714
|
-
variant="ghost"
|
|
715
|
-
size="icon"
|
|
716
|
-
className="text-muted-foreground hover:text-destructive"
|
|
717
|
-
onClick={() => setEntryToDelete(entry)}
|
|
718
|
-
aria-label={t('messages.confirmDeleteTitle')}
|
|
719
|
-
>
|
|
720
|
-
<Trash2 className="size-4" />
|
|
721
|
-
</Button>
|
|
722
|
-
) : null}
|
|
723
|
-
</div>
|
|
724
|
-
</div>
|
|
968
|
+
</div>
|
|
969
|
+
</TableCell>
|
|
970
|
+
</TableRow>
|
|
725
971
|
))}
|
|
726
|
-
</
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
</div>
|
|
731
|
-
)}
|
|
732
|
-
</CardContent>
|
|
733
|
-
</Card>
|
|
734
|
-
) : null}
|
|
735
|
-
|
|
736
|
-
{filteredRows.length > 0 ? (
|
|
737
|
-
<div className="overflow-x-auto rounded-md border">
|
|
738
|
-
<Table>
|
|
739
|
-
<TableHeader>
|
|
740
|
-
<TableRow>
|
|
741
|
-
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
742
|
-
<TableHead>{commonT('labels.week')}</TableHead>
|
|
743
|
-
<TableHead>{commonT('labels.entries')}</TableHead>
|
|
744
|
-
<TableHead>{commonT('labels.totalHours')}</TableHead>
|
|
745
|
-
<TableHead>{commonT('labels.approver')}</TableHead>
|
|
746
|
-
<TableHead>{commonT('labels.decisionNote')}</TableHead>
|
|
747
|
-
<TableHead>{commonT('labels.status')}</TableHead>
|
|
748
|
-
<TableHead>{commonT('labels.actions')}</TableHead>
|
|
749
|
-
</TableRow>
|
|
750
|
-
</TableHeader>
|
|
751
|
-
<TableBody>
|
|
752
|
-
{filteredRows.map((timesheet) => (
|
|
753
|
-
<TableRow key={timesheet.id} className="hover:bg-muted/30">
|
|
754
|
-
<TableCell>
|
|
755
|
-
<div className="font-medium">
|
|
756
|
-
{timesheet.collaboratorName}
|
|
757
|
-
</div>
|
|
758
|
-
<div className="text-xs text-muted-foreground">
|
|
759
|
-
{timesheet.notes || commonT('labels.noNotes')}
|
|
760
|
-
</div>
|
|
761
|
-
</TableCell>
|
|
762
|
-
<TableCell>
|
|
763
|
-
{formatDateRange(
|
|
764
|
-
timesheet.weekStartDate,
|
|
765
|
-
timesheet.weekEndDate
|
|
766
|
-
)}
|
|
767
|
-
</TableCell>
|
|
768
|
-
<TableCell>
|
|
769
|
-
<div className="font-medium">
|
|
770
|
-
{timesheet.entries?.length ?? 0} {commonT('labels.lines')}
|
|
771
|
-
</div>
|
|
772
|
-
<div className="text-xs text-muted-foreground">
|
|
773
|
-
{(timesheet.entries ?? [])
|
|
774
|
-
.slice(0, 2)
|
|
775
|
-
.map(
|
|
776
|
-
(entry) =>
|
|
777
|
-
[
|
|
778
|
-
entry.projectName,
|
|
779
|
-
entry.taskName || entry.activityLabel,
|
|
780
|
-
]
|
|
781
|
-
.filter(Boolean)
|
|
782
|
-
.join(' • ') || commonT('labels.unassigned')
|
|
783
|
-
)
|
|
784
|
-
.join(', ') || commonT('labels.unassigned')}
|
|
785
|
-
</div>
|
|
786
|
-
</TableCell>
|
|
787
|
-
<TableCell>{formatHours(timesheet.totalHours)}</TableCell>
|
|
788
|
-
<TableCell>
|
|
789
|
-
{timesheet.approverName || commonT('labels.notAssigned')}
|
|
790
|
-
</TableCell>
|
|
791
|
-
<TableCell>
|
|
792
|
-
<div className="text-sm text-muted-foreground">
|
|
793
|
-
{timesheet.decisionNote || commonT('labels.noNotes')}
|
|
794
|
-
</div>
|
|
795
|
-
</TableCell>
|
|
796
|
-
<TableCell>
|
|
797
|
-
<StatusBadge
|
|
798
|
-
label={formatEnumLabel(timesheet.status)}
|
|
799
|
-
className={getStatusBadgeClass(timesheet.status)}
|
|
800
|
-
/>
|
|
801
|
-
</TableCell>
|
|
802
|
-
<TableCell>
|
|
803
|
-
<div className="flex justify-end gap-2">
|
|
804
|
-
{canManageRow(timesheet) ? (
|
|
805
|
-
<Button
|
|
806
|
-
size="icon"
|
|
807
|
-
onClick={() => void submitTimesheet(timesheet.id)}
|
|
808
|
-
>
|
|
809
|
-
<Send className="size-4" />
|
|
810
|
-
</Button>
|
|
811
|
-
) : (
|
|
812
|
-
<span className="text-xs text-muted-foreground">
|
|
813
|
-
{commonT('labels.viewOnly')}
|
|
814
|
-
</span>
|
|
815
|
-
)}
|
|
816
|
-
</div>
|
|
817
|
-
</TableCell>
|
|
818
|
-
</TableRow>
|
|
819
|
-
))}
|
|
820
|
-
</TableBody>
|
|
821
|
-
</Table>
|
|
822
|
-
</div>
|
|
972
|
+
</TableBody>
|
|
973
|
+
</Table>
|
|
974
|
+
</div>
|
|
975
|
+
)
|
|
823
976
|
) : (
|
|
824
977
|
<EmptyState
|
|
825
978
|
icon={<ClipboardList className="size-12" />}
|
|
@@ -834,6 +987,18 @@ export default function OperationsTimesheetsPage() {
|
|
|
834
987
|
/>
|
|
835
988
|
)}
|
|
836
989
|
|
|
990
|
+
<PaginationFooter
|
|
991
|
+
currentPage={page}
|
|
992
|
+
pageSize={pageSize}
|
|
993
|
+
totalItems={timesheetsResponse?.total ?? 0}
|
|
994
|
+
pageSizeOptions={[12, 24, 48]}
|
|
995
|
+
onPageChange={setPage}
|
|
996
|
+
onPageSizeChange={(size) => {
|
|
997
|
+
setPageSize(size);
|
|
998
|
+
setPage(1);
|
|
999
|
+
}}
|
|
1000
|
+
/>
|
|
1001
|
+
|
|
837
1002
|
<Sheet
|
|
838
1003
|
open={isSheetOpen}
|
|
839
1004
|
onOpenChange={(open) => {
|
|
@@ -857,7 +1022,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
857
1022
|
</div>
|
|
858
1023
|
|
|
859
1024
|
<div className="space-y-1">
|
|
860
|
-
<SheetTitle>
|
|
1025
|
+
<SheetTitle>
|
|
1026
|
+
{isEditingEntry
|
|
1027
|
+
? t('sheet.editTitle')
|
|
1028
|
+
: t('sheet.createTitle')}
|
|
1029
|
+
</SheetTitle>
|
|
861
1030
|
<SheetDescription>{t('sheet.description')}</SheetDescription>
|
|
862
1031
|
</div>
|
|
863
1032
|
</div>
|
|
@@ -926,7 +1095,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
926
1095
|
<AsyncOptionsCombobox
|
|
927
1096
|
value={field.value}
|
|
928
1097
|
selectedOption={selectedTask}
|
|
929
|
-
options={
|
|
1098
|
+
options={filteredTaskOptions}
|
|
930
1099
|
onSelect={(option) => field.onChange(option?.id)}
|
|
931
1100
|
searchValue={taskQuery.search}
|
|
932
1101
|
onSearchValueChange={(value) =>
|
|
@@ -1091,6 +1260,142 @@ export default function OperationsTimesheetsPage() {
|
|
|
1091
1260
|
onCreated={handleTaskCreated}
|
|
1092
1261
|
/>
|
|
1093
1262
|
|
|
1263
|
+
<Sheet open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
|
1264
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
|
|
1265
|
+
<SheetHeader>
|
|
1266
|
+
<SheetTitle>{t('details.title')}</SheetTitle>
|
|
1267
|
+
<SheetDescription>{t('details.description')}</SheetDescription>
|
|
1268
|
+
</SheetHeader>
|
|
1269
|
+
|
|
1270
|
+
{selectedTimesheet ? (
|
|
1271
|
+
<div className="mt-6 space-y-5 px-4 pb-8">
|
|
1272
|
+
<div className="rounded-lg border bg-muted/20 p-4">
|
|
1273
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1274
|
+
<div>
|
|
1275
|
+
<div className="text-lg font-semibold">
|
|
1276
|
+
{selectedTimesheet.collaboratorName}
|
|
1277
|
+
</div>
|
|
1278
|
+
<div className="mt-1 text-sm text-muted-foreground">
|
|
1279
|
+
{formatDateRange(
|
|
1280
|
+
selectedTimesheet.weekStartDate,
|
|
1281
|
+
selectedTimesheet.weekEndDate
|
|
1282
|
+
)}
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
<StatusBadge
|
|
1286
|
+
label={
|
|
1287
|
+
t.has(`statuses.${selectedTimesheet.status}`)
|
|
1288
|
+
? t(`statuses.${selectedTimesheet.status}`)
|
|
1289
|
+
: formatEnumLabel(selectedTimesheet.status)
|
|
1290
|
+
}
|
|
1291
|
+
className={getStatusBadgeClass(selectedTimesheet.status)}
|
|
1292
|
+
/>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
|
|
1296
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
1297
|
+
<div>
|
|
1298
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1299
|
+
{commonT('labels.totalHours')}
|
|
1300
|
+
</div>
|
|
1301
|
+
<div className="mt-1 text-sm">
|
|
1302
|
+
{formatHours(selectedTimesheet.totalHours)}
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div>
|
|
1306
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1307
|
+
{commonT('labels.entries')}
|
|
1308
|
+
</div>
|
|
1309
|
+
<div className="mt-1 text-sm">
|
|
1310
|
+
{selectedTimesheet.entries?.length ?? 0}
|
|
1311
|
+
</div>
|
|
1312
|
+
</div>
|
|
1313
|
+
<div>
|
|
1314
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1315
|
+
{commonT('labels.approver')}
|
|
1316
|
+
</div>
|
|
1317
|
+
<div className="mt-1 text-sm">
|
|
1318
|
+
{selectedTimesheet.approverName ||
|
|
1319
|
+
commonT('labels.notAssigned')}
|
|
1320
|
+
</div>
|
|
1321
|
+
</div>
|
|
1322
|
+
<div>
|
|
1323
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1324
|
+
{t('details.period')}
|
|
1325
|
+
</div>
|
|
1326
|
+
<div className="mt-1 text-sm">
|
|
1327
|
+
{formatDateLabel(selectedTimesheet.weekStartDate)} -{' '}
|
|
1328
|
+
{formatDateLabel(selectedTimesheet.weekEndDate)}
|
|
1329
|
+
</div>
|
|
1330
|
+
</div>
|
|
1331
|
+
</div>
|
|
1332
|
+
|
|
1333
|
+
<div>
|
|
1334
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1335
|
+
{commonT('labels.notes')}
|
|
1336
|
+
</div>
|
|
1337
|
+
<div className="mt-1 rounded-lg border p-3 text-sm">
|
|
1338
|
+
{selectedTimesheet.notes || commonT('labels.noNotes')}
|
|
1339
|
+
</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
|
|
1342
|
+
<div>
|
|
1343
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1344
|
+
{commonT('labels.decisionNote')}
|
|
1345
|
+
</div>
|
|
1346
|
+
<div className="mt-1 rounded-lg border p-3 text-sm">
|
|
1347
|
+
{selectedTimesheet.decisionNote || commonT('labels.noNotes')}
|
|
1348
|
+
</div>
|
|
1349
|
+
</div>
|
|
1350
|
+
|
|
1351
|
+
<div>
|
|
1352
|
+
<div className="text-xs font-medium uppercase text-muted-foreground">
|
|
1353
|
+
{t('details.entriesTitle')}
|
|
1354
|
+
</div>
|
|
1355
|
+
<div className="mt-2 space-y-3">
|
|
1356
|
+
{(selectedTimesheet.entries ?? []).length ? (
|
|
1357
|
+
(selectedTimesheet.entries ?? []).map((entry) => (
|
|
1358
|
+
<div key={entry.id} className="rounded-lg border p-3">
|
|
1359
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1360
|
+
<div>
|
|
1361
|
+
<div className="font-medium">
|
|
1362
|
+
{entry.taskName ||
|
|
1363
|
+
entry.activityLabel ||
|
|
1364
|
+
commonT('labels.unassigned')}
|
|
1365
|
+
</div>
|
|
1366
|
+
<div className="text-xs text-muted-foreground">
|
|
1367
|
+
{[entry.projectName, entry.roleLabel]
|
|
1368
|
+
.filter(Boolean)
|
|
1369
|
+
.join(' • ') || commonT('labels.unassigned')}
|
|
1370
|
+
</div>
|
|
1371
|
+
</div>
|
|
1372
|
+
<div className="text-sm text-muted-foreground">
|
|
1373
|
+
{formatEntryDuration(
|
|
1374
|
+
entry.durationMinutes,
|
|
1375
|
+
entry.hours
|
|
1376
|
+
)}
|
|
1377
|
+
</div>
|
|
1378
|
+
</div>
|
|
1379
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
1380
|
+
{formatDateLabel(entry.workDate)}
|
|
1381
|
+
</div>
|
|
1382
|
+
<div className="mt-2 text-sm">
|
|
1383
|
+
{entry.description || commonT('labels.noNotes')}
|
|
1384
|
+
</div>
|
|
1385
|
+
</div>
|
|
1386
|
+
))
|
|
1387
|
+
) : (
|
|
1388
|
+
<div className="rounded-lg border p-3 text-sm text-muted-foreground">
|
|
1389
|
+
{t('details.noEntries')}
|
|
1390
|
+
</div>
|
|
1391
|
+
)}
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
</div>
|
|
1395
|
+
) : null}
|
|
1396
|
+
</SheetContent>
|
|
1397
|
+
</Sheet>
|
|
1398
|
+
|
|
1094
1399
|
<AlertDialog
|
|
1095
1400
|
open={Boolean(entryToDelete)}
|
|
1096
1401
|
onOpenChange={(open) => {
|