@hed-hog/operations 0.0.2 → 0.0.285

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 (108) hide show
  1. package/README.md +122 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/operations-data.controller.d.ts +139 -0
  7. package/dist/operations-data.controller.d.ts.map +1 -0
  8. package/dist/operations-data.controller.js +113 -0
  9. package/dist/operations-data.controller.js.map +1 -0
  10. package/dist/operations-growth.controller.d.ts +48 -0
  11. package/dist/operations-growth.controller.d.ts.map +1 -0
  12. package/dist/operations-growth.controller.js +90 -0
  13. package/dist/operations-growth.controller.js.map +1 -0
  14. package/dist/operations.module.d.ts.map +1 -1
  15. package/dist/operations.module.js +10 -4
  16. package/dist/operations.module.js.map +1 -1
  17. package/dist/operations.service.d.ts +178 -0
  18. package/dist/operations.service.d.ts.map +1 -0
  19. package/dist/operations.service.js +134 -0
  20. package/dist/operations.service.js.map +1 -0
  21. package/hedhog/data/menu.yaml +251 -132
  22. package/hedhog/data/operations_career_level.yaml +102 -0
  23. package/hedhog/data/operations_career_track.yaml +8 -0
  24. package/hedhog/data/operations_certification.yaml +38 -0
  25. package/hedhog/data/operations_evaluation_cycle.yaml +18 -0
  26. package/hedhog/data/operations_performance_criterion.yaml +48 -0
  27. package/hedhog/data/role.yaml +14 -7
  28. package/hedhog/data/route.yaml +143 -80
  29. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +56 -56
  30. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +83 -83
  31. package/hedhog/frontend/app/_components/operations-header.tsx.ejs +29 -29
  32. package/hedhog/frontend/app/_components/section-card.tsx.ejs +32 -32
  33. package/hedhog/frontend/app/_components/status-badge.tsx.ejs +15 -15
  34. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +142 -142
  35. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +41 -41
  36. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +63 -0
  37. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +74 -74
  38. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +74 -74
  39. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +824 -0
  40. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +60 -60
  41. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +88 -88
  42. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +84 -84
  43. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +67 -67
  44. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +10 -10
  45. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +31 -0
  46. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +10 -10
  47. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +10 -10
  48. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +10 -10
  49. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +209 -0
  50. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +95 -95
  51. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +25 -25
  52. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +62 -0
  53. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +103 -103
  54. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +80 -80
  55. package/hedhog/frontend/app/allocations/page.tsx.ejs +154 -99
  56. package/hedhog/frontend/app/approvals/page.tsx.ejs +147 -147
  57. package/hedhog/frontend/app/career/page.tsx.ejs +143 -0
  58. package/hedhog/frontend/app/certifications/page.tsx.ejs +201 -0
  59. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +108 -108
  60. package/hedhog/frontend/app/contracts/page.tsx.ejs +180 -124
  61. package/hedhog/frontend/app/evaluations/page.tsx.ejs +277 -0
  62. package/hedhog/frontend/app/goals/page.tsx.ejs +170 -0
  63. package/hedhog/frontend/app/growth/page.tsx.ejs +288 -0
  64. package/hedhog/frontend/app/layout.tsx.ejs +9 -9
  65. package/hedhog/frontend/app/manager/page.tsx.ejs +175 -0
  66. package/hedhog/frontend/app/page.tsx.ejs +177 -177
  67. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +186 -186
  68. package/hedhog/frontend/app/projects/page.tsx.ejs +111 -111
  69. package/hedhog/frontend/app/rewards/page.tsx.ejs +195 -0
  70. package/hedhog/frontend/app/tasks/page.tsx.ejs +47 -47
  71. package/hedhog/frontend/app/timesheets/page.tsx.ejs +126 -126
  72. package/hedhog/frontend/messages/en.json +152 -142
  73. package/hedhog/frontend/messages/pt.json +152 -142
  74. package/hedhog/table/operations_allocation.yaml +52 -0
  75. package/hedhog/table/operations_calibration_item.yaml +61 -0
  76. package/hedhog/table/operations_calibration_session.yaml +25 -0
  77. package/hedhog/table/operations_career_level.yaml +75 -0
  78. package/hedhog/table/operations_career_track.yaml +21 -0
  79. package/hedhog/table/operations_certification.yaml +48 -0
  80. package/hedhog/table/operations_contract.yaml +57 -0
  81. package/hedhog/table/operations_employee.yaml +64 -0
  82. package/hedhog/table/operations_employee_certification.yaml +43 -0
  83. package/hedhog/table/operations_employee_connect.yaml +61 -0
  84. package/hedhog/table/operations_employee_evaluation.yaml +113 -0
  85. package/hedhog/table/operations_employee_evaluation_item.yaml +39 -0
  86. package/hedhog/table/operations_employee_profile.yaml +80 -0
  87. package/hedhog/table/operations_employee_skill_matrix.yaml +30 -0
  88. package/hedhog/table/operations_evaluation_cycle.yaml +31 -0
  89. package/hedhog/table/operations_goal.yaml +67 -0
  90. package/hedhog/table/operations_goal_progress.yaml +31 -0
  91. package/hedhog/table/operations_performance_criterion.yaml +29 -0
  92. package/hedhog/table/operations_project.yaml +66 -0
  93. package/hedhog/table/operations_promotion_readiness.yaml +49 -0
  94. package/hedhog/table/operations_promotion_recommendation.yaml +63 -0
  95. package/hedhog/table/operations_public_recognition.yaml +46 -0
  96. package/hedhog/table/operations_reward.yaml +100 -0
  97. package/hedhog/table/operations_score_event.yaml +81 -0
  98. package/hedhog/table/operations_task.yaml +60 -0
  99. package/hedhog/table/operations_timesheet.yaml +49 -0
  100. package/hedhog/table/operations_timesheet_entry.yaml +51 -0
  101. package/package.json +3 -3
  102. package/src/index.ts +2 -1
  103. package/src/language/en.json +8 -8
  104. package/src/language/pt.json +8 -8
  105. package/src/operations-data.controller.ts +54 -0
  106. package/src/operations-growth.controller.ts +44 -0
  107. package/src/operations.module.ts +21 -15
  108. package/src/operations.service.ts +137 -0
