@hed-hog/operations 0.0.3 → 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.
- package/README.md +122 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations-data.controller.d.ts +139 -0
- package/dist/operations-data.controller.d.ts.map +1 -0
- package/dist/operations-data.controller.js +113 -0
- package/dist/operations-data.controller.js.map +1 -0
- package/dist/operations-growth.controller.d.ts +48 -0
- package/dist/operations-growth.controller.d.ts.map +1 -0
- package/dist/operations-growth.controller.js +90 -0
- package/dist/operations-growth.controller.js.map +1 -0
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +10 -4
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.service.d.ts +178 -0
- package/dist/operations.service.d.ts.map +1 -0
- package/dist/operations.service.js +134 -0
- package/dist/operations.service.js.map +1 -0
- package/hedhog/data/menu.yaml +251 -132
- package/hedhog/data/operations_career_level.yaml +102 -0
- package/hedhog/data/operations_career_track.yaml +8 -0
- package/hedhog/data/operations_certification.yaml +38 -0
- package/hedhog/data/operations_evaluation_cycle.yaml +18 -0
- package/hedhog/data/operations_performance_criterion.yaml +48 -0
- package/hedhog/data/role.yaml +14 -7
- package/hedhog/data/route.yaml +143 -80
- package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +56 -56
- package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +83 -83
- package/hedhog/frontend/app/_components/operations-header.tsx.ejs +29 -29
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +32 -32
- package/hedhog/frontend/app/_components/status-badge.tsx.ejs +15 -15
- package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +142 -142
- package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +41 -41
- package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +63 -0
- package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +74 -74
- package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +74 -74
- package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +824 -0
- package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +60 -60
- package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +88 -88
- package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +84 -84
- package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +67 -67
- package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +10 -10
- package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +31 -0
- package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +10 -10
- package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +10 -10
- package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +10 -10
- package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +209 -0
- package/hedhog/frontend/app/_lib/types/operations.ts.ejs +95 -95
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +25 -25
- package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +62 -0
- package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +103 -103
- package/hedhog/frontend/app/_lib/utils/status.ts.ejs +80 -80
- package/hedhog/frontend/app/allocations/page.tsx.ejs +154 -99
- package/hedhog/frontend/app/approvals/page.tsx.ejs +147 -147
- package/hedhog/frontend/app/career/page.tsx.ejs +143 -0
- package/hedhog/frontend/app/certifications/page.tsx.ejs +201 -0
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +108 -108
- package/hedhog/frontend/app/contracts/page.tsx.ejs +180 -124
- package/hedhog/frontend/app/evaluations/page.tsx.ejs +277 -0
- package/hedhog/frontend/app/goals/page.tsx.ejs +170 -0
- package/hedhog/frontend/app/growth/page.tsx.ejs +288 -0
- package/hedhog/frontend/app/layout.tsx.ejs +9 -9
- package/hedhog/frontend/app/manager/page.tsx.ejs +175 -0
- package/hedhog/frontend/app/page.tsx.ejs +177 -177
- package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +186 -186
- package/hedhog/frontend/app/projects/page.tsx.ejs +111 -111
- package/hedhog/frontend/app/rewards/page.tsx.ejs +195 -0
- package/hedhog/frontend/app/tasks/page.tsx.ejs +47 -47
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +126 -126
- package/hedhog/frontend/messages/en.json +152 -142
- package/hedhog/frontend/messages/pt.json +152 -142
- package/hedhog/table/operations_allocation.yaml +52 -0
- package/hedhog/table/operations_calibration_item.yaml +61 -0
- package/hedhog/table/operations_calibration_session.yaml +25 -0
- package/hedhog/table/operations_career_level.yaml +75 -0
- package/hedhog/table/operations_career_track.yaml +21 -0
- package/hedhog/table/operations_certification.yaml +48 -0
- package/hedhog/table/operations_contract.yaml +57 -0
- package/hedhog/table/operations_employee.yaml +64 -0
- package/hedhog/table/operations_employee_certification.yaml +43 -0
- package/hedhog/table/operations_employee_connect.yaml +61 -0
- package/hedhog/table/operations_employee_evaluation.yaml +113 -0
- package/hedhog/table/operations_employee_evaluation_item.yaml +39 -0
- package/hedhog/table/operations_employee_profile.yaml +80 -0
- package/hedhog/table/operations_employee_skill_matrix.yaml +30 -0
- package/hedhog/table/operations_evaluation_cycle.yaml +31 -0
- package/hedhog/table/operations_goal.yaml +67 -0
- package/hedhog/table/operations_goal_progress.yaml +31 -0
- package/hedhog/table/operations_performance_criterion.yaml +29 -0
- package/hedhog/table/operations_project.yaml +66 -0
- package/hedhog/table/operations_promotion_readiness.yaml +49 -0
- package/hedhog/table/operations_promotion_recommendation.yaml +63 -0
- package/hedhog/table/operations_public_recognition.yaml +46 -0
- package/hedhog/table/operations_reward.yaml +100 -0
- package/hedhog/table/operations_score_event.yaml +81 -0
- package/hedhog/table/operations_task.yaml +60 -0
- package/hedhog/table/operations_timesheet.yaml +49 -0
- package/hedhog/table/operations_timesheet_entry.yaml +51 -0
- package/package.json +4 -4
- package/src/index.ts +2 -1
- package/src/language/en.json +8 -8
- package/src/language/pt.json +8 -8
- package/src/operations-data.controller.ts +54 -0
- package/src/operations-growth.controller.ts +44 -0
- package/src/operations.module.ts +21 -15
- 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 {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|