@hed-hog/operations 0.0.294 → 0.0.296

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 (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +7 -6
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. package/src/operations-growth.controller.ts +0 -44
@@ -1,936 +1,11 @@
1
- 'use client';
2
-
3
- import {
4
- Page,
5
- StatsCards,
6
- type StatCardConfig,
7
- } from '@/components/entity-list';
8
- import { Progress } from '@/components/ui/progress';
9
- import {
10
- Table,
11
- TableBody,
12
- TableCell,
13
- TableHead,
14
- TableHeader,
15
- TableRow,
16
- } from '@/components/ui/table';
17
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
18
- import { cn } from '@/lib/utils';
19
- import {
20
- ArrowUpRight,
21
- CalendarClock,
22
- CheckCircle2,
23
- DollarSign,
24
- Gauge,
25
- ShieldAlert,
26
- Target,
27
- TrendingDown,
28
- Users,
29
- } from 'lucide-react';
30
- import { useTranslations } from 'next-intl';
31
- import { useParams } from 'next/navigation';
32
- import { useMemo } from 'react';
33
- import {
34
- Bar,
35
- BarChart,
36
- CartesianGrid,
37
- Cell,
38
- Legend,
39
- Line,
40
- LineChart,
41
- Pie,
42
- PieChart,
43
- ResponsiveContainer,
44
- Tooltip,
45
- XAxis,
46
- YAxis,
47
- } from 'recharts';
48
- import { KanbanBoard } from '../../_components/kanban-board';
49
- import { OperationsHeader } from '../../_components/operations-header';
50
- import { SectionCard } from '../../_components/section-card';
51
- import { StatusBadge } from '../../_components/status-badge';
52
- import { useOperationsData } from '../../_lib/hooks/use-operations-data';
53
- import {
54
- formatCurrency,
55
- formatDate,
56
- formatHours,
57
- } from '../../_lib/utils/format';
58
- import { getProjectTeam } from '../../_lib/utils/metrics';
59
- import {
60
- getApprovalBadgeClasses,
61
- getApprovalLabel,
62
- getProjectBadgeClasses,
63
- } from '../../_lib/utils/status';
64
-
65
- export default function ProjectDetailsPage() {
66
- const t = useTranslations('operations.ProjectDetailsPage');
67
- const params = useParams<{ id: string }>();
68
- const { projects, contracts, users, tasks, timesheets } = useOperationsData();
69
- const project = projects.find((item) => item.id === params.id);
70
-
71
- if (!project) {
72
- return <Page>{t('notFound')}</Page>;
73
- }
74
-
75
- const contract = contracts.find((item) => item.id === project.contractId);
76
- const team = getProjectTeam(project.id);
77
- const projectTasks = tasks.filter((task) => task.projectId === project.id);
78
- const projectTimesheets = timesheets.filter(
79
- (entry) => entry.projectId === project.id
80
- );
81
-
82
- const taskStatusData = useMemo(
83
- () => [
84
- {
85
- status: t('taskStatus.backlog'),
86
- total: projectTasks.filter((task) => task.status === 'backlog').length,
87
- },
88
- {
89
- status: t('taskStatus.todo'),
90
- total: projectTasks.filter((task) => task.status === 'todo').length,
91
- },
92
- {
93
- status: t('taskStatus.inProgress'),
94
- total: projectTasks.filter((task) => task.status === 'in-progress').length,
95
- },
96
- {
97
- status: t('taskStatus.review'),
98
- total: projectTasks.filter((task) => task.status === 'review').length,
99
- },
100
- {
101
- status: t('taskStatus.done'),
102
- total: projectTasks.filter((task) => task.status === 'done').length,
103
- },
104
- ],
105
- [projectTasks, t]
106
- );
107
-
108
- const approvalsData = useMemo(
109
- () => [
110
- {
111
- name: t('approvals.approved'),
112
- value: projectTimesheets.filter((entry) => entry.status === 'approved')
113
- .length,
114
- },
115
- {
116
- name: t('approvals.pending'),
117
- value: projectTimesheets.filter((entry) => entry.status === 'pending')
118
- .length,
119
- },
120
- {
121
- name: t('approvals.rejected'),
122
- value: projectTimesheets.filter((entry) => entry.status === 'rejected')
123
- .length,
124
- },
125
- ],
126
- [projectTimesheets, t]
127
- );
128
-
129
- const chartColors = ['#0ea5e9', '#f59e0b', '#ef4444', '#8b5cf6', '#10b981'];
130
-
131
- const totalEstimatedHours = projectTasks.reduce(
132
- (total, task) => total + task.estimatedHours,
133
- 0
134
- );
135
- const approvedHours = projectTimesheets
136
- .filter((entry) => entry.status === 'approved')
137
- .reduce((total, entry) => total + entry.hours, 0);
138
- const pendingHours = projectTimesheets
139
- .filter((entry) => entry.status === 'pending')
140
- .reduce((total, entry) => total + entry.hours, 0);
141
- const rejectedHours = projectTimesheets
142
- .filter((entry) => entry.status === 'rejected')
143
- .reduce((total, entry) => total + entry.hours, 0);
144
-
145
- const actualCost = projectTimesheets.reduce((total, entry) => {
146
- const user = users.find((item) => item.id === entry.userId);
147
- return total + entry.hours * (user?.hourlyRate ?? 0);
148
- }, 0);
149
-
150
- const budgetConsumption = project.budget
151
- ? Math.min((actualCost / project.budget) * 100, 100)
152
- : 0;
153
- const remainingBudget = Math.max(project.budget - actualCost, 0);
154
-
155
- const today = new Date();
156
- const startDate = new Date(project.startDate);
157
- const endDate = new Date(project.endDate);
158
- const elapsedDays = Math.max(
159
- 1,
160
- Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
161
- );
162
- const totalDays = Math.max(
163
- 1,
164
- Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
165
- );
166
- const elapsedPercent = Math.min((elapsedDays / totalDays) * 100, 100);
167
- const scheduleVariance = project.progress - elapsedPercent;
168
- const doneTasks = projectTasks.filter((task) => task.status === 'done').length;
169
- const throughput = projectTasks.length
170
- ? (doneTasks / projectTasks.length) * 100
171
- : 0;
172
-
173
- const openRisks = (project.risks ?? []).filter(
174
- (risk) => risk.status === 'open'
175
- ).length;
176
- const monthlyForecast = project.monthlyForecast ?? [];
177
- const health = project.health ?? {
178
- overall: 'stable',
179
- scope: 'stable',
180
- schedule: 'stable',
181
- budget: 'stable',
182
- quality: 'stable',
183
- };
184
-
185
- const healthClasses: Record<string, string> = {
186
- excellent: 'bg-emerald-100 text-emerald-700',
187
- stable: 'bg-sky-100 text-sky-700',
188
- attention: 'bg-amber-100 text-amber-700',
189
- critical: 'bg-rose-100 text-rose-700',
190
- };
191
-
192
- const riskSeverityClasses: Record<string, string> = {
193
- low: 'bg-sky-100 text-sky-700',
194
- medium: 'bg-amber-100 text-amber-700',
195
- high: 'bg-rose-100 text-rose-700',
196
- };
197
-
198
- const dependencyStatusClasses: Record<string, string> = {
199
- 'on-track': 'bg-emerald-100 text-emerald-700',
200
- watch: 'bg-amber-100 text-amber-700',
201
- blocked: 'bg-rose-100 text-rose-700',
202
- };
203
-
204
- const milestoneStatusClasses: Record<string, string> = {
205
- done: 'bg-emerald-100 text-emerald-700',
206
- next: 'bg-sky-100 text-sky-700',
207
- delayed: 'bg-rose-100 text-rose-700',
208
- };
209
-
210
- const stats: StatCardConfig[] = [
211
- {
212
- title: t('stats.progress'),
213
- value: `${project.progress}%`,
214
- icon: <Gauge className="size-5" />,
215
- iconBgColor: 'bg-sky-100',
216
- iconColor: 'text-sky-700',
217
- },
218
- {
219
- title: t('stats.actualCost'),
220
- value: formatCurrency(actualCost),
221
- icon: <DollarSign className="size-5" />,
222
- iconBgColor: 'bg-emerald-100',
223
- iconColor: 'text-emerald-700',
224
- },
225
- {
226
- title: t('stats.remainingBudget'),
227
- value: formatCurrency(remainingBudget),
228
- icon: <TrendingDown className="size-5" />,
229
- iconBgColor: 'bg-amber-100',
230
- iconColor: 'text-amber-700',
231
- },
232
- {
233
- title: t('stats.throughput'),
234
- value: `${Math.round(throughput)}%`,
235
- icon: <Target className="size-5" />,
236
- iconBgColor: 'bg-violet-100',
237
- iconColor: 'text-violet-700',
238
- },
239
- ];
240
-
241
- const healthBars = [
242
- { key: 'scope', label: t('health.scope'), value: health.scope },
243
- { key: 'schedule', label: t('health.schedule'), value: health.schedule },
244
- { key: 'budget', label: t('health.budget'), value: health.budget },
245
- { key: 'quality', label: t('health.quality'), value: health.quality },
246
- ];
247
-
248
- const healthToProgress: Record<string, number> = {
249
- excellent: 95,
250
- stable: 78,
251
- attention: 52,
252
- critical: 28,
253
- };
254
-
255
- return (
256
- <Page>
257
- <OperationsHeader
258
- title={project.name}
259
- description={t('description', { client: project.client })}
260
- current={t('breadcrumb')}
261
- />
262
-
263
- <div className="relative overflow-hidden rounded-xl border bg-linear-to-r from-slate-900 via-slate-800 to-sky-900 p-5 text-slate-50">
264
- <div className="absolute -right-10 -top-10 size-40 rounded-full bg-sky-400/20 blur-2xl" />
265
- <div className="absolute -bottom-12 left-20 size-40 rounded-full bg-emerald-400/20 blur-2xl" />
266
-
267
- <div className="relative flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
268
- <div className="space-y-3">
269
- <div className="flex flex-wrap items-center gap-2">
270
- <StatusBadge
271
- label={t(`projectStatus.${project.status}`)}
272
- className={cn('border-transparent', getProjectBadgeClasses(project.status))}
273
- />
274
- <StatusBadge
275
- label={`${t('labels.health')}: ${t(`healthStatus.${health.overall}`)}`}
276
- className={healthClasses[health.overall]}
277
- />
278
- <StatusBadge
279
- label={`${t('labels.risks')}: ${openRisks}`}
280
- className={
281
- openRisks > 0
282
- ? 'bg-amber-100 text-amber-700'
283
- : 'bg-emerald-100 text-emerald-700'
284
- }
285
- />
286
- </div>
287
- <p className="max-w-2xl text-sm text-slate-200">{project.description}</p>
288
- <div className="flex flex-wrap gap-5 text-xs text-slate-200">
289
- <p className="inline-flex items-center gap-1.5">
290
- <CalendarClock className="size-4" />
291
- {t('labels.period')}: {formatDate(project.startDate)} -{' '}
292
- {formatDate(project.endDate)}
293
- </p>
294
- <p className="inline-flex items-center gap-1.5">
295
- <Users className="size-4" />
296
- {t('labels.teamSize', { count: team.length })}
297
- </p>
298
- <p className="inline-flex items-center gap-1.5">
299
- <Target className="size-4" />
300
- {t('labels.contract')}: {contract?.name ?? '-'}
301
- </p>
302
- </div>
303
- </div>
304
-
305
- <div className="grid w-full max-w-md grid-cols-2 gap-3 rounded-lg border border-white/10 bg-white/5 p-4">
306
- <div>
307
- <p className="text-xs uppercase text-slate-300">{t('hero.progress')}</p>
308
- <p className="text-xl font-semibold">{project.progress}%</p>
309
- </div>
310
- <div>
311
- <p className="text-xs uppercase text-slate-300">{t('hero.budget')}</p>
312
- <p className="text-xl font-semibold">{formatCurrency(project.budget)}</p>
313
- </div>
314
- <div>
315
- <p className="text-xs uppercase text-slate-300">{t('hero.hours')}</p>
316
- <p className="text-xl font-semibold">{formatHours(project.hoursLogged)}</p>
317
- </div>
318
- <div>
319
- <p className="text-xs uppercase text-slate-300">
320
- {t('hero.scheduleVariance')}
321
- </p>
322
- <p className="text-xl font-semibold">
323
- {scheduleVariance >= 0 ? '+' : ''}
324
- {Math.round(scheduleVariance)}%
325
- </p>
326
- </div>
327
- </div>
328
- </div>
329
- </div>
330
-
331
- <StatsCards stats={stats} />
332
-
333
- <Tabs defaultValue="overview" className="space-y-4">
334
- <TabsList className="h-auto w-full flex-wrap justify-start gap-2 bg-transparent p-0">
335
- <TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
336
- <TabsTrigger value="execution">{t('tabs.execution')}</TabsTrigger>
337
- <TabsTrigger value="team">{t('tabs.team')}</TabsTrigger>
338
- <TabsTrigger value="finance">{t('tabs.finance')}</TabsTrigger>
339
- <TabsTrigger value="timesheets">{t('tabs.timesheets')}</TabsTrigger>
340
- <TabsTrigger value="governance">{t('tabs.governance')}</TabsTrigger>
341
- </TabsList>
342
-
343
- <TabsContent value="overview">
344
- <div className="grid gap-4 xl:grid-cols-5">
345
- <SectionCard
346
- className="xl:col-span-3"
347
- title={t('sections.progressTrend')}
348
- description={t('sections.progressTrendDescription')}
349
- >
350
- <div className="h-72">
351
- <ResponsiveContainer width="100%" height="100%">
352
- <LineChart data={monthlyForecast}>
353
- <CartesianGrid strokeDasharray="3 3" />
354
- <XAxis dataKey="month" />
355
- <YAxis />
356
- <Tooltip />
357
- <Legend />
358
- <Line
359
- type="monotone"
360
- dataKey="plannedProgress"
361
- name={t('charts.plannedProgress')}
362
- stroke="#0ea5e9"
363
- strokeWidth={2}
364
- dot={{ r: 3 }}
365
- />
366
- <Line
367
- type="monotone"
368
- dataKey="actualProgress"
369
- name={t('charts.actualProgress')}
370
- stroke="#22c55e"
371
- strokeWidth={2}
372
- dot={{ r: 3 }}
373
- />
374
- </LineChart>
375
- </ResponsiveContainer>
376
- </div>
377
- </SectionCard>
378
-
379
- <SectionCard
380
- className="xl:col-span-2"
381
- title={t('sections.health')}
382
- description={t('sections.healthDescription')}
383
- >
384
- <div className="space-y-4">
385
- {healthBars.map((item) => (
386
- <div key={item.key} className="space-y-2">
387
- <div className="flex items-center justify-between text-sm">
388
- <span className="font-medium">{item.label}</span>
389
- <StatusBadge
390
- label={t(`healthStatus.${item.value}`)}
391
- className={healthClasses[item.value]}
392
- />
393
- </div>
394
- <Progress value={healthToProgress[item.value]} />
395
- </div>
396
- ))}
397
- </div>
398
- </SectionCard>
399
-
400
- <SectionCard
401
- className="xl:col-span-3"
402
- title={t('sections.milestones')}
403
- description={t('sections.milestonesDescription')}
404
- >
405
- <div className="space-y-3">
406
- {(project.milestones ?? []).map((milestone) => (
407
- <div key={milestone.id} className="rounded-lg border p-3">
408
- <div className="flex flex-wrap items-center justify-between gap-2">
409
- <p className="font-medium">{milestone.title}</p>
410
- <StatusBadge
411
- label={t(`milestoneStatus.${milestone.status}`)}
412
- className={milestoneStatusClasses[milestone.status]}
413
- />
414
- </div>
415
- <p className="mt-1 text-xs text-muted-foreground">
416
- {formatDate(milestone.date)}
417
- </p>
418
- <p className="mt-2 text-sm text-muted-foreground">
419
- {milestone.description}
420
- </p>
421
- </div>
422
- ))}
423
- </div>
424
- </SectionCard>
425
-
426
- <SectionCard
427
- className="xl:col-span-2"
428
- title={t('sections.risks')}
429
- description={t('sections.risksDescription')}
430
- >
431
- <div className="space-y-3">
432
- {(project.risks ?? []).map((risk) => {
433
- const owner = users.find((item) => item.id === risk.ownerUserId);
434
-
435
- return (
436
- <div key={risk.id} className="rounded-lg border p-3">
437
- <div className="mb-2 flex items-center gap-2">
438
- <ShieldAlert className="size-4 text-amber-600" />
439
- <p className="font-medium">{risk.title}</p>
440
- </div>
441
- <div className="mb-2 flex flex-wrap gap-2">
442
- <StatusBadge
443
- label={t(`riskSeverity.${risk.severity}`)}
444
- className={riskSeverityClasses[risk.severity]}
445
- />
446
- <StatusBadge
447
- label={t(`riskStatus.${risk.status}`)}
448
- className={
449
- risk.status === 'open'
450
- ? 'bg-rose-100 text-rose-700'
451
- : 'bg-emerald-100 text-emerald-700'
452
- }
453
- />
454
- </div>
455
- <p className="text-xs text-muted-foreground">
456
- {t('labels.owner')}: {owner?.name ?? '-'}
457
- </p>
458
- <p className="mt-2 text-xs text-muted-foreground">
459
- <span className="font-medium text-foreground">
460
- {t('labels.impact')}:
461
- </span>{' '}
462
- {risk.impact}
463
- </p>
464
- <p className="mt-1 text-xs text-muted-foreground">
465
- <span className="font-medium text-foreground">
466
- {t('labels.mitigation')}:
467
- </span>{' '}
468
- {risk.mitigation}
469
- </p>
470
- </div>
471
- );
472
- })}
473
- </div>
474
- </SectionCard>
475
- </div>
476
- </TabsContent>
477
-
478
- <TabsContent value="execution">
479
- <div className="grid gap-4 xl:grid-cols-5">
480
- <SectionCard
481
- className="xl:col-span-2"
482
- title={t('sections.taskDistribution')}
483
- description={t('sections.taskDistributionDescription')}
484
- >
485
- <div className="h-72">
486
- <ResponsiveContainer width="100%" height="100%">
487
- <BarChart data={taskStatusData}>
488
- <CartesianGrid strokeDasharray="3 3" />
489
- <XAxis dataKey="status" />
490
- <YAxis allowDecimals={false} />
491
- <Tooltip />
492
- <Bar dataKey="total" radius={[6, 6, 0, 0]}>
493
- {taskStatusData.map((entry, index) => (
494
- <Cell
495
- key={`${entry.status}-${index}`}
496
- fill={chartColors[index % chartColors.length]}
497
- />
498
- ))}
499
- </Bar>
500
- </BarChart>
501
- </ResponsiveContainer>
502
- </div>
503
- <div className="mt-3 grid grid-cols-2 gap-2 text-xs">
504
- <p className="rounded-md bg-muted/50 p-2">
505
- {t('labels.totalTasks')}:{' '}
506
- <span className="font-semibold">{projectTasks.length}</span>
507
- </p>
508
- <p className="rounded-md bg-muted/50 p-2">
509
- {t('labels.estimatedHours')}:{' '}
510
- <span className="font-semibold">
511
- {formatHours(totalEstimatedHours)}
512
- </span>
513
- </p>
514
- </div>
515
- </SectionCard>
516
-
517
- <SectionCard
518
- className="xl:col-span-3"
519
- title={t('sections.kanban')}
520
- description={t('sections.kanbanDescription')}
521
- >
522
- <KanbanBoard tasks={projectTasks} users={users} />
523
- </SectionCard>
524
-
525
- <SectionCard
526
- className="xl:col-span-5"
527
- title={t('sections.taskTable')}
528
- description={t('sections.taskTableDescription')}
529
- >
530
- <Table>
531
- <TableHeader>
532
- <TableRow>
533
- <TableHead>{t('table.task')}</TableHead>
534
- <TableHead>{t('table.assignee')}</TableHead>
535
- <TableHead>{t('table.dueDate')}</TableHead>
536
- <TableHead>{t('table.estimate')}</TableHead>
537
- <TableHead>{t('table.status')}</TableHead>
538
- </TableRow>
539
- </TableHeader>
540
- <TableBody>
541
- {projectTasks.map((task) => {
542
- const assignee = users.find(
543
- (item) => item.id === task.assignedUserId
544
- );
545
-
546
- return (
547
- <TableRow key={task.id}>
548
- <TableCell>
549
- <div>
550
- <p className="font-medium">{task.title}</p>
551
- <p className="text-xs text-muted-foreground">
552
- {task.labels.join(', ')}
553
- </p>
554
- </div>
555
- </TableCell>
556
- <TableCell>{assignee?.name ?? '-'}</TableCell>
557
- <TableCell>{formatDate(task.dueDate)}</TableCell>
558
- <TableCell>{formatHours(task.estimatedHours)}</TableCell>
559
- <TableCell>
560
- <StatusBadge
561
- label={task.status}
562
- className={
563
- task.status === 'done'
564
- ? 'bg-emerald-100 text-emerald-700'
565
- : task.status === 'in-progress'
566
- ? 'bg-blue-100 text-blue-700'
567
- : 'bg-slate-100 text-slate-700'
568
- }
569
- />
570
- </TableCell>
571
- </TableRow>
572
- );
573
- })}
574
- </TableBody>
575
- </Table>
576
- </SectionCard>
577
- </div>
578
- </TabsContent>
579
-
580
- <TabsContent value="team">
581
- <div className="grid gap-4 xl:grid-cols-5">
582
- <SectionCard
583
- className="xl:col-span-3"
584
- title={t('sections.teamAllocation')}
585
- description={t('sections.teamAllocationDescription')}
586
- >
587
- <Table>
588
- <TableHeader>
589
- <TableRow>
590
- <TableHead>{t('table.member')}</TableHead>
591
- <TableHead>{t('table.role')}</TableHead>
592
- <TableHead>{t('table.hourRate')}</TableHead>
593
- <TableHead>{t('table.utilization')}</TableHead>
594
- </TableRow>
595
- </TableHeader>
596
- <TableBody>
597
- {team.map((member) => (
598
- <TableRow key={member.id}>
599
- <TableCell>
600
- <div className="flex items-center gap-2">
601
- <div className="flex size-8 items-center justify-center rounded-full bg-muted text-xs font-semibold">
602
- {member.name
603
- .split(' ')
604
- .map((part) => part[0])
605
- .slice(0, 2)
606
- .join('')
607
- .toUpperCase()}
608
- </div>
609
- <div>
610
- <p className="font-medium">{member.name}</p>
611
- <p className="text-xs text-muted-foreground">
612
- {member.department}
613
- </p>
614
- </div>
615
- </div>
616
- </TableCell>
617
- <TableCell>{member.role}</TableCell>
618
- <TableCell>{formatCurrency(member.hourlyRate)}</TableCell>
619
- <TableCell>
620
- <div className="space-y-2">
621
- <div className="text-xs">{member.utilizationTarget}%</div>
622
- <Progress value={member.utilizationTarget} />
623
- </div>
624
- </TableCell>
625
- </TableRow>
626
- ))}
627
- </TableBody>
628
- </Table>
629
- </SectionCard>
630
-
631
- <SectionCard
632
- className="xl:col-span-2"
633
- title={t('sections.teamCost')}
634
- description={t('sections.teamCostDescription')}
635
- >
636
- <div className="h-72">
637
- <ResponsiveContainer width="100%" height="100%">
638
- <PieChart>
639
- <Pie
640
- data={team.map((member) => ({
641
- name: member.name.split(' ')[0],
642
- value: member.hourlyRate,
643
- }))}
644
- dataKey="value"
645
- nameKey="name"
646
- innerRadius={55}
647
- outerRadius={95}
648
- paddingAngle={2}
649
- >
650
- {team.map((member, index) => (
651
- <Cell
652
- key={`${member.id}-${index}`}
653
- fill={chartColors[index % chartColors.length]}
654
- />
655
- ))}
656
- </Pie>
657
- <Tooltip />
658
- <Legend />
659
- </PieChart>
660
- </ResponsiveContainer>
661
- </div>
662
- </SectionCard>
663
- </div>
664
- </TabsContent>
665
-
666
- <TabsContent value="finance">
667
- <div className="grid gap-4 xl:grid-cols-5">
668
- <SectionCard
669
- className="xl:col-span-2"
670
- title={t('sections.budgetStatus')}
671
- description={t('sections.budgetStatusDescription')}
672
- >
673
- <div className="space-y-4 text-sm">
674
- <div className="rounded-lg border p-3">
675
- <p className="text-muted-foreground">{t('finance.totalBudget')}</p>
676
- <p className="text-xl font-semibold">
677
- {formatCurrency(project.budget)}
678
- </p>
679
- </div>
680
- <div className="rounded-lg border p-3">
681
- <p className="text-muted-foreground">{t('finance.actualCost')}</p>
682
- <p className="text-xl font-semibold">{formatCurrency(actualCost)}</p>
683
- </div>
684
- <div className="rounded-lg border p-3">
685
- <p className="text-muted-foreground">
686
- {t('finance.remainingBudget')}
687
- </p>
688
- <p className="text-xl font-semibold">
689
- {formatCurrency(remainingBudget)}
690
- </p>
691
- </div>
692
- <div className="space-y-2">
693
- <div className="flex items-center justify-between text-xs">
694
- <span>{t('finance.budgetConsumption')}</span>
695
- <span>{Math.round(budgetConsumption)}%</span>
696
- </div>
697
- <Progress value={budgetConsumption} />
698
- </div>
699
- </div>
700
- </SectionCard>
701
-
702
- <SectionCard
703
- className="xl:col-span-3"
704
- title={t('sections.costForecast')}
705
- description={t('sections.costForecastDescription')}
706
- >
707
- <div className="h-80">
708
- <ResponsiveContainer width="100%" height="100%">
709
- <BarChart data={monthlyForecast}>
710
- <CartesianGrid strokeDasharray="3 3" />
711
- <XAxis dataKey="month" />
712
- <YAxis />
713
- <Tooltip />
714
- <Legend />
715
- <Bar
716
- dataKey="plannedCost"
717
- name={t('charts.plannedCost')}
718
- fill="#0ea5e9"
719
- radius={[6, 6, 0, 0]}
720
- />
721
- <Bar
722
- dataKey="actualCost"
723
- name={t('charts.actualCost')}
724
- fill="#22c55e"
725
- radius={[6, 6, 0, 0]}
726
- />
727
- </BarChart>
728
- </ResponsiveContainer>
729
- </div>
730
- </SectionCard>
731
- </div>
732
- </TabsContent>
733
-
734
- <TabsContent value="timesheets">
735
- <div className="grid gap-4 xl:grid-cols-5">
736
- <SectionCard
737
- className="xl:col-span-2"
738
- title={t('sections.approvalsDistribution')}
739
- description={t('sections.approvalsDistributionDescription')}
740
- >
741
- <div className="h-72">
742
- <ResponsiveContainer width="100%" height="100%">
743
- <PieChart>
744
- <Pie
745
- data={approvalsData}
746
- dataKey="value"
747
- nameKey="name"
748
- innerRadius={55}
749
- outerRadius={95}
750
- >
751
- {approvalsData.map((entry, index) => (
752
- <Cell
753
- key={`${entry.name}-${index}`}
754
- fill={chartColors[index % chartColors.length]}
755
- />
756
- ))}
757
- </Pie>
758
- <Tooltip />
759
- <Legend />
760
- </PieChart>
761
- </ResponsiveContainer>
762
- </div>
763
- <div className="mt-2 grid grid-cols-3 gap-2 text-xs">
764
- <p className="rounded-md bg-emerald-50 p-2 text-emerald-700">
765
- {t('approvals.approved')}: {formatHours(approvedHours)}
766
- </p>
767
- <p className="rounded-md bg-amber-50 p-2 text-amber-700">
768
- {t('approvals.pending')}: {formatHours(pendingHours)}
769
- </p>
770
- <p className="rounded-md bg-rose-50 p-2 text-rose-700">
771
- {t('approvals.rejected')}: {formatHours(rejectedHours)}
772
- </p>
773
- </div>
774
- </SectionCard>
775
-
776
- <SectionCard
777
- className="xl:col-span-3"
778
- title={t('sections.timesheets')}
779
- description={t('sections.timesheetsDescription')}
780
- >
781
- <Table>
782
- <TableHeader>
783
- <TableRow>
784
- <TableHead>{t('table.date')}</TableHead>
785
- <TableHead>{t('table.user')}</TableHead>
786
- <TableHead>{t('table.task')}</TableHead>
787
- <TableHead>{t('table.hours')}</TableHead>
788
- <TableHead>{t('table.description')}</TableHead>
789
- <TableHead>{t('table.status')}</TableHead>
790
- </TableRow>
791
- </TableHeader>
792
- <TableBody>
793
- {projectTimesheets.map((entry) => {
794
- const user = users.find((item) => item.id === entry.userId);
795
- const task = projectTasks.find((item) => item.id === entry.taskId);
796
-
797
- return (
798
- <TableRow key={entry.id}>
799
- <TableCell>{formatDate(entry.date)}</TableCell>
800
- <TableCell>{user?.name ?? '-'}</TableCell>
801
- <TableCell>{task?.title ?? '-'}</TableCell>
802
- <TableCell>{formatHours(entry.hours)}</TableCell>
803
- <TableCell>{entry.description}</TableCell>
804
- <TableCell>
805
- <StatusBadge
806
- label={getApprovalLabel(entry.status)}
807
- className={getApprovalBadgeClasses(entry.status)}
808
- />
809
- </TableCell>
810
- </TableRow>
811
- );
812
- })}
813
- </TableBody>
814
- </Table>
815
- </SectionCard>
816
- </div>
817
- </TabsContent>
818
-
819
- <TabsContent value="governance">
820
- <div className="grid gap-4 xl:grid-cols-5">
821
- <SectionCard
822
- className="xl:col-span-2"
823
- title={t('sections.governance')}
824
- description={t('sections.governanceDescription')}
825
- >
826
- <div className="space-y-3 text-sm">
827
- <div className="rounded-lg border p-3">
828
- <p className="text-xs uppercase text-muted-foreground">
829
- {t('governance.client')}
830
- </p>
831
- <p className="font-medium">{project.client}</p>
832
- </div>
833
- <div className="rounded-lg border p-3">
834
- <p className="text-xs uppercase text-muted-foreground">
835
- {t('governance.contract')}
836
- </p>
837
- <p className="font-medium">{contract?.name ?? '-'}</p>
838
- </div>
839
- <div className="rounded-lg border p-3">
840
- <p className="text-xs uppercase text-muted-foreground">
841
- {t('governance.sla')}
842
- </p>
843
- <p className="font-medium text-muted-foreground">
844
- {contract?.sla ?? '-'}
845
- </p>
846
- </div>
847
- <div className="rounded-lg border p-3">
848
- <p className="text-xs uppercase text-muted-foreground">
849
- {t('governance.billingRules')}
850
- </p>
851
- <ul className="mt-2 space-y-1 text-muted-foreground">
852
- {(contract?.billingRules ?? []).map((rule) => (
853
- <li key={rule} className="flex items-start gap-2">
854
- <ArrowUpRight className="mt-0.5 size-3.5 text-sky-600" />
855
- <span>{rule}</span>
856
- </li>
857
- ))}
858
- </ul>
859
- </div>
860
- </div>
861
- </SectionCard>
862
-
863
- <SectionCard
864
- className="xl:col-span-3"
865
- title={t('sections.dependencies')}
866
- description={t('sections.dependenciesDescription')}
867
- >
868
- <div className="space-y-3">
869
- {(project.dependencies ?? []).map((dependency) => (
870
- <div key={dependency.id} className="rounded-lg border p-3">
871
- <div className="flex flex-wrap items-center justify-between gap-2">
872
- <p className="font-medium">{dependency.title}</p>
873
- <StatusBadge
874
- label={t(`dependencyStatus.${dependency.status}`)}
875
- className={dependencyStatusClasses[dependency.status]}
876
- />
877
- </div>
878
- <p className="mt-2 text-xs text-muted-foreground">
879
- {t('labels.owner')}: {dependency.owner}
880
- </p>
881
- <p className="text-xs text-muted-foreground">
882
- {t('labels.type')}: {t(`dependencyType.${dependency.type}`)}
883
- </p>
884
- </div>
885
- ))}
886
- </div>
887
- </SectionCard>
888
-
889
- <SectionCard
890
- className="xl:col-span-3"
891
- title={t('sections.decisions')}
892
- description={t('sections.decisionsDescription')}
893
- >
894
- <div className="space-y-3">
895
- {(project.decisions ?? []).map((decision) => {
896
- const owner = users.find((item) => item.id === decision.ownerUserId);
897
-
898
- return (
899
- <div key={decision.id} className="rounded-lg border p-3">
900
- <p className="font-medium">{decision.title}</p>
901
- <p className="mt-1 text-xs text-muted-foreground">
902
- {formatDate(decision.date)} • {t('labels.owner')}:{' '}
903
- {owner?.name ?? '-'}
904
- </p>
905
- <p className="mt-2 text-sm text-muted-foreground">
906
- {decision.summary}
907
- </p>
908
- </div>
909
- );
910
- })}
911
- </div>
912
- </SectionCard>
913
-
914
- <SectionCard
915
- className="xl:col-span-2"
916
- title={t('sections.nextActions')}
917
- description={t('sections.nextActionsDescription')}
918
- >
919
- <div className="space-y-2">
920
- {(project.nextActions ?? []).map((item) => (
921
- <div
922
- key={item}
923
- className="flex items-start gap-2 rounded-lg border p-3 text-sm"
924
- >
925
- <CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
926
- <span>{item}</span>
927
- </div>
928
- ))}
929
- </div>
930
- </SectionCard>
931
- </div>
932
- </TabsContent>
933
- </Tabs>
934
- </Page>
935
- );
936
- }
1
+ import { ProjectDetailsScreen } from '../../_components/project-details-screen';
2
+
3
+ export default async function OperationsProjectDetailsPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ id: string }>;
7
+ }) {
8
+ const { id } = await params;
9
+
10
+ return <ProjectDetailsScreen projectId={Number(id)} />;
11
+ }