@@ -1,103 +1,103 @@
1
- import { allocationsMock } from '../mocks/allocations.mock';
2
- import { contractsMock } from '../mocks/contracts.mock';
3
- import { projectsMock } from '../mocks/projects.mock';
4
- import { tasksMock } from '../mocks/tasks.mock';
5
- import { timesheetsMock } from '../mocks/timesheets.mock';
6
- import { operationsUsersMock } from '../mocks/users.mock';
7
- import { getApprovalLabel } from './status';
8
-
9
- export function getDashboardMetrics() {
10
- const totalActiveProjects = projectsMock.filter(
11
- (project) => project.status === 'active'
12
- ).length;
13
- const hoursLoggedThisMonth = timesheetsMock.reduce(
14
- (total, entry) => total + entry.hours,
15
- 0
16
- );
17
- const hoursPendingApproval = timesheetsMock
18
- .filter((entry) => entry.status === 'pending')
19
- .reduce((total, entry) => total + entry.hours, 0);
20
- const revenue = contractsMock
21
- .filter((contract) => contract.status === 'active')
22
- .reduce((total, contract) => total + contract.hourlyRate * 160, 0);
23
- const utilization =
24
- allocationsMock.reduce(
25
- (total, allocation) => total + allocation.allocationPercent,
26
- 0
27
- ) / allocationsMock.length;
28
-
29
- return {
30
- totalActiveProjects,
31
- hoursLoggedThisMonth,
32
- hoursPendingApproval,
33
- revenue,
34
- utilization,
35
- };
36
- }
37
-
38
- export function getHoursByProject() {
39
- return projectsMock.map((project) => ({
40
- name: project.name,
41
- hours: timesheetsMock
42
- .filter((entry) => entry.projectId === project.id)
43
- .reduce((total, entry) => total + entry.hours, 0),
44
- }));
45
- }
46
-
47
- export function getHoursByUser() {
48
- return operationsUsersMock.map((user) => ({
49
- name: user.name.split(' ')[0],
50
- hours: timesheetsMock
51
- .filter((entry) => entry.userId === user.id)
52
- .reduce((total, entry) => total + entry.hours, 0),
53
- }));
54
- }
55
-
56
- export function getApprovalStatusData() {
57
- return ['approved', 'pending', 'rejected'].map((status) => ({
58
- name: getApprovalLabel(status as 'approved' | 'pending' | 'rejected'),
59
- value: timesheetsMock.filter((entry) => entry.status === status).length,
60
- }));
61
- }
62
-
63
- export function getWeeklyTrend() {
64
- return [
65
- { week: 'W1', hours: 32 },
66
- { week: 'W2', hours: 36 },
67
- { week: 'W3', hours: 29 },
68
- { week: 'W4', hours: 41 },
69
- ];
70
- }
71
-
72
- export function getRecentTimesheetEntries() {
73
- return [...timesheetsMock]
74
- .sort((a, b) => b.date.localeCompare(a.date))
75
- .slice(0, 6);
76
- }
77
-
78
- export function getDailyTotals() {
79
- return timesheetsMock.reduce<Record<string, number>>((acc, entry) => {
80
- acc[entry.date] = (acc[entry.date] || 0) + entry.hours;
81
- return acc;
82
- }, {});
83
- }
84
-
85
- export function getPendingApprovalsSummary() {
86
- return {
87
- pending: timesheetsMock.filter((entry) => entry.status === 'pending').length,
88
- approved: timesheetsMock.filter((entry) => entry.status === 'approved').length,
89
- rejected: timesheetsMock.filter((entry) => entry.status === 'rejected').length,
90
- };
91
- }
92
-
93
- export function getProjectTeam(projectId: string) {
94
- const project = projectsMock.find((item) => item.id === projectId);
95
-
96
- return operationsUsersMock.filter((user) =>
97
- project?.teamMemberIds.includes(user.id)
98
- );
99
- }
100
-
101
- export function getProjectTasks(projectId: string) {
102
- return tasksMock.filter((task) => task.projectId === projectId);
103
- }
1
+ import { allocationsMock } from '../mocks/allocations.mock';
2
+ import { contractsMock } from '../mocks/contracts.mock';
3
+ import { projectsMock } from '../mocks/projects.mock';
4
+ import { tasksMock } from '../mocks/tasks.mock';
5
+ import { timesheetsMock } from '../mocks/timesheets.mock';
6
+ import { operationsUsersMock } from '../mocks/users.mock';
7
+ import { getApprovalLabel } from './status';
8
+
9
+ export function getDashboardMetrics() {
10
+ const totalActiveProjects = projectsMock.filter(
11
+ (project) => project.status === 'active'
12
+ ).length;
13
+ const hoursLoggedThisMonth = timesheetsMock.reduce(
14
+ (total, entry) => total + entry.hours,
15
+ 0
16
+ );
17
+ const hoursPendingApproval = timesheetsMock
18
+ .filter((entry) => entry.status === 'pending')
19
+ .reduce((total, entry) => total + entry.hours, 0);
20
+ const revenue = contractsMock
21
+ .filter((contract) => contract.status === 'active')
22
+ .reduce((total, contract) => total + contract.hourlyRate * 160, 0);
23
+ const utilization =
24
+ allocationsMock.reduce(
25
+ (total, allocation) => total + allocation.allocationPercent,
26
+ 0
27
+ ) / allocationsMock.length;
28
+
29
+ return {
30
+ totalActiveProjects,
31
+ hoursLoggedThisMonth,
32
+ hoursPendingApproval,
33
+ revenue,
34
+ utilization,
35
+ };
36
+ }
37
+
38
+ export function getHoursByProject() {
39
+ return projectsMock.map((project) => ({
40
+ name: project.name,
41
+ hours: timesheetsMock
42
+ .filter((entry) => entry.projectId === project.id)
43
+ .reduce((total, entry) => total + entry.hours, 0),
44
+ }));
45
+ }
46
+
47
+ export function getHoursByUser() {
48
+ return operationsUsersMock.map((user) => ({
49
+ name: user.name.split(' ')[0],
50
+ hours: timesheetsMock
51
+ .filter((entry) => entry.userId === user.id)
52
+ .reduce((total, entry) => total + entry.hours, 0),
53
+ }));
54
+ }
55
+
56
+ export function getApprovalStatusData() {
57
+ return ['approved', 'pending', 'rejected'].map((status) => ({
58
+ name: getApprovalLabel(status as 'approved' | 'pending' | 'rejected'),
59
+ value: timesheetsMock.filter((entry) => entry.status === status).length,
60
+ }));
61
+ }
62
+
63
+ export function getWeeklyTrend() {
64
+ return [
65
+ { week: 'W1', hours: 32 },
66
+ { week: 'W2', hours: 36 },
67
+ { week: 'W3', hours: 29 },
68
+ { week: 'W4', hours: 41 },
69
+ ];
70
+ }
71
+
72
+ export function getRecentTimesheetEntries() {
73
+ return [...timesheetsMock]
74
+ .sort((a, b) => b.date.localeCompare(a.date))
75
+ .slice(0, 6);
76
+ }
77
+
78
+ export function getDailyTotals() {
79
+ return timesheetsMock.reduce<Record<string, number>>((acc, entry) => {
80
+ acc[entry.date] = (acc[entry.date] || 0) + entry.hours;
81
+ return acc;
82
+ }, {});
83
+ }
84
+
85
+ export function getPendingApprovalsSummary() {
86
+ return {
87
+ pending: timesheetsMock.filter((entry) => entry.status === 'pending').length,
88
+ approved: timesheetsMock.filter((entry) => entry.status === 'approved').length,
89
+ rejected: timesheetsMock.filter((entry) => entry.status === 'rejected').length,
90
+ };
91
+ }
92
+
93
+ export function getProjectTeam(projectId: string) {
94
+ const project = projectsMock.find((item) => item.id === projectId);
95
+
96
+ return operationsUsersMock.filter((user) =>
97
+ project?.teamMemberIds.includes(user.id)
98
+ );
99
+ }
100
+
101
+ export function getProjectTasks(projectId: string) {
102
+ return tasksMock.filter((task) => task.projectId === projectId);
103
+ }
@@ -1,80 +1,80 @@
1
- import type {
2
- ApprovalStatus,
3
- ContractStatus,
4
- ContractType,
5
- ProjectStatus,
6
- TaskStatus,
7
- } from '../types/operations';
8
-
9
- export function getApprovalBadgeClasses(status: ApprovalStatus) {
10
- return {
11
- approved: 'bg-emerald-100 text-emerald-700',
12
- pending: 'bg-amber-100 text-amber-700',
13
- rejected: 'bg-rose-100 text-rose-700',
14
- }[status];
15
- }
16
-
17
- export function getProjectBadgeClasses(status: ProjectStatus) {
18
- return {
19
- planning: 'bg-slate-100 text-slate-700',
20
- active: 'bg-blue-100 text-blue-700',
21
- 'at-risk': 'bg-orange-100 text-orange-700',
22
- paused: 'bg-zinc-100 text-zinc-700',
23
- completed: 'bg-emerald-100 text-emerald-700',
24
- }[status];
25
- }
26
-
27
- export function getTaskBadgeClasses(status: TaskStatus) {
28
- return {
29
- backlog: 'bg-slate-100 text-slate-700',
30
- todo: 'bg-cyan-100 text-cyan-700',
31
- 'in-progress': 'bg-blue-100 text-blue-700',
32
- review: 'bg-violet-100 text-violet-700',
33
- done: 'bg-emerald-100 text-emerald-700',
34
- }[status];
35
- }
36
-
37
- export function getContractBadgeClasses(status: ContractStatus) {
38
- return {
39
- active: 'bg-emerald-100 text-emerald-700',
40
- draft: 'bg-slate-100 text-slate-700',
41
- expired: 'bg-zinc-200 text-zinc-700',
42
- renewal: 'bg-orange-100 text-orange-700',
43
- }[status];
44
- }
45
-
46
- export function getContractTypeLabel(type: ContractType) {
47
- return {
48
- tm: 'T&M',
49
- monthly: 'Monthly',
50
- fixed: 'Fixed',
51
- }[type];
52
- }
53
-
54
- export function getApprovalLabel(status: ApprovalStatus) {
55
- return {
56
- approved: 'Approved',
57
- pending: 'Pending',
58
- rejected: 'Rejected',
59
- }[status];
60
- }
61
-
62
- export function getProjectStatusLabel(status: ProjectStatus) {
63
- return {
64
- planning: 'Planning',
65
- active: 'Active',
66
- 'at-risk': 'At Risk',
67
- paused: 'Paused',
68
- completed: 'Completed',
69
- }[status];
70
- }
71
-
72
- export function getTaskStatusLabel(status: TaskStatus) {
73
- return {
74
- backlog: 'Backlog',
75
- todo: 'Todo',
76
- 'in-progress': 'In Progress',
77
- review: 'Review',
78
- done: 'Done',
79
- }[status];
80
- }
1
+ import type {
2
+ ApprovalStatus,
3
+ ContractStatus,
4
+ ContractType,
5
+ ProjectStatus,
6
+ TaskStatus,
7
+ } from '../types/operations';
8
+
9
+ export function getApprovalBadgeClasses(status: ApprovalStatus) {
10
+ return {
11
+ approved: 'bg-emerald-100 text-emerald-700',
12
+ pending: 'bg-amber-100 text-amber-700',
13
+ rejected: 'bg-rose-100 text-rose-700',
14
+ }[status];
15
+ }
16
+
17
+ export function getProjectBadgeClasses(status: ProjectStatus) {
18
+ return {
19
+ planning: 'bg-slate-100 text-slate-700',
20
+ active: 'bg-blue-100 text-blue-700',
21
+ 'at-risk': 'bg-orange-100 text-orange-700',
22
+ paused: 'bg-zinc-100 text-zinc-700',
23
+ completed: 'bg-emerald-100 text-emerald-700',
24
+ }[status];
25
+ }
26
+
27
+ export function getTaskBadgeClasses(status: TaskStatus) {
28
+ return {
29
+ backlog: 'bg-slate-100 text-slate-700',
30
+ todo: 'bg-cyan-100 text-cyan-700',
31
+ 'in-progress': 'bg-blue-100 text-blue-700',
32
+ review: 'bg-violet-100 text-violet-700',
33
+ done: 'bg-emerald-100 text-emerald-700',
34
+ }[status];
35
+ }
36
+
37
+ export function getContractBadgeClasses(status: ContractStatus) {
38
+ return {
39
+ active: 'bg-emerald-100 text-emerald-700',
40
+ draft: 'bg-slate-100 text-slate-700',
41
+ expired: 'bg-zinc-200 text-zinc-700',
42
+ renewal: 'bg-orange-100 text-orange-700',
43
+ }[status];
44
+ }
45
+
46
+ export function getContractTypeLabel(type: ContractType) {
47
+ return {
48
+ tm: 'T&M',
49
+ monthly: 'Monthly',
50
+ fixed: 'Fixed',
51
+ }[type];
52
+ }
53
+
54
+ export function getApprovalLabel(status: ApprovalStatus) {
55
+ return {
56
+ approved: 'Approved',
57
+ pending: 'Pending',
58
+ rejected: 'Rejected',
59
+ }[status];
60
+ }
61
+
62
+ export function getProjectStatusLabel(status: ProjectStatus) {
63
+ return {
64
+ planning: 'Planning',
65
+ active: 'Active',
66
+ 'at-risk': 'At Risk',
67
+ paused: 'Paused',
68
+ completed: 'Completed',
69
+ }[status];
70
+ }
71
+
72
+ export function getTaskStatusLabel(status: TaskStatus) {
73
+ return {
74
+ backlog: 'Backlog',
75
+ todo: 'Todo',
76
+ 'in-progress': 'In Progress',
77
+ review: 'Review',
78
+ done: 'Done',
79
+ }[status];
80
+ }
@@ -1,99 +1,154 @@
1
- 'use client';
2
-
3
- import { Page } from '@/components/entity-list';
4
- import { Input } from '@/components/ui/input';
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from '@/components/ui/table';
13
- import { useTranslations } from 'next-intl';
14
- import { useMemo, useState } from 'react';
15
- import { AllocationCalendar } from '../_components/allocation-calendar';
16
- import { OperationsHeader } from '../_components/operations-header';
17
- import { SectionCard } from '../_components/section-card';
18
- import { useOperationsData } from '../_lib/hooks/use-operations-data';
19
- import { formatDate } from '../_lib/utils/format';
20
-
21
- export default function AllocationsPage() {
22
- const t = useTranslations('operations.AllocationsPage');
23
- const { allocations, users, projects } = useOperationsData();
24
- const [search, setSearch] = useState('');
25
-
26
- const filteredAllocations = useMemo(
27
- () =>
28
- allocations.filter((allocation) => {
29
- const user = users.find((item) => item.id === allocation.userId);
30
- const project = projects.find((item) => item.id === allocation.projectId);
31
- return `${user?.name} ${project?.name}`
32
- .toLowerCase()
33
- .includes(search.toLowerCase());
34
- }),
35
- [allocations, users, projects, search]
36
- );
37
-
38
- return (
39
- <Page>
40
- <OperationsHeader
41
- title={t('title')}
42
- description={t('description')}
43
- current={t('breadcrumb')}
44
- />
45
-
46
- <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
47
- <div className="mb-4">
48
- <Input
49
- value={search}
50
- onChange={(event) => setSearch(event.target.value)}
51
- placeholder={t('searchPlaceholder')}
52
- />
53
- </div>
54
- <Table>
55
- <TableHeader>
56
- <TableRow>
57
- <TableHead>{t('columns.user')}</TableHead>
58
- <TableHead>{t('columns.role')}</TableHead>
59
- <TableHead>{t('columns.project')}</TableHead>
60
- <TableHead>{t('columns.weeklyHours')}</TableHead>
61
- <TableHead>{t('columns.allocation')}</TableHead>
62
- <TableHead>{t('columns.startDate')}</TableHead>
63
- <TableHead>{t('columns.endDate')}</TableHead>
64
- </TableRow>
65
- </TableHeader>
66
- <TableBody>
67
- {filteredAllocations.map((allocation) => {
68
- const user = users.find((item) => item.id === allocation.userId);
69
- const project = projects.find((item) => item.id === allocation.projectId);
70
-
71
- return (
72
- <TableRow key={allocation.id}>
73
- <TableCell>{user?.name}</TableCell>
74
- <TableCell>{allocation.role}</TableCell>
75
- <TableCell>{project?.name}</TableCell>
76
- <TableCell>{allocation.weeklyHours}h</TableCell>
77
- <TableCell>{allocation.allocationPercent}%</TableCell>
78
- <TableCell>{formatDate(allocation.startDate)}</TableCell>
79
- <TableCell>{formatDate(allocation.endDate)}</TableCell>
80
- </TableRow>
81
- );
82
- })}
83
- </TableBody>
84
- </Table>
85
- </SectionCard>
86
-
87
- <SectionCard
88
- title={t('calendarTitle')}
89
- description={t('calendarDescription')}
90
- >
91
- <AllocationCalendar
92
- allocations={filteredAllocations}
93
- users={users}
94
- projects={projects}
95
- />
96
- </SectionCard>
97
- </Page>
98
- );
99
- }
1
+ 'use client';
2
+
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import {
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ } from '@/components/ui/table';
17
+ import { useTranslations } from 'next-intl';
18
+ import { useMemo, useState } from 'react';
19
+ import { Layers } from 'lucide-react';
20
+ import { AllocationCalendar } from '../_components/allocation-calendar';
21
+ import { OperationsHeader } from '../_components/operations-header';
22
+ import { SectionCard } from '../_components/section-card';
23
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
24
+ import { formatDate } from '../_lib/utils/format';
25
+
26
+ const PAGE_SIZE_OPTIONS = [10, 20, 30, 50];
27
+
28
+ export default function AllocationsPage() {
29
+ const t = useTranslations('operations.AllocationsPage');
30
+ const { allocations, users, projects } = useOperationsData();
31
+ const [searchInput, setSearchInput] = useState('');
32
+ const [search, setSearch] = useState('');
33
+ const [currentPage, setCurrentPage] = useState(1);
34
+ const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[0]);
35
+
36
+ const filteredAllocations = useMemo(
37
+ () =>
38
+ allocations.filter((allocation) => {
39
+ const user = users.find((item) => item.id === allocation.userId);
40
+ const project = projects.find((item) => item.id === allocation.projectId);
41
+ return `${user?.name} ${project?.name}`
42
+ .toLowerCase()
43
+ .includes(search.toLowerCase());
44
+ }),
45
+ [allocations, users, projects, search]
46
+ );
47
+
48
+ const totalPages = Math.max(
49
+ 1,
50
+ Math.ceil(filteredAllocations.length / pageSize)
51
+ );
52
+ const safePage = Math.min(Math.max(currentPage, 1), totalPages);
53
+ const paginatedAllocations = useMemo(() => {
54
+ const start = (safePage - 1) * pageSize;
55
+ return filteredAllocations.slice(start, start + pageSize);
56
+ }, [filteredAllocations, safePage, pageSize]);
57
+
58
+ return (
59
+ <Page>
60
+ <OperationsHeader
61
+ title={t('title')}
62
+ description={t('description')}
63
+ current={t('breadcrumb')}
64
+ />
65
+
66
+ <SearchBar
67
+ className="mb-6"
68
+ searchQuery={searchInput}
69
+ onSearchChange={setSearchInput}
70
+ placeholder={t('searchPlaceholder')}
71
+ onSearch={() => {
72
+ setSearch(searchInput);
73
+ setCurrentPage(1);
74
+ }}
75
+ />
76
+
77
+ <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
78
+ {filteredAllocations.length === 0 ? (
79
+ <EmptyState
80
+ icon={<Layers className="h-12 w-12" />}
81
+ title={t('emptyState.title')}
82
+ description={t('emptyState.description')}
83
+ actionLabel={t('emptyState.action')}
84
+ onAction={() => {
85
+ setSearch('');
86
+ setSearchInput('');
87
+ setCurrentPage(1);
88
+ }}
89
+ />
90
+ ) : (
91
+ <div className="space-y-4">
92
+ <Table>
93
+ <TableHeader>
94
+ <TableRow>
95
+ <TableHead>{t('columns.user')}</TableHead>
96
+ <TableHead>{t('columns.role')}</TableHead>
97
+ <TableHead>{t('columns.project')}</TableHead>
98
+ <TableHead>{t('columns.weeklyHours')}</TableHead>
99
+ <TableHead>{t('columns.allocation')}</TableHead>
100
+ <TableHead>{t('columns.startDate')}</TableHead>
101
+ <TableHead>{t('columns.endDate')}</TableHead>
102
+ </TableRow>
103
+ </TableHeader>
104
+ <TableBody>
105
+ {paginatedAllocations.map((allocation) => {
106
+ const user = users.find(
107
+ (item) => item.id === allocation.userId
108
+ );
109
+ const project = projects.find(
110
+ (item) => item.id === allocation.projectId
111
+ );
112
+
113
+ return (
114
+ <TableRow key={allocation.id}>
115
+ <TableCell>{user?.name}</TableCell>
116
+ <TableCell>{allocation.role}</TableCell>
117
+ <TableCell>{project?.name}</TableCell>
118
+ <TableCell>{allocation.weeklyHours}h</TableCell>
119
+ <TableCell>{allocation.allocationPercent}%</TableCell>
120
+ <TableCell>{formatDate(allocation.startDate)}</TableCell>
121
+ <TableCell>{formatDate(allocation.endDate)}</TableCell>
122
+ </TableRow>
123
+ );
124
+ })}
125
+ </TableBody>
126
+ </Table>
127
+ <PaginationFooter
128
+ currentPage={safePage}
129
+ pageSize={pageSize}
130
+ totalItems={filteredAllocations.length}
131
+ onPageChange={setCurrentPage}
132
+ onPageSizeChange={(nextSize) => {
133
+ setPageSize(nextSize);
134
+ setCurrentPage(1);
135
+ }}
136
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
137
+ />
138
+ </div>
139
+ )}
140
+ </SectionCard>
141
+
142
+ <SectionCard
143
+ title={t('calendarTitle')}
144
+ description={t('calendarDescription')}
145
+ >
146
+ <AllocationCalendar
147
+ allocations={filteredAllocations}
148
+ users={users}
149
+ projects={projects}
150
+ />
151
+ </SectionCard>
152
+ </Page>
153
+ );
154
+ }