@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,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,12 +51,12 @@ 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
|
-
Pencil,
|
|
58
58
|
Plus,
|
|
59
59
|
Send,
|
|
60
|
-
Sparkles,
|
|
61
|
-
Trash2,
|
|
62
60
|
} from 'lucide-react';
|
|
63
61
|
import { useTranslations } from 'next-intl';
|
|
64
62
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -100,7 +98,10 @@ type QuickEntryFormValues = {
|
|
|
100
98
|
description: string;
|
|
101
99
|
};
|
|
102
100
|
|
|
103
|
-
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
|
+
};
|
|
104
105
|
|
|
105
106
|
const buildDefaultFormValues = (): QuickEntryFormValues => ({
|
|
106
107
|
projectId: undefined,
|
|
@@ -146,7 +147,13 @@ function formatDateLabel(value?: string | null) {
|
|
|
146
147
|
return '—';
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
|
|
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`);
|
|
150
157
|
|
|
151
158
|
if (Number.isNaN(date.getTime())) {
|
|
152
159
|
return value;
|
|
@@ -196,69 +203,6 @@ function buildFollowUpFormValues(
|
|
|
196
203
|
};
|
|
197
204
|
}
|
|
198
205
|
|
|
199
|
-
function buildProjectOptionFromEntry(
|
|
200
|
-
entry: OperationsTimesheetEntry
|
|
201
|
-
): OperationsProjectOption | null {
|
|
202
|
-
if (!entry.projectId) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
id: entry.projectId,
|
|
208
|
-
label:
|
|
209
|
-
[entry.projectCode, entry.projectName, entry.roleLabel]
|
|
210
|
-
.filter(Boolean)
|
|
211
|
-
.join(' • ') || String(entry.projectId),
|
|
212
|
-
name: entry.projectName || String(entry.projectId),
|
|
213
|
-
code: entry.projectCode,
|
|
214
|
-
projectAssignmentId: entry.projectAssignmentId,
|
|
215
|
-
roleLabel: entry.roleLabel,
|
|
216
|
-
status: 'active',
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function buildTaskOptionFromEntry(
|
|
221
|
-
entry: OperationsTimesheetEntry
|
|
222
|
-
): OperationsTaskOption | null {
|
|
223
|
-
if (!entry.taskId || !entry.projectId || !entry.projectAssignmentId) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
id: entry.taskId,
|
|
229
|
-
label:
|
|
230
|
-
[entry.taskName || entry.activityLabel, entry.projectName]
|
|
231
|
-
.filter(Boolean)
|
|
232
|
-
.join(' • ') || String(entry.taskId),
|
|
233
|
-
name: entry.taskName || entry.activityLabel || String(entry.taskId),
|
|
234
|
-
status: 'todo',
|
|
235
|
-
projectId: entry.projectId,
|
|
236
|
-
projectAssignmentId: entry.projectAssignmentId,
|
|
237
|
-
projectName: entry.projectName || '',
|
|
238
|
-
projectCode: entry.projectCode,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function buildEditFormValues(
|
|
243
|
-
entry: OperationsTimesheetEntry
|
|
244
|
-
): QuickEntryFormValues {
|
|
245
|
-
const durationMinutes =
|
|
246
|
-
typeof entry.durationMinutes === 'number' &&
|
|
247
|
-
Number.isFinite(entry.durationMinutes)
|
|
248
|
-
? entry.durationMinutes
|
|
249
|
-
: Math.round(Number(entry.hours ?? 0) * 60);
|
|
250
|
-
const useHours = durationMinutes > 0 && durationMinutes % 60 === 0;
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
projectId: entry.projectId ?? undefined,
|
|
254
|
-
taskId: entry.taskId ?? undefined,
|
|
255
|
-
workDate: entry.workDate ? entry.workDate.slice(0, 10) : '',
|
|
256
|
-
duration: useHours ? durationMinutes / 60 : durationMinutes,
|
|
257
|
-
unit: useHours ? 'hours' : 'minutes',
|
|
258
|
-
description: entry.description ?? '',
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
206
|
export default function OperationsTimesheetsPage() {
|
|
263
207
|
const t = useTranslations('operations.TimesheetsPage');
|
|
264
208
|
const commonT = useTranslations('operations.Common');
|
|
@@ -266,13 +210,25 @@ export default function OperationsTimesheetsPage() {
|
|
|
266
210
|
const access = useOperationsAccess();
|
|
267
211
|
const [search, setSearch] = useState('');
|
|
268
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
|
+
});
|
|
269
222
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
223
|
+
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
|
270
224
|
const [isTaskCreateSheetOpen, setIsTaskCreateSheetOpen] = useState(false);
|
|
271
225
|
const [keepContextOnSave, setKeepContextOnSave] = useState(true);
|
|
272
226
|
const [editingEntry, setEditingEntry] =
|
|
273
227
|
useState<OperationsTimesheetEntry | null>(null);
|
|
274
228
|
const [entryToDelete, setEntryToDelete] =
|
|
275
229
|
useState<OperationsTimesheetEntry | null>(null);
|
|
230
|
+
const [selectedTimesheet, setSelectedTimesheet] =
|
|
231
|
+
useState<OperationsTimesheet | null>(null);
|
|
276
232
|
const [isDeletingEntry, setIsDeletingEntry] = useState(false);
|
|
277
233
|
const [projectQuery, setProjectQuery] = useState({ search: '', page: 1 });
|
|
278
234
|
const [projectOptions, setProjectOptions] = useState<
|
|
@@ -321,13 +277,42 @@ export default function OperationsTimesheetsPage() {
|
|
|
321
277
|
name: 'unit',
|
|
322
278
|
});
|
|
323
279
|
|
|
324
|
-
const { data:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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,
|
|
329
304
|
});
|
|
330
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
|
+
|
|
331
316
|
const { data: me } = useQuery<OperationsCollaborator>({
|
|
332
317
|
queryKey: ['operations-timesheets-me', currentLocaleCode],
|
|
333
318
|
enabled: access.isCollaborator,
|
|
@@ -338,11 +323,9 @@ export default function OperationsTimesheetsPage() {
|
|
|
338
323
|
),
|
|
339
324
|
});
|
|
340
325
|
|
|
341
|
-
const {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
refetch: refetchRecentEntries,
|
|
345
|
-
} = useQuery<PaginatedResponse<OperationsTimesheetEntry>>({
|
|
326
|
+
const { refetch: refetchRecentEntries } = useQuery<
|
|
327
|
+
PaginatedResponse<OperationsTimesheetEntry>
|
|
328
|
+
>({
|
|
346
329
|
queryKey: [
|
|
347
330
|
'operations-timesheet-recent-entries',
|
|
348
331
|
currentLocaleCode,
|
|
@@ -532,7 +515,6 @@ export default function OperationsTimesheetsPage() {
|
|
|
532
515
|
);
|
|
533
516
|
}, [showToastHandler, t, taskOptionsError]);
|
|
534
517
|
|
|
535
|
-
const recentEntries = recentEntriesResponse?.data ?? [];
|
|
536
518
|
const filteredTaskOptions = selectedProjectId
|
|
537
519
|
? taskOptions.filter((option) => option.projectId === selectedProjectId)
|
|
538
520
|
: [];
|
|
@@ -540,34 +522,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
540
522
|
filteredTaskOptions.find((option) => option.id === selectedTaskId) ?? null;
|
|
541
523
|
const isEditingEntry = Boolean(editingEntry);
|
|
542
524
|
|
|
543
|
-
const filteredRows =
|
|
544
|
-
() =>
|
|
545
|
-
timesheets.filter((item) => {
|
|
546
|
-
const matchesSearch = !search.trim()
|
|
547
|
-
? true
|
|
548
|
-
: [
|
|
549
|
-
item.collaboratorName,
|
|
550
|
-
item.approverName,
|
|
551
|
-
item.notes,
|
|
552
|
-
...((item.entries ?? []).flatMap((entry) => [
|
|
553
|
-
entry.projectName,
|
|
554
|
-
entry.taskName,
|
|
555
|
-
entry.activityLabel,
|
|
556
|
-
entry.description,
|
|
557
|
-
]) as Array<string | undefined>),
|
|
558
|
-
]
|
|
559
|
-
.filter(Boolean)
|
|
560
|
-
.some((value) =>
|
|
561
|
-
String(value)
|
|
562
|
-
.toLowerCase()
|
|
563
|
-
.includes(search.trim().toLowerCase())
|
|
564
|
-
);
|
|
565
|
-
const matchesStatus =
|
|
566
|
-
statusFilter === 'all' ? true : item.status === statusFilter;
|
|
567
|
-
return matchesSearch && matchesStatus;
|
|
568
|
-
}),
|
|
569
|
-
[timesheets, search, statusFilter]
|
|
570
|
-
);
|
|
525
|
+
const filteredRows = timesheets;
|
|
571
526
|
|
|
572
527
|
const cards = [
|
|
573
528
|
{
|
|
@@ -606,20 +561,6 @@ export default function OperationsTimesheetsPage() {
|
|
|
606
561
|
setIsSheetOpen(true);
|
|
607
562
|
};
|
|
608
563
|
|
|
609
|
-
const openEditEntry = (entry: OperationsTimesheetEntry) => {
|
|
610
|
-
const projectOption = buildProjectOptionFromEntry(entry);
|
|
611
|
-
const taskOption = buildTaskOptionFromEntry(entry);
|
|
612
|
-
|
|
613
|
-
setEditingEntry(entry);
|
|
614
|
-
form.reset(buildEditFormValues(entry));
|
|
615
|
-
setProjectQuery({ search: '', page: 1 });
|
|
616
|
-
setTaskQuery({ search: '', page: 1 });
|
|
617
|
-
setProjectOptions(projectOption ? [projectOption] : []);
|
|
618
|
-
setTaskOptions(taskOption ? [taskOption] : []);
|
|
619
|
-
setIsTaskCreateSheetOpen(false);
|
|
620
|
-
setIsSheetOpen(true);
|
|
621
|
-
};
|
|
622
|
-
|
|
623
564
|
const closeCreateSheet = () => {
|
|
624
565
|
setIsSheetOpen(false);
|
|
625
566
|
setEditingEntry(null);
|
|
@@ -631,6 +572,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
631
572
|
setTaskOptions([]);
|
|
632
573
|
};
|
|
633
574
|
|
|
575
|
+
const openDetails = (timesheet: OperationsTimesheet) => {
|
|
576
|
+
setSelectedTimesheet(timesheet);
|
|
577
|
+
setIsDetailsOpen(true);
|
|
578
|
+
};
|
|
579
|
+
|
|
634
580
|
const canManageRow = (timesheet: OperationsTimesheet) => {
|
|
635
581
|
return Boolean(
|
|
636
582
|
me?.id &&
|
|
@@ -639,14 +585,6 @@ export default function OperationsTimesheetsPage() {
|
|
|
639
585
|
);
|
|
640
586
|
};
|
|
641
587
|
|
|
642
|
-
const canManageEntry = (entry: OperationsTimesheetEntry) => {
|
|
643
|
-
return Boolean(
|
|
644
|
-
me?.id &&
|
|
645
|
-
entry.collaboratorId === me.id &&
|
|
646
|
-
['draft', 'rejected'].includes(entry.status ?? '')
|
|
647
|
-
);
|
|
648
|
-
};
|
|
649
|
-
|
|
650
588
|
const handleQuickEntrySubmit = async (values: QuickEntryFormValues) => {
|
|
651
589
|
const payload = {
|
|
652
590
|
projectId: values.projectId,
|
|
@@ -781,251 +719,260 @@ export default function OperationsTimesheetsPage() {
|
|
|
781
719
|
}
|
|
782
720
|
/>
|
|
783
721
|
|
|
784
|
-
<
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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>
|
|
806
777
|
|
|
807
778
|
<KpiCardsGrid items={cards} columns={3} />
|
|
808
779
|
|
|
809
|
-
{
|
|
810
|
-
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
<div
|
|
814
|
-
|
|
815
|
-
|
|
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
|
+
/>
|
|
816
808
|
</div>
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
{
|
|
822
|
-
|
|
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>
|
|
823
822
|
</div>
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
<div className="divide-y">
|
|
840
|
-
{recentEntries.map((entry) => {
|
|
841
|
-
const isEditableEntry = canManageEntry(entry);
|
|
842
|
-
|
|
843
|
-
return (
|
|
844
|
-
<div
|
|
845
|
-
key={entry.id}
|
|
846
|
-
className={`flex flex-col gap-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between ${
|
|
847
|
-
isEditableEntry
|
|
848
|
-
? 'cursor-pointer hover:bg-muted/30'
|
|
849
|
-
: ''
|
|
850
|
-
}`}
|
|
851
|
-
onClick={
|
|
852
|
-
isEditableEntry ? () => openEditEntry(entry) : undefined
|
|
853
|
-
}
|
|
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)}
|
|
854
838
|
>
|
|
855
|
-
<
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
+
)}
|
|
863
|
+
</div>
|
|
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}
|
|
881
887
|
</div>
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
{
|
|
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) ? (
|
|
899
940
|
<>
|
|
900
941
|
<Button
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
onClick={(event) => {
|
|
906
|
-
event.stopPropagation();
|
|
907
|
-
openEditEntry(entry);
|
|
908
|
-
}}
|
|
909
|
-
aria-label={t('sheet.editTitle')}
|
|
942
|
+
variant="outline"
|
|
943
|
+
size="sm"
|
|
944
|
+
className="cursor-pointer"
|
|
945
|
+
onClick={() => openDetails(timesheet)}
|
|
910
946
|
>
|
|
911
|
-
<
|
|
947
|
+
<Eye className="size-4" />
|
|
948
|
+
{t('actions.viewDetails')}
|
|
912
949
|
</Button>
|
|
913
950
|
<Button
|
|
914
|
-
type="button"
|
|
915
|
-
variant="ghost"
|
|
916
951
|
size="icon"
|
|
917
|
-
|
|
918
|
-
onClick={(event) => {
|
|
919
|
-
event.stopPropagation();
|
|
920
|
-
setEntryToDelete(entry);
|
|
921
|
-
}}
|
|
922
|
-
aria-label={t('messages.confirmDeleteTitle')}
|
|
952
|
+
onClick={() => void submitTimesheet(timesheet.id)}
|
|
923
953
|
>
|
|
924
|
-
<
|
|
954
|
+
<Send className="size-4" />
|
|
925
955
|
</Button>
|
|
926
956
|
</>
|
|
927
|
-
) :
|
|
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>
|
|
967
|
+
)}
|
|
928
968
|
</div>
|
|
929
|
-
</
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
</
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
</div>
|
|
937
|
-
)}
|
|
938
|
-
</CardContent>
|
|
939
|
-
</Card>
|
|
940
|
-
) : null}
|
|
941
|
-
|
|
942
|
-
{filteredRows.length > 0 ? (
|
|
943
|
-
<div className="overflow-x-auto rounded-md border">
|
|
944
|
-
<Table>
|
|
945
|
-
<TableHeader>
|
|
946
|
-
<TableRow>
|
|
947
|
-
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
948
|
-
<TableHead>{commonT('labels.week')}</TableHead>
|
|
949
|
-
<TableHead>{commonT('labels.entries')}</TableHead>
|
|
950
|
-
<TableHead>{commonT('labels.totalHours')}</TableHead>
|
|
951
|
-
<TableHead>{commonT('labels.approver')}</TableHead>
|
|
952
|
-
<TableHead>{commonT('labels.decisionNote')}</TableHead>
|
|
953
|
-
<TableHead>{commonT('labels.status')}</TableHead>
|
|
954
|
-
<TableHead>{commonT('labels.actions')}</TableHead>
|
|
955
|
-
</TableRow>
|
|
956
|
-
</TableHeader>
|
|
957
|
-
<TableBody>
|
|
958
|
-
{filteredRows.map((timesheet) => (
|
|
959
|
-
<TableRow key={timesheet.id} className="hover:bg-muted/30">
|
|
960
|
-
<TableCell>
|
|
961
|
-
<div className="font-medium">
|
|
962
|
-
{timesheet.collaboratorName}
|
|
963
|
-
</div>
|
|
964
|
-
<div className="text-xs text-muted-foreground">
|
|
965
|
-
{timesheet.notes || commonT('labels.noNotes')}
|
|
966
|
-
</div>
|
|
967
|
-
</TableCell>
|
|
968
|
-
<TableCell>
|
|
969
|
-
{formatDateRange(
|
|
970
|
-
timesheet.weekStartDate,
|
|
971
|
-
timesheet.weekEndDate
|
|
972
|
-
)}
|
|
973
|
-
</TableCell>
|
|
974
|
-
<TableCell>
|
|
975
|
-
<div className="font-medium">
|
|
976
|
-
{timesheet.entries?.length ?? 0} {commonT('labels.lines')}
|
|
977
|
-
</div>
|
|
978
|
-
<div className="text-xs text-muted-foreground">
|
|
979
|
-
{(timesheet.entries ?? [])
|
|
980
|
-
.slice(0, 2)
|
|
981
|
-
.map(
|
|
982
|
-
(entry) =>
|
|
983
|
-
[
|
|
984
|
-
entry.projectName,
|
|
985
|
-
entry.taskName || entry.activityLabel,
|
|
986
|
-
]
|
|
987
|
-
.filter(Boolean)
|
|
988
|
-
.join(' • ') || commonT('labels.unassigned')
|
|
989
|
-
)
|
|
990
|
-
.join(', ') || commonT('labels.unassigned')}
|
|
991
|
-
</div>
|
|
992
|
-
</TableCell>
|
|
993
|
-
<TableCell>{formatHours(timesheet.totalHours)}</TableCell>
|
|
994
|
-
<TableCell>
|
|
995
|
-
{timesheet.approverName || commonT('labels.notAssigned')}
|
|
996
|
-
</TableCell>
|
|
997
|
-
<TableCell>
|
|
998
|
-
<div className="text-sm text-muted-foreground">
|
|
999
|
-
{timesheet.decisionNote || commonT('labels.noNotes')}
|
|
1000
|
-
</div>
|
|
1001
|
-
</TableCell>
|
|
1002
|
-
<TableCell>
|
|
1003
|
-
<StatusBadge
|
|
1004
|
-
label={formatEnumLabel(timesheet.status)}
|
|
1005
|
-
className={getStatusBadgeClass(timesheet.status)}
|
|
1006
|
-
/>
|
|
1007
|
-
</TableCell>
|
|
1008
|
-
<TableCell>
|
|
1009
|
-
<div className="flex justify-end gap-2">
|
|
1010
|
-
{canManageRow(timesheet) ? (
|
|
1011
|
-
<Button
|
|
1012
|
-
size="icon"
|
|
1013
|
-
onClick={() => void submitTimesheet(timesheet.id)}
|
|
1014
|
-
>
|
|
1015
|
-
<Send className="size-4" />
|
|
1016
|
-
</Button>
|
|
1017
|
-
) : (
|
|
1018
|
-
<span className="text-xs text-muted-foreground">
|
|
1019
|
-
{commonT('labels.viewOnly')}
|
|
1020
|
-
</span>
|
|
1021
|
-
)}
|
|
1022
|
-
</div>
|
|
1023
|
-
</TableCell>
|
|
1024
|
-
</TableRow>
|
|
1025
|
-
))}
|
|
1026
|
-
</TableBody>
|
|
1027
|
-
</Table>
|
|
1028
|
-
</div>
|
|
969
|
+
</TableCell>
|
|
970
|
+
</TableRow>
|
|
971
|
+
))}
|
|
972
|
+
</TableBody>
|
|
973
|
+
</Table>
|
|
974
|
+
</div>
|
|
975
|
+
)
|
|
1029
976
|
) : (
|
|
1030
977
|
<EmptyState
|
|
1031
978
|
icon={<ClipboardList className="size-12" />}
|
|
@@ -1040,6 +987,18 @@ export default function OperationsTimesheetsPage() {
|
|
|
1040
987
|
/>
|
|
1041
988
|
)}
|
|
1042
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
|
+
|
|
1043
1002
|
<Sheet
|
|
1044
1003
|
open={isSheetOpen}
|
|
1045
1004
|
onOpenChange={(open) => {
|
|
@@ -1301,6 +1260,142 @@ export default function OperationsTimesheetsPage() {
|
|
|
1301
1260
|
onCreated={handleTaskCreated}
|
|
1302
1261
|
/>
|
|
1303
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
|
+
|
|
1304
1399
|
<AlertDialog
|
|
1305
1400
|
open={Boolean(entryToDelete)}
|
|
1306
1401
|
onOpenChange={(open) => {
|