@hed-hog/operations 0.0.318 → 0.0.321

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.
Files changed (138) hide show
  1. package/dist/controllers/operations-collaborator-costs.controller.d.ts +144 -0
  2. package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
  3. package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
  4. package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
  5. package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +11 -0
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +9 -9
  10. package/dist/controllers/operations-projects.controller.d.ts +31 -0
  11. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  12. package/dist/controllers/operations-projects.controller.js +23 -0
  13. package/dist/controllers/operations-projects.controller.js.map +1 -1
  14. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  15. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  16. package/dist/controllers/operations-reports.controller.js +53 -0
  17. package/dist/controllers/operations-reports.controller.js.map +1 -0
  18. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  19. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  20. package/dist/controllers/operations-tasks.controller.js +17 -5
  21. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  22. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  23. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  24. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  25. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  26. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  27. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  28. package/dist/dto/create-collaborator.dto.js +0 -6
  29. package/dist/dto/create-collaborator.dto.js.map +1 -1
  30. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  31. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  32. package/dist/dto/create-cost-type.dto.js +87 -0
  33. package/dist/dto/create-cost-type.dto.js.map +1 -0
  34. package/dist/dto/list-approvals.dto.d.ts +2 -0
  35. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  36. package/dist/dto/list-approvals.dto.js +10 -0
  37. package/dist/dto/list-approvals.dto.js.map +1 -1
  38. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  39. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  40. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  41. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  42. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  43. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  44. package/dist/dto/list-cost-types.dto.js +35 -0
  45. package/dist/dto/list-cost-types.dto.js.map +1 -0
  46. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  47. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  48. package/dist/dto/list-my-projects.dto.js +23 -0
  49. package/dist/dto/list-my-projects.dto.js.map +1 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  51. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  52. package/dist/dto/list-my-tasks.dto.js +33 -0
  53. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  54. package/dist/dto/list-projects.dto.d.ts +1 -0
  55. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  56. package/dist/dto/list-projects.dto.js +7 -0
  57. package/dist/dto/list-projects.dto.js.map +1 -1
  58. package/dist/dto/list-reports.dto.d.ts +16 -0
  59. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  60. package/dist/dto/list-reports.dto.js +75 -0
  61. package/dist/dto/list-reports.dto.js.map +1 -0
  62. package/dist/dto/list-tasks.dto.d.ts +2 -0
  63. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  64. package/dist/dto/list-tasks.dto.js +12 -0
  65. package/dist/dto/list-tasks.dto.js.map +1 -1
  66. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  67. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  68. package/dist/dto/list-timesheets.dto.js +10 -0
  69. package/dist/dto/list-timesheets.dto.js.map +1 -1
  70. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  71. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  72. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  73. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  74. package/dist/dto/update-task.dto.d.ts +1 -0
  75. package/dist/dto/update-task.dto.d.ts.map +1 -1
  76. package/dist/dto/update-task.dto.js +6 -0
  77. package/dist/dto/update-task.dto.js.map +1 -1
  78. package/dist/operations.module.d.ts.map +1 -1
  79. package/dist/operations.module.js +4 -0
  80. package/dist/operations.module.js.map +1 -1
  81. package/dist/operations.service.d.ts +457 -3
  82. package/dist/operations.service.d.ts.map +1 -1
  83. package/dist/operations.service.js +1445 -208
  84. package/dist/operations.service.js.map +1 -1
  85. package/dist/operations.service.spec.js +31 -7
  86. package/dist/operations.service.spec.js.map +1 -1
  87. package/hedhog/data/menu.yaml +112 -7
  88. package/hedhog/data/operations_cost_type.yaml +166 -0
  89. package/hedhog/data/route.yaml +185 -0
  90. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  91. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +80 -1
  92. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  93. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  94. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  95. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  96. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  97. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  98. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  99. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  100. package/hedhog/frontend/app/_lib/types.ts.ejs +227 -1
  101. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  102. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  103. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  104. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  105. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  106. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  107. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  108. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  109. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  110. package/hedhog/frontend/messages/en.json +234 -25
  111. package/hedhog/frontend/messages/pt.json +234 -25
  112. package/hedhog/table/operations_collaborator.yaml +0 -4
  113. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  114. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  115. package/hedhog/table/operations_cost_type.yaml +38 -0
  116. package/package.json +7 -7
  117. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  118. package/src/controllers/operations-collaborators.controller.ts +19 -8
  119. package/src/controllers/operations-projects.controller.ts +19 -8
  120. package/src/controllers/operations-reports.controller.ts +32 -0
  121. package/src/controllers/operations-tasks.controller.ts +32 -12
  122. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  123. package/src/dto/create-collaborator.dto.ts +9 -14
  124. package/src/dto/create-cost-type.dto.ts +62 -0
  125. package/src/dto/list-approvals.dto.ts +8 -0
  126. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  127. package/src/dto/list-cost-types.dto.ts +19 -0
  128. package/src/dto/list-my-projects.dto.ts +8 -0
  129. package/src/dto/list-my-tasks.dto.ts +17 -0
  130. package/src/dto/list-projects.dto.ts +7 -1
  131. package/src/dto/list-reports.dto.ts +51 -0
  132. package/src/dto/list-tasks.dto.ts +11 -1
  133. package/src/dto/list-timesheets.dto.ts +8 -0
  134. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  135. package/src/dto/update-task.dto.ts +6 -0
  136. package/src/operations.module.ts +4 -0
  137. package/src/operations.service.spec.ts +45 -7
  138. package/src/operations.service.ts +1988 -221
@@ -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
- formatDateRange,
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
- 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
- });
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: formatEnumLabel('draft') },
746
- { value: 'submitted', label: formatEnumLabel('submitted') },
747
- { value: 'approved', label: formatEnumLabel('approved') },
748
- { value: 'rejected', label: formatEnumLabel('rejected') },
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 === 'cards' ? (
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
- {formatDateRange(
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
- <Button
843
- size="sm"
844
- onClick={() => void submitTimesheet(timesheet.id)}
845
- >
846
- <Send className="size-4" />
847
- {commonT('actions.submit')}
848
- </Button>
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.week')}</TableHead>
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
- <Button
951
- size="icon"
952
- onClick={() => void submitTimesheet(timesheet.id)}
953
- >
954
- <Send className="size-4" />
955
- </Button>
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={commonT('actions.save')}
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
- {formatDateRange(
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
  ) : (