@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.
Files changed (137) 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-projects.controller.d.ts +31 -0
  10. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-projects.controller.js +23 -0
  12. package/dist/controllers/operations-projects.controller.js.map +1 -1
  13. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  14. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  15. package/dist/controllers/operations-reports.controller.js +53 -0
  16. package/dist/controllers/operations-reports.controller.js.map +1 -0
  17. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  18. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-tasks.controller.js +17 -5
  20. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  21. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  22. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  23. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  24. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  25. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  26. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  27. package/dist/dto/create-collaborator.dto.js +0 -6
  28. package/dist/dto/create-collaborator.dto.js.map +1 -1
  29. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  30. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  31. package/dist/dto/create-cost-type.dto.js +87 -0
  32. package/dist/dto/create-cost-type.dto.js.map +1 -0
  33. package/dist/dto/list-approvals.dto.d.ts +2 -0
  34. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  35. package/dist/dto/list-approvals.dto.js +10 -0
  36. package/dist/dto/list-approvals.dto.js.map +1 -1
  37. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  38. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  39. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  40. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  41. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  42. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  43. package/dist/dto/list-cost-types.dto.js +35 -0
  44. package/dist/dto/list-cost-types.dto.js.map +1 -0
  45. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-my-projects.dto.js +23 -0
  48. package/dist/dto/list-my-projects.dto.js.map +1 -0
  49. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  51. package/dist/dto/list-my-tasks.dto.js +33 -0
  52. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  53. package/dist/dto/list-projects.dto.d.ts +1 -0
  54. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  55. package/dist/dto/list-projects.dto.js +7 -0
  56. package/dist/dto/list-projects.dto.js.map +1 -1
  57. package/dist/dto/list-reports.dto.d.ts +16 -0
  58. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  59. package/dist/dto/list-reports.dto.js +75 -0
  60. package/dist/dto/list-reports.dto.js.map +1 -0
  61. package/dist/dto/list-tasks.dto.d.ts +2 -0
  62. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  63. package/dist/dto/list-tasks.dto.js +12 -0
  64. package/dist/dto/list-tasks.dto.js.map +1 -1
  65. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  66. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  67. package/dist/dto/list-timesheets.dto.js +10 -0
  68. package/dist/dto/list-timesheets.dto.js.map +1 -1
  69. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  70. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  71. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  72. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  73. package/dist/dto/update-task.dto.d.ts +1 -0
  74. package/dist/dto/update-task.dto.d.ts.map +1 -1
  75. package/dist/dto/update-task.dto.js +6 -0
  76. package/dist/dto/update-task.dto.js.map +1 -1
  77. package/dist/operations.module.d.ts.map +1 -1
  78. package/dist/operations.module.js +4 -0
  79. package/dist/operations.module.js.map +1 -1
  80. package/dist/operations.service.d.ts +457 -3
  81. package/dist/operations.service.d.ts.map +1 -1
  82. package/dist/operations.service.js +1445 -208
  83. package/dist/operations.service.js.map +1 -1
  84. package/dist/operations.service.spec.js +31 -7
  85. package/dist/operations.service.spec.js.map +1 -1
  86. package/hedhog/data/menu.yaml +112 -7
  87. package/hedhog/data/operations_cost_type.yaml +166 -0
  88. package/hedhog/data/route.yaml +185 -0
  89. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  90. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +94 -15
  91. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  92. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  93. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  94. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  95. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  96. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  97. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  98. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  99. package/hedhog/frontend/app/_lib/types.ts.ejs +229 -3
  100. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  101. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  102. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  103. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  104. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  105. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  106. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  107. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  108. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  109. package/hedhog/frontend/messages/en.json +234 -25
  110. package/hedhog/frontend/messages/pt.json +234 -25
  111. package/hedhog/table/operations_collaborator.yaml +0 -4
  112. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  113. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  114. package/hedhog/table/operations_cost_type.yaml +38 -0
  115. package/package.json +6 -6
  116. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  117. package/src/controllers/operations-collaborators.controller.ts +19 -8
  118. package/src/controllers/operations-projects.controller.ts +19 -8
  119. package/src/controllers/operations-reports.controller.ts +32 -0
  120. package/src/controllers/operations-tasks.controller.ts +32 -12
  121. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  122. package/src/dto/create-collaborator.dto.ts +9 -14
  123. package/src/dto/create-cost-type.dto.ts +62 -0
  124. package/src/dto/list-approvals.dto.ts +8 -0
  125. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  126. package/src/dto/list-cost-types.dto.ts +19 -0
  127. package/src/dto/list-my-projects.dto.ts +8 -0
  128. package/src/dto/list-my-tasks.dto.ts +17 -0
  129. package/src/dto/list-projects.dto.ts +7 -1
  130. package/src/dto/list-reports.dto.ts +51 -0
  131. package/src/dto/list-tasks.dto.ts +11 -1
  132. package/src/dto/list-timesheets.dto.ts +8 -0
  133. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  134. package/src/dto/update-task.dto.ts +6 -0
  135. package/src/operations.module.ts +7 -3
  136. package/src/operations.service.spec.ts +45 -7
  137. package/src/operations.service.ts +1992 -225
