@hed-hog/operations 0.0.317 → 0.0.319
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-collaborator-costs.controller.d.ts +144 -0
- package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
- package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
- package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
- package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +11 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +31 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +23 -0
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-reports.controller.d.ts +199 -0
- package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
- package/dist/controllers/operations-reports.controller.js +53 -0
- package/dist/controllers/operations-reports.controller.js.map +1 -0
- package/dist/controllers/operations-tasks.controller.d.ts +41 -2
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +17 -5
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
- package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-cost.dto.js +88 -0
- package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
- package/dist/dto/create-collaborator.dto.d.ts +0 -1
- package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
- package/dist/dto/create-collaborator.dto.js +0 -6
- package/dist/dto/create-collaborator.dto.js.map +1 -1
- package/dist/dto/create-cost-type.dto.d.ts +13 -0
- package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
- package/dist/dto/create-cost-type.dto.js +87 -0
- package/dist/dto/create-cost-type.dto.js.map +1 -0
- package/dist/dto/list-approvals.dto.d.ts +2 -0
- package/dist/dto/list-approvals.dto.d.ts.map +1 -1
- package/dist/dto/list-approvals.dto.js +10 -0
- package/dist/dto/list-approvals.dto.js.map +1 -1
- package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
- package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-costs.dto.js +23 -0
- package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
- package/dist/dto/list-cost-types.dto.d.ts +6 -0
- package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
- package/dist/dto/list-cost-types.dto.js +35 -0
- package/dist/dto/list-cost-types.dto.js.map +1 -0
- package/dist/dto/list-my-projects.dto.d.ts +5 -0
- package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
- package/dist/dto/list-my-projects.dto.js +23 -0
- package/dist/dto/list-my-projects.dto.js.map +1 -0
- package/dist/dto/list-my-tasks.dto.d.ts +6 -0
- package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
- package/dist/dto/list-my-tasks.dto.js +33 -0
- package/dist/dto/list-my-tasks.dto.js.map +1 -0
- package/dist/dto/list-projects.dto.d.ts +1 -0
- package/dist/dto/list-projects.dto.d.ts.map +1 -1
- package/dist/dto/list-projects.dto.js +7 -0
- package/dist/dto/list-projects.dto.js.map +1 -1
- package/dist/dto/list-reports.dto.d.ts +16 -0
- package/dist/dto/list-reports.dto.d.ts.map +1 -0
- package/dist/dto/list-reports.dto.js +75 -0
- package/dist/dto/list-reports.dto.js.map +1 -0
- package/dist/dto/list-tasks.dto.d.ts +2 -0
- package/dist/dto/list-tasks.dto.d.ts.map +1 -1
- package/dist/dto/list-tasks.dto.js +12 -0
- package/dist/dto/list-tasks.dto.js.map +1 -1
- package/dist/dto/list-timesheets.dto.d.ts +2 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
- package/dist/dto/list-timesheets.dto.js +10 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -1
- package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-cost.dto.js +9 -0
- package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
- package/dist/dto/update-task.dto.d.ts +1 -0
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +6 -0
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +4 -0
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +457 -3
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1445 -208
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +31 -7
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +112 -7
- package/hedhog/data/operations_cost_type.yaml +166 -0
- package/hedhog/data/route.yaml +185 -0
- package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +94 -15
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
- package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
- package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +229 -3
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
- package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
- package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
- package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
- package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
- package/hedhog/frontend/messages/en.json +234 -25
- package/hedhog/frontend/messages/pt.json +234 -25
- package/hedhog/table/operations_collaborator.yaml +0 -4
- package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
- package/hedhog/table/operations_collaborator_cost.yaml +56 -0
- package/hedhog/table/operations_cost_type.yaml +38 -0
- package/package.json +6 -6
- package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
- package/src/controllers/operations-collaborators.controller.ts +19 -8
- package/src/controllers/operations-projects.controller.ts +19 -8
- package/src/controllers/operations-reports.controller.ts +32 -0
- package/src/controllers/operations-tasks.controller.ts +32 -12
- package/src/dto/create-collaborator-cost.dto.ts +78 -0
- package/src/dto/create-collaborator.dto.ts +9 -14
- package/src/dto/create-cost-type.dto.ts +62 -0
- package/src/dto/list-approvals.dto.ts +8 -0
- package/src/dto/list-collaborator-costs.dto.ts +8 -0
- package/src/dto/list-cost-types.dto.ts +19 -0
- package/src/dto/list-my-projects.dto.ts +8 -0
- package/src/dto/list-my-tasks.dto.ts +17 -0
- package/src/dto/list-projects.dto.ts +7 -1
- package/src/dto/list-reports.dto.ts +51 -0
- package/src/dto/list-tasks.dto.ts +11 -1
- package/src/dto/list-timesheets.dto.ts +8 -0
- package/src/dto/update-collaborator-cost.dto.ts +4 -0
- package/src/dto/update-task.dto.ts +6 -0
- package/src/operations.module.ts +7 -3
- package/src/operations.service.spec.ts +45 -7
- package/src/operations.service.ts +1992 -225
|
@@ -49,21 +49,28 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
|
49
49
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
50
50
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
51
51
|
import {
|
|
52
|
+
CalendarDays,
|
|
52
53
|
ClipboardList,
|
|
53
54
|
Clock3,
|
|
54
55
|
Eye,
|
|
55
56
|
LayoutGrid,
|
|
56
57
|
List,
|
|
57
58
|
Loader2,
|
|
59
|
+
Pencil,
|
|
58
60
|
Plus,
|
|
59
61
|
Send,
|
|
62
|
+
Trash2,
|
|
60
63
|
} from 'lucide-react';
|
|
61
64
|
import { useTranslations } from 'next-intl';
|
|
62
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
65
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
63
66
|
import { useForm, useWatch } from 'react-hook-form';
|
|
64
67
|
import { z } from 'zod';
|
|
65
68
|
|
|
66
69
|
import { AsyncOptionsCombobox } from '../_components/async-options-combobox';
|
|
70
|
+
import {
|
|
71
|
+
OperationsCalendarView,
|
|
72
|
+
type OperationsCalendarItem,
|
|
73
|
+
} from '../_components/operations-calendar-view';
|
|
67
74
|
import { OperationsHeader } from '../_components/operations-header';
|
|
68
75
|
import { StatusBadge } from '../_components/status-badge';
|
|
69
76
|
import { TimesheetTaskCreateSheet } from '../_components/timesheet-task-create-sheet';
|
|
@@ -82,7 +89,7 @@ import type {
|
|
|
82
89
|
PaginatedResponse,
|
|
83
90
|
} from '../_lib/types';
|
|
84
91
|
import {
|
|
85
|
-
|
|
92
|
+
formatDate,
|
|
86
93
|
formatEnumLabel,
|
|
87
94
|
formatHours,
|
|
88
95
|
getStatusBadgeClass,
|
|
@@ -203,6 +210,22 @@ function buildFollowUpFormValues(
|
|
|
203
210
|
};
|
|
204
211
|
}
|
|
205
212
|
|
|
213
|
+
function buildEntryFormValues(
|
|
214
|
+
entry: OperationsTimesheetEntry
|
|
215
|
+
): QuickEntryFormValues {
|
|
216
|
+
return {
|
|
217
|
+
projectId: entry.projectId ?? undefined,
|
|
218
|
+
taskId: entry.taskId ?? undefined,
|
|
219
|
+
workDate: String(entry.workDate).match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? '',
|
|
220
|
+
duration:
|
|
221
|
+
typeof entry.durationMinutes === 'number' && entry.durationMinutes > 0
|
|
222
|
+
? Number((entry.durationMinutes / 60).toFixed(2))
|
|
223
|
+
: Number(entry.hours ?? 0),
|
|
224
|
+
unit: 'hours',
|
|
225
|
+
description: entry.description ?? '',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
206
229
|
export default function OperationsTimesheetsPage() {
|
|
207
230
|
const t = useTranslations('operations.TimesheetsPage');
|
|
208
231
|
const commonT = useTranslations('operations.Common');
|
|
@@ -210,15 +233,25 @@ export default function OperationsTimesheetsPage() {
|
|
|
210
233
|
const access = useOperationsAccess();
|
|
211
234
|
const [search, setSearch] = useState('');
|
|
212
235
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
236
|
+
const [dateFrom, setDateFrom] = useState('');
|
|
237
|
+
const [dateTo, setDateTo] = useState('');
|
|
238
|
+
const [calendarYear, setCalendarYear] = useState(() =>
|
|
239
|
+
new Date().getFullYear()
|
|
240
|
+
);
|
|
241
|
+
const [calendarMonth, setCalendarMonth] = useState(
|
|
242
|
+
() => new Date().getMonth() + 1
|
|
243
|
+
);
|
|
213
244
|
const [page, setPage] = useState(1);
|
|
214
245
|
const [pageSize, setPageSize] = useState(12);
|
|
215
|
-
const [viewMode, setViewMode] = useState<'table' | 'cards'>(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
const [viewMode, setViewMode] = useState<'table' | 'cards' | 'calendar'>(
|
|
247
|
+
() => {
|
|
248
|
+
if (typeof window === 'undefined') return 'table';
|
|
249
|
+
const saved = window.localStorage.getItem(
|
|
250
|
+
'operations-timesheets-view-mode'
|
|
251
|
+
);
|
|
252
|
+
return saved === 'cards' || saved === 'calendar' ? saved : 'table';
|
|
253
|
+
}
|
|
254
|
+
);
|
|
222
255
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
223
256
|
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
|
224
257
|
const [isTaskCreateSheetOpen, setIsTaskCreateSheetOpen] = useState(false);
|
|
@@ -285,6 +318,8 @@ export default function OperationsTimesheetsPage() {
|
|
|
285
318
|
currentLocaleCode,
|
|
286
319
|
search,
|
|
287
320
|
statusFilter,
|
|
321
|
+
dateFrom,
|
|
322
|
+
dateTo,
|
|
288
323
|
page,
|
|
289
324
|
pageSize,
|
|
290
325
|
],
|
|
@@ -295,6 +330,8 @@ export default function OperationsTimesheetsPage() {
|
|
|
295
330
|
});
|
|
296
331
|
if (search.trim()) params.set('search', search.trim());
|
|
297
332
|
if (statusFilter !== 'all') params.set('status', statusFilter);
|
|
333
|
+
if (dateFrom) params.set('dateFrom', dateFrom);
|
|
334
|
+
if (dateTo) params.set('dateTo', dateTo);
|
|
298
335
|
return fetchOperations<PaginatedResponse<OperationsTimesheet>>(
|
|
299
336
|
request,
|
|
300
337
|
`/operations/timesheets?${params.toString()}`
|
|
@@ -305,9 +342,61 @@ export default function OperationsTimesheetsPage() {
|
|
|
305
342
|
|
|
306
343
|
const timesheets = timesheetsResponse?.data ?? [];
|
|
307
344
|
|
|
345
|
+
const calendarDateFrom = `${String(calendarYear)}-${String(calendarMonth).padStart(2, '0')}-01`;
|
|
346
|
+
const calendarDateTo = (() => {
|
|
347
|
+
const lastDay = new Date(calendarYear, calendarMonth, 0).getDate();
|
|
348
|
+
return `${String(calendarYear)}-${String(calendarMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
349
|
+
})();
|
|
350
|
+
|
|
351
|
+
const { data: calendarTimesheetsResponse } = useQuery<
|
|
352
|
+
PaginatedResponse<OperationsTimesheet>
|
|
353
|
+
>({
|
|
354
|
+
queryKey: [
|
|
355
|
+
'operations-timesheets-calendar',
|
|
356
|
+
currentLocaleCode,
|
|
357
|
+
search,
|
|
358
|
+
statusFilter,
|
|
359
|
+
calendarYear,
|
|
360
|
+
calendarMonth,
|
|
361
|
+
],
|
|
362
|
+
enabled: viewMode === 'calendar',
|
|
363
|
+
queryFn: () => {
|
|
364
|
+
const params = new URLSearchParams({
|
|
365
|
+
sortField: 'weekStartDate',
|
|
366
|
+
sortOrder: 'desc',
|
|
367
|
+
dateFrom: calendarDateFrom,
|
|
368
|
+
dateTo: calendarDateTo,
|
|
369
|
+
});
|
|
370
|
+
if (search.trim()) params.set('search', search.trim());
|
|
371
|
+
if (statusFilter !== 'all') params.set('status', statusFilter);
|
|
372
|
+
return fetchOperations<PaginatedResponse<OperationsTimesheet>>(
|
|
373
|
+
request,
|
|
374
|
+
`/operations/timesheets?${params.toString()}`
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
placeholderData: (previous) => previous,
|
|
378
|
+
});
|
|
379
|
+
const calendarTimesheets = useMemo(
|
|
380
|
+
() => calendarTimesheetsResponse?.data ?? [],
|
|
381
|
+
[calendarTimesheetsResponse]
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const getTimesheetStatusLabel = useCallback(
|
|
385
|
+
(status?: string | null) => {
|
|
386
|
+
if (!status) {
|
|
387
|
+
return commonT('labels.notAvailable');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return t.has(`statuses.${status}`)
|
|
391
|
+
? t(`statuses.${status}`)
|
|
392
|
+
: formatEnumLabel(status);
|
|
393
|
+
},
|
|
394
|
+
[commonT, t]
|
|
395
|
+
);
|
|
396
|
+
|
|
308
397
|
const handleViewModeChange = (value: string) => {
|
|
309
|
-
if (value !== 'table' && value !== 'cards') return;
|
|
310
|
-
setViewMode(value as 'table' | 'cards');
|
|
398
|
+
if (value !== 'table' && value !== 'cards' && value !== 'calendar') return;
|
|
399
|
+
setViewMode(value as 'table' | 'cards' | 'calendar');
|
|
311
400
|
if (typeof window !== 'undefined') {
|
|
312
401
|
window.localStorage.setItem('operations-timesheets-view-mode', value);
|
|
313
402
|
}
|
|
@@ -523,6 +612,31 @@ export default function OperationsTimesheetsPage() {
|
|
|
523
612
|
const isEditingEntry = Boolean(editingEntry);
|
|
524
613
|
|
|
525
614
|
const filteredRows = timesheets;
|
|
615
|
+
const calendarItems = useMemo<OperationsCalendarItem[]>(
|
|
616
|
+
() =>
|
|
617
|
+
calendarTimesheets.map((timesheet) => ({
|
|
618
|
+
id: `timesheet-${timesheet.id}`,
|
|
619
|
+
date:
|
|
620
|
+
String(timesheet.weekStartDate).match(/(\d{4}-\d{2}-\d{2})/)?.[1] ??
|
|
621
|
+
timesheet.weekStartDate,
|
|
622
|
+
kind: 'timesheet',
|
|
623
|
+
title: `${formatHours(timesheet.totalHours)} • ${timesheet.collaboratorName}`,
|
|
624
|
+
subtitle:
|
|
625
|
+
(timesheet.entries ?? [])
|
|
626
|
+
.slice(0, 2)
|
|
627
|
+
.map(
|
|
628
|
+
(entry) =>
|
|
629
|
+
[entry.projectName, entry.taskName || entry.activityLabel]
|
|
630
|
+
.filter(Boolean)
|
|
631
|
+
.join(' • ') || commonT('labels.unassigned')
|
|
632
|
+
)
|
|
633
|
+
.join(', ') || commonT('labels.unassigned'),
|
|
634
|
+
meta: `${timesheet.entries?.length ?? 0} ${commonT('labels.lines')}`,
|
|
635
|
+
statusLabel: getTimesheetStatusLabel(timesheet.status),
|
|
636
|
+
badgeClassName: getStatusBadgeClass(timesheet.status),
|
|
637
|
+
})),
|
|
638
|
+
[calendarTimesheets, commonT, getTimesheetStatusLabel]
|
|
639
|
+
);
|
|
526
640
|
|
|
527
641
|
const cards = [
|
|
528
642
|
{
|
|
@@ -577,7 +691,93 @@ export default function OperationsTimesheetsPage() {
|
|
|
577
691
|
setIsDetailsOpen(true);
|
|
578
692
|
};
|
|
579
693
|
|
|
694
|
+
const openEditEntry = (entry: OperationsTimesheetEntry) => {
|
|
695
|
+
setEditingEntry(entry);
|
|
696
|
+
form.reset(buildEntryFormValues(entry));
|
|
697
|
+
setProjectQuery({ search: '', page: 1 });
|
|
698
|
+
setTaskQuery({ search: '', page: 1 });
|
|
699
|
+
setProjectOptions((current) =>
|
|
700
|
+
appendUniqueById(
|
|
701
|
+
current,
|
|
702
|
+
entry.projectId && entry.projectName
|
|
703
|
+
? [
|
|
704
|
+
{
|
|
705
|
+
id: entry.projectId,
|
|
706
|
+
label:
|
|
707
|
+
[entry.projectCode, entry.projectName]
|
|
708
|
+
.filter(Boolean)
|
|
709
|
+
.join(' • ') || entry.projectName,
|
|
710
|
+
name: entry.projectName,
|
|
711
|
+
code: entry.projectCode,
|
|
712
|
+
status: entry.status ?? 'submitted',
|
|
713
|
+
},
|
|
714
|
+
]
|
|
715
|
+
: []
|
|
716
|
+
)
|
|
717
|
+
);
|
|
718
|
+
setTaskOptions((current) =>
|
|
719
|
+
appendUniqueById(
|
|
720
|
+
current,
|
|
721
|
+
entry.taskId && entry.projectId
|
|
722
|
+
? [
|
|
723
|
+
{
|
|
724
|
+
id: entry.taskId,
|
|
725
|
+
label:
|
|
726
|
+
entry.taskName ||
|
|
727
|
+
entry.activityLabel ||
|
|
728
|
+
commonT('labels.unassigned'),
|
|
729
|
+
name:
|
|
730
|
+
entry.taskName ||
|
|
731
|
+
entry.activityLabel ||
|
|
732
|
+
commonT('labels.unassigned'),
|
|
733
|
+
status: entry.status ?? 'submitted',
|
|
734
|
+
projectId: entry.projectId,
|
|
735
|
+
projectAssignmentId: entry.projectAssignmentId ?? 0,
|
|
736
|
+
projectName:
|
|
737
|
+
entry.projectName || commonT('labels.unassigned'),
|
|
738
|
+
},
|
|
739
|
+
]
|
|
740
|
+
: []
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
setEntryToDelete(null);
|
|
744
|
+
setIsDetailsOpen(false);
|
|
745
|
+
setIsTaskCreateSheetOpen(false);
|
|
746
|
+
setIsSheetOpen(true);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const openTimesheetForDate = (date: string) => {
|
|
750
|
+
const existingTimesheet =
|
|
751
|
+
calendarTimesheets.find((item) => item.weekStartDate === date) ??
|
|
752
|
+
timesheets.find((item) => item.weekStartDate === date);
|
|
753
|
+
|
|
754
|
+
if (existingTimesheet) {
|
|
755
|
+
openDetails(existingTimesheet);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
setEditingEntry(null);
|
|
760
|
+
form.reset({
|
|
761
|
+
...buildDefaultFormValues(),
|
|
762
|
+
workDate: date,
|
|
763
|
+
});
|
|
764
|
+
setProjectQuery({ search: '', page: 1 });
|
|
765
|
+
setTaskQuery({ search: '', page: 1 });
|
|
766
|
+
setProjectOptions([]);
|
|
767
|
+
setTaskOptions([]);
|
|
768
|
+
setIsTaskCreateSheetOpen(false);
|
|
769
|
+
setIsSheetOpen(true);
|
|
770
|
+
};
|
|
771
|
+
|
|
580
772
|
const canManageRow = (timesheet: OperationsTimesheet) => {
|
|
773
|
+
return Boolean(
|
|
774
|
+
me?.id &&
|
|
775
|
+
timesheet.collaboratorId === me.id &&
|
|
776
|
+
timesheet.status !== 'approved'
|
|
777
|
+
);
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const canSubmitTimesheet = (timesheet: OperationsTimesheet) => {
|
|
581
781
|
return Boolean(
|
|
582
782
|
me?.id &&
|
|
583
783
|
timesheet.collaboratorId === me.id &&
|
|
@@ -742,12 +942,41 @@ export default function OperationsTimesheetsPage() {
|
|
|
742
942
|
placeholder: commonT('labels.status'),
|
|
743
943
|
options: [
|
|
744
944
|
{ value: 'all', label: commonT('filters.allStatuses') },
|
|
745
|
-
{ value: 'draft', label:
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
945
|
+
{ value: 'draft', label: getTimesheetStatusLabel('draft') },
|
|
946
|
+
{
|
|
947
|
+
value: 'submitted',
|
|
948
|
+
label: getTimesheetStatusLabel('submitted'),
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
value: 'approved',
|
|
952
|
+
label: getTimesheetStatusLabel('approved'),
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
value: 'rejected',
|
|
956
|
+
label: getTimesheetStatusLabel('rejected'),
|
|
957
|
+
},
|
|
749
958
|
],
|
|
750
959
|
},
|
|
960
|
+
{
|
|
961
|
+
id: 'dateFrom',
|
|
962
|
+
type: 'date',
|
|
963
|
+
value: dateFrom,
|
|
964
|
+
onChange: (value) => {
|
|
965
|
+
setDateFrom(value);
|
|
966
|
+
setPage(1);
|
|
967
|
+
},
|
|
968
|
+
label: commonT('filters.dateFrom'),
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
id: 'dateTo',
|
|
972
|
+
type: 'date',
|
|
973
|
+
value: dateTo,
|
|
974
|
+
onChange: (value) => {
|
|
975
|
+
setDateTo(value);
|
|
976
|
+
setPage(1);
|
|
977
|
+
},
|
|
978
|
+
label: commonT('filters.dateTo'),
|
|
979
|
+
},
|
|
751
980
|
]}
|
|
752
981
|
/>
|
|
753
982
|
</div>
|
|
@@ -771,14 +1000,37 @@ export default function OperationsTimesheetsPage() {
|
|
|
771
1000
|
<LayoutGrid className="h-4 w-4" />
|
|
772
1001
|
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
773
1002
|
</ToggleGroupItem>
|
|
1003
|
+
<ToggleGroupItem value="calendar" className="gap-1.5 px-2.5">
|
|
1004
|
+
<CalendarDays className="h-4 w-4" />
|
|
1005
|
+
<span className="hidden sm:inline">{t('viewModeCalendar')}</span>
|
|
1006
|
+
</ToggleGroupItem>
|
|
774
1007
|
</ToggleGroup>
|
|
775
1008
|
</div>
|
|
776
1009
|
</div>
|
|
777
1010
|
|
|
778
1011
|
<KpiCardsGrid items={cards} columns={3} />
|
|
779
1012
|
|
|
780
|
-
{timesheets.length > 0 ? (
|
|
781
|
-
viewMode === '
|
|
1013
|
+
{(viewMode === 'calendar' ? true : timesheets.length > 0) ? (
|
|
1014
|
+
viewMode === 'calendar' ? (
|
|
1015
|
+
<OperationsCalendarView
|
|
1016
|
+
locale={currentLocaleCode}
|
|
1017
|
+
items={calendarItems}
|
|
1018
|
+
emptyLabel={t('emptyDescription')}
|
|
1019
|
+
actionLabel={t('actions.viewDetails')}
|
|
1020
|
+
onMonthChange={(year, month) => {
|
|
1021
|
+
setCalendarYear(year);
|
|
1022
|
+
setCalendarMonth(month);
|
|
1023
|
+
}}
|
|
1024
|
+
onOpenItem={(item) => {
|
|
1025
|
+
openTimesheetForDate(item.date);
|
|
1026
|
+
}}
|
|
1027
|
+
onDateSelect={(date, items) => {
|
|
1028
|
+
if (items.some((item) => item.kind === 'timesheet')) {
|
|
1029
|
+
openTimesheetForDate(date);
|
|
1030
|
+
}
|
|
1031
|
+
}}
|
|
1032
|
+
/>
|
|
1033
|
+
) : viewMode === 'cards' ? (
|
|
782
1034
|
<div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
|
783
1035
|
{timesheets.map((timesheet) => (
|
|
784
1036
|
<div
|
|
@@ -791,18 +1043,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
791
1043
|
{timesheet.collaboratorName}
|
|
792
1044
|
</div>
|
|
793
1045
|
<div className="truncate text-xs text-muted-foreground">
|
|
794
|
-
{
|
|
795
|
-
timesheet.weekStartDate,
|
|
796
|
-
timesheet.weekEndDate
|
|
797
|
-
)}
|
|
1046
|
+
{formatDate(timesheet.weekStartDate)}
|
|
798
1047
|
</div>
|
|
799
1048
|
</div>
|
|
800
1049
|
<StatusBadge
|
|
801
|
-
label={
|
|
802
|
-
t.has(`statuses.${timesheet.status}`)
|
|
803
|
-
? t(`statuses.${timesheet.status}`)
|
|
804
|
-
: formatEnumLabel(timesheet.status)
|
|
805
|
-
}
|
|
1050
|
+
label={getTimesheetStatusLabel(timesheet.status)}
|
|
806
1051
|
className={getStatusBadgeClass(timesheet.status)}
|
|
807
1052
|
/>
|
|
808
1053
|
</div>
|
|
@@ -839,13 +1084,15 @@ export default function OperationsTimesheetsPage() {
|
|
|
839
1084
|
<Eye className="size-4" />
|
|
840
1085
|
{t('actions.viewDetails')}
|
|
841
1086
|
</Button>
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1087
|
+
{canSubmitTimesheet(timesheet) ? (
|
|
1088
|
+
<Button
|
|
1089
|
+
size="sm"
|
|
1090
|
+
onClick={() => void submitTimesheet(timesheet.id)}
|
|
1091
|
+
>
|
|
1092
|
+
<Send className="size-4" />
|
|
1093
|
+
{commonT('actions.submit')}
|
|
1094
|
+
</Button>
|
|
1095
|
+
) : null}
|
|
849
1096
|
</div>
|
|
850
1097
|
) : (
|
|
851
1098
|
<div className="flex justify-end border-t border-border/60 pt-3">
|
|
@@ -869,7 +1116,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
869
1116
|
<TableHeader>
|
|
870
1117
|
<TableRow>
|
|
871
1118
|
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
872
|
-
<TableHead>{commonT('labels.
|
|
1119
|
+
<TableHead>{commonT('labels.workDate')}</TableHead>
|
|
873
1120
|
<TableHead>{commonT('labels.entries')}</TableHead>
|
|
874
1121
|
<TableHead>{commonT('labels.totalHours')}</TableHead>
|
|
875
1122
|
<TableHead>{commonT('labels.approver')}</TableHead>
|
|
@@ -889,12 +1136,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
889
1136
|
{timesheet.notes || commonT('labels.noNotes')}
|
|
890
1137
|
</div>
|
|
891
1138
|
</TableCell>
|
|
892
|
-
<TableCell>
|
|
893
|
-
{formatDateRange(
|
|
894
|
-
timesheet.weekStartDate,
|
|
895
|
-
timesheet.weekEndDate
|
|
896
|
-
)}
|
|
897
|
-
</TableCell>
|
|
1139
|
+
<TableCell>{formatDate(timesheet.weekStartDate)}</TableCell>
|
|
898
1140
|
<TableCell>
|
|
899
1141
|
<div className="font-medium">
|
|
900
1142
|
{timesheet.entries?.length ?? 0}{' '}
|
|
@@ -926,11 +1168,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
926
1168
|
</TableCell>
|
|
927
1169
|
<TableCell>
|
|
928
1170
|
<StatusBadge
|
|
929
|
-
label={
|
|
930
|
-
t.has(`statuses.${timesheet.status}`)
|
|
931
|
-
? t(`statuses.${timesheet.status}`)
|
|
932
|
-
: formatEnumLabel(timesheet.status)
|
|
933
|
-
}
|
|
1171
|
+
label={getTimesheetStatusLabel(timesheet.status)}
|
|
934
1172
|
className={getStatusBadgeClass(timesheet.status)}
|
|
935
1173
|
/>
|
|
936
1174
|
</TableCell>
|
|
@@ -947,12 +1185,14 @@ export default function OperationsTimesheetsPage() {
|
|
|
947
1185
|
<Eye className="size-4" />
|
|
948
1186
|
{t('actions.viewDetails')}
|
|
949
1187
|
</Button>
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1188
|
+
{canSubmitTimesheet(timesheet) ? (
|
|
1189
|
+
<Button
|
|
1190
|
+
size="icon"
|
|
1191
|
+
onClick={() => void submitTimesheet(timesheet.id)}
|
|
1192
|
+
>
|
|
1193
|
+
<Send className="size-4" />
|
|
1194
|
+
</Button>
|
|
1195
|
+
) : null}
|
|
956
1196
|
</>
|
|
957
1197
|
) : (
|
|
958
1198
|
<Button
|
|
@@ -1245,7 +1485,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
1245
1485
|
onCancel={closeCreateSheet}
|
|
1246
1486
|
cancelLabel={commonT('actions.cancel')}
|
|
1247
1487
|
submitType="submit"
|
|
1248
|
-
submitLabel={
|
|
1488
|
+
submitLabel={
|
|
1489
|
+
isEditingEntry
|
|
1490
|
+
? commonT('actions.save')
|
|
1491
|
+
: commonT('actions.submit')
|
|
1492
|
+
}
|
|
1249
1493
|
submitDisabled={form.formState.isSubmitting}
|
|
1250
1494
|
/>
|
|
1251
1495
|
</form>
|
|
@@ -1276,10 +1520,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
1276
1520
|
{selectedTimesheet.collaboratorName}
|
|
1277
1521
|
</div>
|
|
1278
1522
|
<div className="mt-1 text-sm text-muted-foreground">
|
|
1279
|
-
{
|
|
1280
|
-
selectedTimesheet.weekStartDate,
|
|
1281
|
-
selectedTimesheet.weekEndDate
|
|
1282
|
-
)}
|
|
1523
|
+
{formatDate(selectedTimesheet.weekStartDate)}
|
|
1283
1524
|
</div>
|
|
1284
1525
|
</div>
|
|
1285
1526
|
<StatusBadge
|
|
@@ -1324,8 +1565,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
1324
1565
|
{t('details.period')}
|
|
1325
1566
|
</div>
|
|
1326
1567
|
<div className="mt-1 text-sm">
|
|
1327
|
-
{formatDateLabel(selectedTimesheet.weekStartDate)}
|
|
1328
|
-
{formatDateLabel(selectedTimesheet.weekEndDate)}
|
|
1568
|
+
{formatDateLabel(selectedTimesheet.weekStartDate)}
|
|
1329
1569
|
</div>
|
|
1330
1570
|
</div>
|
|
1331
1571
|
</div>
|
|
@@ -1382,6 +1622,30 @@ export default function OperationsTimesheetsPage() {
|
|
|
1382
1622
|
<div className="mt-2 text-sm">
|
|
1383
1623
|
{entry.description || commonT('labels.noNotes')}
|
|
1384
1624
|
</div>
|
|
1625
|
+
{canManageRow(selectedTimesheet) ? (
|
|
1626
|
+
<div className="mt-3 flex justify-end gap-2 border-t border-border/60 pt-3">
|
|
1627
|
+
<Button
|
|
1628
|
+
type="button"
|
|
1629
|
+
variant="outline"
|
|
1630
|
+
size="sm"
|
|
1631
|
+
className="cursor-pointer"
|
|
1632
|
+
onClick={() => openEditEntry(entry)}
|
|
1633
|
+
>
|
|
1634
|
+
<Pencil className="size-4" />
|
|
1635
|
+
{commonT('actions.edit')}
|
|
1636
|
+
</Button>
|
|
1637
|
+
<Button
|
|
1638
|
+
type="button"
|
|
1639
|
+
variant="outline"
|
|
1640
|
+
size="sm"
|
|
1641
|
+
className="cursor-pointer text-destructive hover:bg-destructive/10"
|
|
1642
|
+
onClick={() => setEntryToDelete(entry)}
|
|
1643
|
+
>
|
|
1644
|
+
<Trash2 className="size-4" />
|
|
1645
|
+
{commonT('actions.delete')}
|
|
1646
|
+
</Button>
|
|
1647
|
+
</div>
|
|
1648
|
+
) : null}
|
|
1385
1649
|
</div>
|
|
1386
1650
|
))
|
|
1387
1651
|
) : (
|