@@ -36,6 +36,7 @@ import { Textarea } from '@/components/ui/textarea';
36
36
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
37
37
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
38
38
  import {
39
+ CalendarDays,
39
40
  CalendarOff,
40
41
  Check,
41
42
  ClipboardCheck,
@@ -48,7 +49,11 @@ import {
48
49
  X,
49
50
  } from 'lucide-react';
50
51
  import { useTranslations } from 'next-intl';
51
- import { useState } from 'react';
52
+ import { useMemo, useState } from 'react';
53
+ import {
54
+ OperationsCalendarView,
55
+ type OperationsCalendarItem,
56
+ } from '../_components/operations-calendar-view';
52
57
  import { OperationsHeader } from '../_components/operations-header';
53
58
  import { StatusBadge } from '../_components/status-badge';
54
59
  import { fetchOperations } from '../_lib/api';
@@ -56,7 +61,6 @@ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
56
61
  import type { OperationsApproval, PaginatedResponse } from '../_lib/types';
57
62
  import {
58
63
  formatDate,
59
- formatDateRange,
60
64
  formatDateTime,
61
65
  formatEnumLabel,
62
66
  formatHours,
@@ -217,13 +221,25 @@ export default function OperationsApprovalsPage() {
217
221
  const [search, setSearch] = useState('');
218
222
  const [statusFilter, setStatusFilter] = useState('all');
219
223
  const [targetFilter, setTargetFilter] = useState('all');
224
+ const [dateFrom, setDateFrom] = useState('');
225
+ const [dateTo, setDateTo] = useState('');
226
+ const [calendarYear, setCalendarYear] = useState(() =>
227
+ new Date().getFullYear()
228
+ );
229
+ const [calendarMonth, setCalendarMonth] = useState(
230
+ () => new Date().getMonth() + 1
231
+ );
220
232
  const [page, setPage] = useState(1);
221
233
  const [pageSize, setPageSize] = useState(12);
222
- const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
223
- if (typeof window === 'undefined') return 'table';
224
- const saved = window.localStorage.getItem('operations-approvals-view-mode');
225
- return saved === 'cards' ? 'cards' : 'table';
226
- });
234
+ const [viewMode, setViewMode] = useState<'table' | 'cards' | 'calendar'>(
235
+ () => {
236
+ if (typeof window === 'undefined') return 'table';
237
+ const saved = window.localStorage.getItem(
238
+ 'operations-approvals-view-mode'
239
+ );
240
+ return saved === 'cards' || saved === 'calendar' ? saved : 'table';
241
+ }
242
+ );
227
243
  const [decisionNote, setDecisionNote] = useState('');
228
244
  const [pendingDecision, setPendingDecision] =
229
245
  useState<PendingDecision | null>(null);
@@ -297,19 +313,16 @@ export default function OperationsApprovalsPage() {
297
313
  const getTargetLabel = (approval: OperationsApproval) => {
298
314
  switch (approval.targetType) {
299
315
  case 'timesheet':
300
- return `${getTargetTypeLabel('timesheet')} ${formatDateRange(
301
- approval.timesheetWeekStartDate,
302
- approval.timesheetWeekEndDate
316
+ return `${getTargetTypeLabel('timesheet')} ${formatDate(
317
+ approval.timesheetWeekStartDate
303
318
  )}`;
304
319
  case 'time_off_request':
305
- return `${getTimeOffTypeLabel(approval.timeOffType)} ${formatDateRange(
306
- approval.timeOffStartDate,
307
- approval.timeOffEndDate
320
+ return `${getTimeOffTypeLabel(approval.timeOffType)} ${formatDate(
321
+ approval.timeOffStartDate
308
322
  )}`;
309
323
  case 'schedule_adjustment_request':
310
- return `${getScheduleScopeLabel(approval.scheduleRequestScope)} ${formatDateRange(
311
- approval.scheduleStartDate,
312
- approval.scheduleEndDate
324
+ return `${getScheduleScopeLabel(approval.scheduleRequestScope)} ${formatDate(
325
+ approval.scheduleStartDate
313
326
  )}`;
314
327
  default:
315
328
  return getTargetTypeLabel(approval.targetType);
@@ -325,6 +338,8 @@ export default function OperationsApprovalsPage() {
325
338
  search,
326
339
  statusFilter,
327
340
  targetFilter,
341
+ dateFrom,
342
+ dateTo,
328
343
  page,
329
344
  pageSize,
330
345
  ],
@@ -337,6 +352,43 @@ export default function OperationsApprovalsPage() {
337
352
  if (search.trim()) params.set('search', search.trim());
338
353
  if (statusFilter !== 'all') params.set('status', statusFilter);
339
354
  if (targetFilter !== 'all') params.set('targetType', targetFilter);
355
+ if (dateFrom) params.set('dateFrom', dateFrom);
356
+ if (dateTo) params.set('dateTo', dateTo);
357
+ return fetchOperations<PaginatedResponse<OperationsApproval>>(
358
+ request,
359
+ `/operations/approvals?${params.toString()}`
360
+ );
361
+ },
362
+ placeholderData: (previous) => previous,
363
+ });
364
+
365
+ const calendarDateFrom = `${String(calendarYear)}-${String(calendarMonth).padStart(2, '0')}-01`;
366
+ const calendarDateTo = (() => {
367
+ const lastDay = new Date(calendarYear, calendarMonth, 0).getDate();
368
+ return `${String(calendarYear)}-${String(calendarMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
369
+ })();
370
+
371
+ const { data: calendarApprovalsResponse } = useQuery<
372
+ PaginatedResponse<OperationsApproval>
373
+ >({
374
+ queryKey: [
375
+ 'operations-approvals-calendar',
376
+ currentLocaleCode,
377
+ search,
378
+ statusFilter,
379
+ calendarYear,
380
+ calendarMonth,
381
+ ],
382
+ enabled: access.isSupervisor && viewMode === 'calendar',
383
+ queryFn: () => {
384
+ const params = new URLSearchParams({
385
+ sortField: 'submittedAt',
386
+ sortOrder: 'desc',
387
+ dateFrom: calendarDateFrom,
388
+ dateTo: calendarDateTo,
389
+ });
390
+ if (search.trim()) params.set('search', search.trim());
391
+ if (statusFilter !== 'all') params.set('status', statusFilter);
340
392
  return fetchOperations<PaginatedResponse<OperationsApproval>>(
341
393
  request,
342
394
  `/operations/approvals?${params.toString()}`
@@ -346,10 +398,56 @@ export default function OperationsApprovalsPage() {
346
398
  });
347
399
 
348
400
  const approvals = approvalsResponse?.data ?? [];
401
+ const calendarApprovals = calendarApprovalsResponse?.data ?? [];
402
+
403
+ const buildCalendarItem = (approval: OperationsApproval) => ({
404
+ id: `approval-${approval.id}`,
405
+ date:
406
+ approval.targetType === 'timesheet'
407
+ ? String(approval.timesheetWeekStartDate ?? '').match(
408
+ /(\d{4}-\d{2}-\d{2})/
409
+ )?.[1] ||
410
+ approval.submittedAt?.slice(0, 10) ||
411
+ ''
412
+ : approval.targetType === 'time_off_request'
413
+ ? String(approval.timeOffStartDate ?? '').match(
414
+ /(\d{4}-\d{2}-\d{2})/
415
+ )?.[1] ||
416
+ approval.submittedAt?.slice(0, 10) ||
417
+ ''
418
+ : String(approval.scheduleStartDate ?? '').match(
419
+ /(\d{4}-\d{2}-\d{2})/
420
+ )?.[1] ||
421
+ approval.submittedAt?.slice(0, 10) ||
422
+ '',
423
+ title: approval.requesterName,
424
+ subtitle: getTargetLabel(approval),
425
+ kind: approval.targetType,
426
+ meta:
427
+ approval.status === 'pending'
428
+ ? formatDateTime(approval.submittedAt)
429
+ : formatDateTime(approval.decidedAt),
430
+ statusLabel: getStatusLabel(approval.status),
431
+ badgeClassName: getStatusBadgeClass(approval.status),
432
+ });
433
+
434
+ const calendarItems = useMemo<OperationsCalendarItem[]>(
435
+ () =>
436
+ (viewMode === 'calendar' ? calendarApprovals : approvals).map(
437
+ buildCalendarItem
438
+ ),
439
+ // eslint-disable-next-line react-hooks/exhaustive-deps
440
+ [approvals, calendarApprovals, viewMode]
441
+ );
442
+
443
+ const calendarApprovalById = useMemo(
444
+ () => new Map(calendarApprovals.map((a) => [`approval-${a.id}`, a])),
445
+ [calendarApprovals]
446
+ );
349
447
 
350
448
  const handleViewModeChange = (value: string) => {
351
- if (value !== 'table' && value !== 'cards') return;
352
- setViewMode(value as 'table' | 'cards');
449
+ if (value !== 'table' && value !== 'cards' && value !== 'calendar') return;
450
+ setViewMode(value as 'table' | 'cards' | 'calendar');
353
451
  if (typeof window !== 'undefined') {
354
452
  window.localStorage.setItem('operations-approvals-view-mode', value);
355
453
  }
@@ -513,6 +611,26 @@ export default function OperationsApprovalsPage() {
513
611
  },
514
612
  ],
515
613
  },
614
+ {
615
+ id: 'dateFrom',
616
+ type: 'date',
617
+ value: dateFrom,
618
+ onChange: (value) => {
619
+ setDateFrom(value);
620
+ setPage(1);
621
+ },
622
+ label: commonT('filters.dateFrom'),
623
+ },
624
+ {
625
+ id: 'dateTo',
626
+ type: 'date',
627
+ value: dateTo,
628
+ onChange: (value) => {
629
+ setDateTo(value);
630
+ setPage(1);
631
+ },
632
+ label: commonT('filters.dateTo'),
633
+ },
516
634
  ]}
517
635
  />
518
636
  </div>
@@ -536,12 +654,37 @@ export default function OperationsApprovalsPage() {
536
654
  <LayoutGrid className="h-4 w-4" />
537
655
  <span className="hidden sm:inline">{t('viewModeCards')}</span>
538
656
  </ToggleGroupItem>
657
+ <ToggleGroupItem value="calendar" className="gap-1.5 px-2.5">
658
+ <CalendarDays className="h-4 w-4" />
659
+ <span className="hidden sm:inline">{t('viewModeCalendar')}</span>
660
+ </ToggleGroupItem>
539
661
  </ToggleGroup>
540
662
  </div>
541
663
  </div>
542
664
 
543
- {approvals.length > 0 ? (
544
- viewMode === 'cards' ? (
665
+ {(viewMode === 'calendar' ? true : approvals.length > 0) ? (
666
+ viewMode === 'calendar' ? (
667
+ <OperationsCalendarView
668
+ locale={currentLocaleCode}
669
+ items={calendarItems.filter((item) => item.date)}
670
+ emptyLabel={t('emptyDescription')}
671
+ actionLabel={t('actions.viewDetails')}
672
+ onMonthChange={(year, month) => {
673
+ setCalendarYear(year);
674
+ setCalendarMonth(month);
675
+ }}
676
+ onDateSelect={(_date, items) => {
677
+ const approval = calendarApprovalById.get(String(items[0]?.id));
678
+ if (approval) void openDetails(approval);
679
+ }}
680
+ onOpenItem={(item) => {
681
+ const approval = calendarApprovalById.get(String(item.id));
682
+ if (approval) {
683
+ void openDetails(approval);
684
+ }
685
+ }}
686
+ />
687
+ ) : viewMode === 'cards' ? (
545
688
  <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
546
689
  {approvals.map((approval) => (
547
690
  <Card
@@ -792,19 +935,20 @@ export default function OperationsApprovalsPage() {
792
935
  </span>
793
936
  <span className="text-sm text-muted-foreground">
794
937
  {selectedApproval.targetType === 'timesheet'
795
- ? formatDateRange(
796
- selectedApproval.timesheetWeekStartDate,
797
- selectedApproval.timesheetWeekEndDate
798
- )
938
+ ? formatDate(selectedApproval.timesheetWeekStartDate)
799
939
  : selectedApproval.targetType === 'time_off_request'
800
- ? formatDateRange(
801
- selectedApproval.timeOffStartDate,
802
- selectedApproval.timeOffEndDate
803
- )
804
- : formatDateRange(
805
- selectedApproval.scheduleStartDate,
806
- selectedApproval.scheduleEndDate
807
- )}
940
+ ? [
941
+ formatDate(selectedApproval.timeOffStartDate),
942
+ formatDate(selectedApproval.timeOffEndDate),
943
+ ]
944
+ .filter(Boolean)
945
+ .join(' – ')
946
+ : [
947
+ formatDate(selectedApproval.scheduleStartDate),
948
+ formatDate(selectedApproval.scheduleEndDate),
949
+ ]
950
+ .filter(Boolean)
951
+ .join(' – ')}
808
952
  </span>
809
953
  </div>
810
954
  </div>
@@ -863,10 +1007,7 @@ export default function OperationsApprovalsPage() {
863
1007
  {t('details.period')}
864
1008
  </div>
865
1009
  <div className="mt-1 text-sm">
866
- {formatDateRange(
867
- selectedApproval.timesheetWeekStartDate,
868
- selectedApproval.timesheetWeekEndDate
869
- )}
1010
+ {formatDate(selectedApproval.timesheetWeekStartDate)}
870
1011
  </div>
871
1012
  </div>
872
1013
  <div>
@@ -949,15 +1090,17 @@ export default function OperationsApprovalsPage() {
949
1090
 
950
1091
  {selectedApproval.targetType === 'time_off_request' ? (
951
1092
  <div className="grid gap-4 sm:grid-cols-2">
952
- <div>
1093
+ <div className="sm:col-span-2">
953
1094
  <div className="text-xs font-medium uppercase text-muted-foreground">
954
1095
  {t('details.period')}
955
1096
  </div>
956
1097
  <div className="mt-1 text-sm">
957
- {formatDateRange(
958
- selectedApproval.timeOffStartDate,
959
- selectedApproval.timeOffEndDate
960
- )}
1098
+ {[
1099
+ formatDate(selectedApproval.timeOffStartDate),
1100
+ formatDate(selectedApproval.timeOffEndDate),
1101
+ ]
1102
+ .filter(Boolean)
1103
+ .join(' – ') || '—'}
961
1104
  </div>
962
1105
  </div>
963
1106
  <div>
@@ -983,15 +1126,17 @@ export default function OperationsApprovalsPage() {
983
1126
  {selectedApproval.targetType === 'schedule_adjustment_request' ? (
984
1127
  <>
985
1128
  <div className="grid gap-4 sm:grid-cols-2">
986
- <div>
1129
+ <div className="sm:col-span-2">
987
1130
  <div className="text-xs font-medium uppercase text-muted-foreground">
988
1131
  {t('details.period')}
989
1132
  </div>
990
1133
  <div className="mt-1 text-sm">
991
- {formatDateRange(
992
- selectedApproval.scheduleStartDate,
993
- selectedApproval.scheduleEndDate
994
- )}
1134
+ {[
1135
+ formatDate(selectedApproval.scheduleStartDate),
1136
+ formatDate(selectedApproval.scheduleEndDate),
1137
+ ]
1138
+ .filter(Boolean)
1139
+ .join(' – ') || '—'}
995
1140
  </div>
996
1141
  </div>
997
1142
  <div>
@@ -26,17 +26,26 @@ import {
26
26
  TableRow,
27
27
  } from '@/components/ui/table';
28
28
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
29
+ import {
30
+ Tooltip,
31
+ TooltipContent,
32
+ TooltipProvider,
33
+ TooltipTrigger,
34
+ } from '@/components/ui/tooltip';
29
35
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
30
36
  import {
31
37
  CalendarDays,
32
38
  FileText,
39
+ Info,
33
40
  LayoutGrid,
34
41
  List,
35
42
  Pencil,
36
43
  Power,
44
+ TrendingUp,
37
45
  UserCheck,
38
46
  UserRound,
39
47
  Users,
48
+ Wallet,
40
49
  } from 'lucide-react';
41
50
  import { useTranslations } from 'next-intl';
42
51
  import Link from 'next/link';
@@ -264,39 +273,140 @@ export default function OperationsCollaboratorsPage() {
264
273
  const collaborators = collaboratorsResponse?.data ?? [];
265
274
  const filteredRows = collaborators;
266
275
 
267
- const statsCards = useMemo(
268
- () => [
276
+ const statsCards = useMemo(() => {
277
+ const withTooltip = (description: string, tooltip: string) => (
278
+ <TooltipProvider>
279
+ <Tooltip>
280
+ <TooltipTrigger asChild>
281
+ <span className="inline-flex cursor-default items-center gap-1">
282
+ {description}
283
+ <Info className="h-3 w-3 shrink-0 text-muted-foreground/60" />
284
+ </span>
285
+ </TooltipTrigger>
286
+ <TooltipContent side="bottom" className="max-w-64 text-xs">
287
+ {tooltip}
288
+ </TooltipContent>
289
+ </Tooltip>
290
+ </TooltipProvider>
291
+ );
292
+
293
+ return [
269
294
  {
270
295
  key: 'total',
271
296
  title: t('cards.total'),
272
- description: t('cards.totalDescription'),
297
+ description: withTooltip(
298
+ t('cards.totalDescription'),
299
+ t('cards.totalTooltip')
300
+ ),
273
301
  value: collaboratorStats?.total ?? 0,
274
302
  icon: Users,
275
303
  },
276
304
  {
277
305
  key: 'active',
278
306
  title: t('cards.active'),
279
- description: t('cards.activeDescription'),
307
+ description: withTooltip(
308
+ t('cards.activeDescription'),
309
+ t('cards.activeTooltip')
310
+ ),
280
311
  value: collaboratorStats?.active ?? 0,
281
312
  icon: UserCheck,
282
313
  },
283
314
  {
284
315
  key: 'onLeave',
285
316
  title: t('cards.onLeave'),
286
- description: t('cards.onLeaveDescription'),
317
+ description: withTooltip(
318
+ t('cards.onLeaveDescription'),
319
+ t('cards.onLeaveTooltip')
320
+ ),
287
321
  value: collaboratorStats?.onLeave ?? 0,
288
322
  icon: CalendarDays,
289
323
  },
290
324
  {
291
325
  key: 'withContracts',
292
326
  title: t('cards.withContracts'),
293
- description: t('cards.withContractsDescription'),
327
+ description: withTooltip(
328
+ t('cards.withContractsDescription'),
329
+ t('cards.withContractsTooltip')
330
+ ),
294
331
  value: collaboratorStats?.withContracts ?? 0,
295
332
  icon: FileText,
296
333
  },
297
- ],
298
- [collaboratorStats, t]
299
- );
334
+ {
335
+ key: 'totalSalary',
336
+ title: t('cards.totalSalary'),
337
+ description: withTooltip(
338
+ t('cards.totalSalaryDescription'),
339
+ t('cards.totalSalaryTooltip')
340
+ ),
341
+ value: formatCurrency(
342
+ collaboratorStats?.totalSalary ?? 0,
343
+ getSettingValue,
344
+ currentLocaleCode
345
+ ),
346
+ icon: Wallet,
347
+ accentClassName: 'from-emerald-500/20 via-green-500/10 to-transparent',
348
+ iconContainerClassName:
349
+ 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
350
+ },
351
+ {
352
+ key: 'totalCosts',
353
+ title: t('cards.totalCosts'),
354
+ description: withTooltip(
355
+ t('cards.totalCostsDescription'),
356
+ t('cards.totalCostsTooltip')
357
+ ),
358
+ value: formatCurrency(
359
+ collaboratorStats?.totalCosts ?? 0,
360
+ getSettingValue,
361
+ currentLocaleCode
362
+ ),
363
+ icon: TrendingUp,
364
+ accentClassName: 'from-amber-500/20 via-yellow-500/10 to-transparent',
365
+ iconContainerClassName:
366
+ 'bg-amber-500/10 text-amber-700 dark:text-amber-400',
367
+ },
368
+ {
369
+ key: 'avgSalaryPlusCosts',
370
+ title: t('cards.avgSalaryPlusCosts'),
371
+ description: withTooltip(
372
+ t('cards.avgSalaryPlusCostsDescription'),
373
+ t('cards.avgSalaryPlusCostsTooltip')
374
+ ),
375
+ value: formatCurrency(
376
+ collaboratorStats?.avgSalaryPlusCosts ?? 0,
377
+ getSettingValue,
378
+ currentLocaleCode
379
+ ),
380
+ icon: Wallet,
381
+ accentClassName: 'from-violet-500/20 via-purple-500/10 to-transparent',
382
+ iconContainerClassName:
383
+ 'bg-violet-500/10 text-violet-700 dark:text-violet-400',
384
+ },
385
+ {
386
+ key: 'avgSalaryPlusCostsPerCollaborator',
387
+ title: t('cards.avgSalaryPlusCostsPerCollaborator'),
388
+ description: withTooltip(
389
+ t('cards.avgSalaryPlusCostsPerCollaboratorDescription'),
390
+ t('cards.avgSalaryPlusCostsPerCollaboratorTooltip')
391
+ ),
392
+ value: formatCurrency(
393
+ collaboratorStats?.avgSalaryPlusCostsPerCollaborator ?? 0,
394
+ getSettingValue,
395
+ currentLocaleCode
396
+ ),
397
+ icon: TrendingUp,
398
+ accentClassName: 'from-rose-500/20 via-pink-500/10 to-transparent',
399
+ iconContainerClassName:
400
+ 'bg-rose-500/10 text-rose-700 dark:text-rose-400',
401
+ },
402
+ ];
403
+ }, [
404
+ collaboratorStats,
405
+ t,
406
+ formatCurrency,
407
+ getSettingValue,
408
+ currentLocaleCode,
409
+ ]);
300
410
 
301
411
  const handleViewModeChange = (value: string) => {
302
412
  if (value !== 'table' && value !== 'cards') {
@@ -533,7 +643,7 @@ export default function OperationsCollaboratorsPage() {
533
643
  </div>
534
644
  ) : null}
535
645
 
536
- <div className="flex flex-wrap justify-end gap-2 border-t border-border/60 pt-3">
646
+ <div className="flex items-center justify-end gap-1.5 border-t border-border/60 pt-3">
537
647
  {access.isDirector ? (
538
648
  <Button
539
649
  variant="outline"
@@ -564,16 +674,15 @@ export default function OperationsCollaboratorsPage() {
564
674
  {access.isDirector ? (
565
675
  <Button
566
676
  variant="outline"
567
- size="sm"
568
- className="gap-1.5 px-2 sm:px-3"
677
+ size="icon"
569
678
  onClick={() => void toggleStatus(collaborator)}
679
+ title={
680
+ collaborator.status === 'inactive'
681
+ ? commonT('actions.activate')
682
+ : commonT('actions.deactivate')
683
+ }
570
684
  >
571
685
  <Power className="size-4" />
572
- <span className="hidden sm:inline">
573
- {collaborator.status === 'inactive'
574
- ? commonT('actions.activate')
575
- : commonT('actions.deactivate')}
576
- </span>
577
686
  </Button>
578
687
  ) : null}
579
688
  </div>
@@ -718,7 +827,7 @@ export default function OperationsCollaboratorsPage() {
718
827
  )}
719
828
  </TableCell>
720
829
  <TableCell>
721
- <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
830
+ <div className="flex items-center justify-end gap-1.5">
722
831
  {access.isDirector ? (
723
832
  <Button
724
833
  variant="outline"
@@ -749,16 +858,15 @@ export default function OperationsCollaboratorsPage() {
749
858
  {access.isDirector ? (
750
859
  <Button
751
860
  variant="outline"
752
- size="sm"
753
- className="gap-1.5 px-2 sm:px-3"
861
+ size="icon"
754
862
  onClick={() => void toggleStatus(collaborator)}
863
+ title={
864
+ collaborator.status === 'inactive'
865
+ ? commonT('actions.activate')
866
+ : commonT('actions.deactivate')
867
+ }
755
868
  >
756
869
  <Power className="size-4" />
757
- <span className="hidden sm:inline">
758
- {collaborator.status === 'inactive'
759
- ? commonT('actions.activate')
760
- : commonT('actions.deactivate')}
761
- </span>
762
870
  </Button>
763
871
  ) : null}
764
872
  </div>
@@ -0,0 +1,11 @@
1
+ import { MyProjectSummaryScreen } from '../../_components/my-project-summary-screen';
2
+
3
+ export default async function MyProjectSummaryPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ id: string }>;
7
+ }) {
8
+ const { id } = await params;
9
+
10
+ return <MyProjectSummaryScreen projectId={Number(id)} />;
11
+ }