@hed-hog/operations 0.0.317 → 0.0.319

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/controllers/operations-collaborator-costs.controller.d.ts +144 -0
  2. package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
  3. package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
  4. package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
  5. package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +11 -0
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-projects.controller.d.ts +31 -0
  10. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-projects.controller.js +23 -0
  12. package/dist/controllers/operations-projects.controller.js.map +1 -1
  13. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  14. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  15. package/dist/controllers/operations-reports.controller.js +53 -0
  16. package/dist/controllers/operations-reports.controller.js.map +1 -0
  17. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  18. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-tasks.controller.js +17 -5
  20. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  21. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  22. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  23. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  24. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  25. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  26. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  27. package/dist/dto/create-collaborator.dto.js +0 -6
  28. package/dist/dto/create-collaborator.dto.js.map +1 -1
  29. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  30. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  31. package/dist/dto/create-cost-type.dto.js +87 -0
  32. package/dist/dto/create-cost-type.dto.js.map +1 -0
  33. package/dist/dto/list-approvals.dto.d.ts +2 -0
  34. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  35. package/dist/dto/list-approvals.dto.js +10 -0
  36. package/dist/dto/list-approvals.dto.js.map +1 -1
  37. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  38. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  39. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  40. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  41. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  42. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  43. package/dist/dto/list-cost-types.dto.js +35 -0
  44. package/dist/dto/list-cost-types.dto.js.map +1 -0
  45. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-my-projects.dto.js +23 -0
  48. package/dist/dto/list-my-projects.dto.js.map +1 -0
  49. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  51. package/dist/dto/list-my-tasks.dto.js +33 -0
  52. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  53. package/dist/dto/list-projects.dto.d.ts +1 -0
  54. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  55. package/dist/dto/list-projects.dto.js +7 -0
  56. package/dist/dto/list-projects.dto.js.map +1 -1
  57. package/dist/dto/list-reports.dto.d.ts +16 -0
  58. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  59. package/dist/dto/list-reports.dto.js +75 -0
  60. package/dist/dto/list-reports.dto.js.map +1 -0
  61. package/dist/dto/list-tasks.dto.d.ts +2 -0
  62. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  63. package/dist/dto/list-tasks.dto.js +12 -0
  64. package/dist/dto/list-tasks.dto.js.map +1 -1
  65. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  66. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  67. package/dist/dto/list-timesheets.dto.js +10 -0
  68. package/dist/dto/list-timesheets.dto.js.map +1 -1
  69. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  70. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  71. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  72. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  73. package/dist/dto/update-task.dto.d.ts +1 -0
  74. package/dist/dto/update-task.dto.d.ts.map +1 -1
  75. package/dist/dto/update-task.dto.js +6 -0
  76. package/dist/dto/update-task.dto.js.map +1 -1
  77. package/dist/operations.module.d.ts.map +1 -1
  78. package/dist/operations.module.js +4 -0
  79. package/dist/operations.module.js.map +1 -1
  80. package/dist/operations.service.d.ts +457 -3
  81. package/dist/operations.service.d.ts.map +1 -1
  82. package/dist/operations.service.js +1445 -208
  83. package/dist/operations.service.js.map +1 -1
  84. package/dist/operations.service.spec.js +31 -7
  85. package/dist/operations.service.spec.js.map +1 -1
  86. package/hedhog/data/menu.yaml +112 -7
  87. package/hedhog/data/operations_cost_type.yaml +166 -0
  88. package/hedhog/data/route.yaml +185 -0
  89. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  90. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +94 -15
  91. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  92. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  93. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  94. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  95. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  96. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  97. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  98. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  99. package/hedhog/frontend/app/_lib/types.ts.ejs +229 -3
  100. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  101. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  102. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  103. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  104. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  105. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  106. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  107. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  108. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  109. package/hedhog/frontend/messages/en.json +234 -25
  110. package/hedhog/frontend/messages/pt.json +234 -25
  111. package/hedhog/table/operations_collaborator.yaml +0 -4
  112. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  113. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  114. package/hedhog/table/operations_cost_type.yaml +38 -0
  115. package/package.json +6 -6
  116. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  117. package/src/controllers/operations-collaborators.controller.ts +19 -8
  118. package/src/controllers/operations-projects.controller.ts +19 -8
  119. package/src/controllers/operations-reports.controller.ts +32 -0
  120. package/src/controllers/operations-tasks.controller.ts +32 -12
  121. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  122. package/src/dto/create-collaborator.dto.ts +9 -14
  123. package/src/dto/create-cost-type.dto.ts +62 -0
  124. package/src/dto/list-approvals.dto.ts +8 -0
  125. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  126. package/src/dto/list-cost-types.dto.ts +19 -0
  127. package/src/dto/list-my-projects.dto.ts +8 -0
  128. package/src/dto/list-my-tasks.dto.ts +17 -0
  129. package/src/dto/list-projects.dto.ts +7 -1
  130. package/src/dto/list-reports.dto.ts +51 -0
  131. package/src/dto/list-tasks.dto.ts +11 -1
  132. package/src/dto/list-timesheets.dto.ts +8 -0
  133. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  134. package/src/dto/update-task.dto.ts +6 -0
  135. package/src/operations.module.ts +7 -3
  136. package/src/operations.service.spec.ts +45 -7
  137. package/src/operations.service.ts +1992 -225
@@ -0,0 +1,771 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from '@/components/ui/card';
13
+ import { Input } from '@/components/ui/input';
14
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
15
+ import { Progress } from '@/components/ui/progress';
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from '@/components/ui/select';
23
+ import {
24
+ Table,
25
+ TableBody,
26
+ TableCell,
27
+ TableHead,
28
+ TableHeader,
29
+ TableRow,
30
+ } from '@/components/ui/table';
31
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
32
+ import {
33
+ AlertTriangle,
34
+ Banknote,
35
+ BriefcaseBusiness,
36
+ CalendarClock,
37
+ Clock3,
38
+ Route,
39
+ Target,
40
+ TrendingUp,
41
+ UserPlus,
42
+ UsersRound,
43
+ } from 'lucide-react';
44
+ import type { LucideIcon } from 'lucide-react';
45
+ import { useState } from 'react';
46
+ import type { TooltipProps } from 'recharts';
47
+ import {
48
+ Area,
49
+ AreaChart,
50
+ Bar,
51
+ BarChart,
52
+ CartesianGrid,
53
+ Cell,
54
+ Legend,
55
+ Pie,
56
+ PieChart,
57
+ PolarAngleAxis,
58
+ PolarGrid,
59
+ Radar,
60
+ RadarChart,
61
+ ResponsiveContainer,
62
+ Tooltip,
63
+ XAxis,
64
+ YAxis,
65
+ } from 'recharts';
66
+ import { OperationsHeader } from '../../_components/operations-header';
67
+ import { fetchOperations } from '../../_lib/api';
68
+ import type {
69
+ OperationsCollaboratorsReport,
70
+ OperationsReportScenario,
71
+ } from '../../_lib/types';
72
+ import { formatCurrency, formatHours } from '../../_lib/utils/format';
73
+
74
+ const CHART_COLORS = {
75
+ revenue: '#2563eb',
76
+ cost: '#dc2626',
77
+ profit: '#16a34a',
78
+ margin: '#7c3aed',
79
+ billable: '#0891b2',
80
+ internal: '#f59e0b',
81
+ free: '#94a3b8',
82
+ overload: '#e11d48',
83
+ };
84
+
85
+ const scenarioLabels: Record<OperationsReportScenario, string> = {
86
+ base: 'Base',
87
+ growth: 'Crescimento',
88
+ conservative: 'Conservador',
89
+ };
90
+
91
+ const planningIcons: LucideIcon[] = [
92
+ UserPlus,
93
+ Route,
94
+ AlertTriangle,
95
+ TrendingUp,
96
+ ];
97
+
98
+ function getDefaultRange() {
99
+ const year = new Date().getFullYear();
100
+ return { from: `${year}-01-01`, to: `${year}-12-31` };
101
+ }
102
+
103
+ function formatPercent(value: number) {
104
+ return `${Math.round(Number(value || 0))}%`;
105
+ }
106
+
107
+ function getTotalCost(row: OperationsCollaboratorsReport['rows'][number]) {
108
+ return row.salaryCost + row.benefitsCost + row.taxesCost + row.toolsCost;
109
+ }
110
+
111
+ function getAllocationStatus(allocation: number) {
112
+ if (allocation >= 98) {
113
+ return {
114
+ label: 'Sobrecarga',
115
+ className: 'border-rose-200 bg-rose-50 text-rose-700',
116
+ };
117
+ }
118
+ if (allocation < 75) {
119
+ return {
120
+ label: 'Ocioso',
121
+ className: 'border-amber-200 bg-amber-50 text-amber-700',
122
+ };
123
+ }
124
+ return {
125
+ label: 'Saudável',
126
+ className: 'border-emerald-200 bg-emerald-50 text-emerald-700',
127
+ };
128
+ }
129
+
130
+ function renderCurrencyTooltip({
131
+ active,
132
+ payload,
133
+ label,
134
+ }: TooltipProps<number, string>) {
135
+ if (!active || !payload?.length) return null;
136
+ return (
137
+ <div className="min-w-52 rounded-md border bg-popover px-3 py-2 text-sm text-popover-foreground shadow-xl">
138
+ <p className="mb-1 font-semibold">{label}</p>
139
+ {payload.map((entry) => (
140
+ <div
141
+ key={String(entry.dataKey ?? entry.name)}
142
+ className="flex items-center justify-between gap-3 py-0.5"
143
+ >
144
+ <span className="flex items-center gap-2 text-muted-foreground">
145
+ <span
146
+ className="size-2.5 rounded-full"
147
+ style={{ backgroundColor: entry.color }}
148
+ />
149
+ {entry.name}
150
+ </span>
151
+ <span className="font-semibold">
152
+ {formatCurrency(Number(entry.value ?? 0))}
153
+ </span>
154
+ </div>
155
+ ))}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ function renderNumberTooltip({
161
+ active,
162
+ payload,
163
+ label,
164
+ }: TooltipProps<number, string>) {
165
+ if (!active || !payload?.length) return null;
166
+ return (
167
+ <div className="min-w-44 rounded-md border bg-popover px-3 py-2 text-sm text-popover-foreground shadow-xl">
168
+ <p className="mb-1 font-semibold">{label}</p>
169
+ {payload.map((entry) => (
170
+ <div
171
+ key={String(entry.dataKey ?? entry.name)}
172
+ className="flex items-center justify-between gap-3 py-0.5"
173
+ >
174
+ <span className="flex items-center gap-2 text-muted-foreground">
175
+ <span
176
+ className="size-2.5 rounded-full"
177
+ style={{ backgroundColor: entry.color }}
178
+ />
179
+ {entry.name}
180
+ </span>
181
+ <span className="font-semibold">{Number(entry.value ?? 0)}</span>
182
+ </div>
183
+ ))}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ export default function OperationsCollaboratorReportsPage() {
189
+ const defaults = getDefaultRange();
190
+ const { request } = useApp();
191
+ const [startDate, setStartDate] = useState(defaults.from);
192
+ const [endDate, setEndDate] = useState(defaults.to);
193
+ const [departmentFilter, setDepartmentFilter] = useState('all');
194
+ const [contractFilter, setContractFilter] = useState('all');
195
+ const [scenario, setScenario] = useState<OperationsReportScenario>('base');
196
+
197
+ const { data } = useQuery<OperationsCollaboratorsReport>({
198
+ queryKey: [
199
+ 'operations-collaborators-report',
200
+ startDate,
201
+ endDate,
202
+ departmentFilter,
203
+ contractFilter,
204
+ scenario,
205
+ ],
206
+ queryFn: () => {
207
+ const params = new URLSearchParams({
208
+ from: startDate,
209
+ to: endDate,
210
+ department: departmentFilter,
211
+ contractType: contractFilter,
212
+ scenario,
213
+ });
214
+ return fetchOperations<OperationsCollaboratorsReport>(
215
+ request,
216
+ `/operations/reports/collaborators?${params.toString()}`
217
+ );
218
+ },
219
+ });
220
+
221
+ const summary = data?.summary ?? {
222
+ cost: 0,
223
+ salary: 0,
224
+ benefits: 0,
225
+ taxes: 0,
226
+ tools: 0,
227
+ billableValue: 0,
228
+ profit: 0,
229
+ margin: 0,
230
+ availableHours: 0,
231
+ allocatedHours: 0,
232
+ billableHours: 0,
233
+ internalHours: 0,
234
+ overtimeHours: 0,
235
+ freeHours: 0,
236
+ allocation: 0,
237
+ utilization: 0,
238
+ overloadCount: 0,
239
+ hourlyCost: 0,
240
+ };
241
+
242
+ return (
243
+ <Page>
244
+ <OperationsHeader
245
+ title="Relatório de Colaboradores"
246
+ description="Dashboard executivo com custo, lucro, capacidade atual e planejamento dos próximos 12 meses."
247
+ current="Relatórios / Colaboradores"
248
+ />
249
+
250
+ <div className="grid gap-3 rounded-lg border bg-card p-3 md:grid-cols-[1fr_1fr_180px_160px_180px_auto]">
251
+ <div className="space-y-1">
252
+ <span className="text-xs font-medium text-muted-foreground">
253
+ Início
254
+ </span>
255
+ <Input
256
+ type="date"
257
+ value={startDate}
258
+ onChange={(event) => setStartDate(event.target.value)}
259
+ />
260
+ </div>
261
+ <div className="space-y-1">
262
+ <span className="text-xs font-medium text-muted-foreground">Fim</span>
263
+ <Input
264
+ type="date"
265
+ value={endDate}
266
+ onChange={(event) => setEndDate(event.target.value)}
267
+ />
268
+ </div>
269
+ <div className="space-y-1">
270
+ <span className="text-xs font-medium text-muted-foreground">
271
+ Departamento
272
+ </span>
273
+ <Select value={departmentFilter} onValueChange={setDepartmentFilter}>
274
+ <SelectTrigger className="w-full">
275
+ <SelectValue />
276
+ </SelectTrigger>
277
+ <SelectContent>
278
+ <SelectItem value="all">Todos</SelectItem>
279
+ {(data?.filters.departments ?? []).map((department) => (
280
+ <SelectItem key={department} value={department}>
281
+ {department}
282
+ </SelectItem>
283
+ ))}
284
+ </SelectContent>
285
+ </Select>
286
+ </div>
287
+ <div className="space-y-1">
288
+ <span className="text-xs font-medium text-muted-foreground">
289
+ Vínculo
290
+ </span>
291
+ <Select value={contractFilter} onValueChange={setContractFilter}>
292
+ <SelectTrigger className="w-full">
293
+ <SelectValue />
294
+ </SelectTrigger>
295
+ <SelectContent>
296
+ <SelectItem value="all">Todos</SelectItem>
297
+ {(data?.filters.contractTypes ?? []).map((contractType) => (
298
+ <SelectItem key={contractType} value={contractType}>
299
+ {contractType}
300
+ </SelectItem>
301
+ ))}
302
+ </SelectContent>
303
+ </Select>
304
+ </div>
305
+ <div className="space-y-1">
306
+ <span className="text-xs font-medium text-muted-foreground">
307
+ Cenário
308
+ </span>
309
+ <Select
310
+ value={scenario}
311
+ onValueChange={(value) =>
312
+ setScenario(value as OperationsReportScenario)
313
+ }
314
+ >
315
+ <SelectTrigger className="w-full">
316
+ <SelectValue />
317
+ </SelectTrigger>
318
+ <SelectContent>
319
+ <SelectItem value="base">Base</SelectItem>
320
+ <SelectItem value="growth">Crescimento</SelectItem>
321
+ <SelectItem value="conservative">Conservador</SelectItem>
322
+ </SelectContent>
323
+ </Select>
324
+ </div>
325
+ <div className="flex items-end">
326
+ <Button
327
+ type="button"
328
+ variant="outline"
329
+ onClick={() => {
330
+ setStartDate(defaults.from);
331
+ setEndDate(defaults.to);
332
+ setDepartmentFilter('all');
333
+ setContractFilter('all');
334
+ setScenario('base');
335
+ }}
336
+ >
337
+ Limpar
338
+ </Button>
339
+ </div>
340
+ </div>
341
+
342
+ <KpiCardsGrid
343
+ columns={4}
344
+ items={[
345
+ {
346
+ key: 'headcount',
347
+ title: 'Headcount',
348
+ value: data?.rows.length ?? 0,
349
+ description: 'Colaboradores no recorte',
350
+ icon: UsersRound,
351
+ accentClassName: 'from-sky-500/20 via-cyan-400/10 to-transparent',
352
+ iconContainerClassName: 'bg-sky-50 text-sky-700',
353
+ },
354
+ {
355
+ key: 'cost',
356
+ title: 'Custo total',
357
+ value: formatCurrency(summary.cost),
358
+ description: `${formatCurrency(summary.hourlyCost)} por hora alocada`,
359
+ icon: Banknote,
360
+ accentClassName: 'from-rose-500/20 via-red-400/10 to-transparent',
361
+ iconContainerClassName: 'bg-rose-50 text-rose-700',
362
+ },
363
+ {
364
+ key: 'revenue',
365
+ title: 'Valor gerado',
366
+ value: formatCurrency(summary.billableValue),
367
+ description: `${formatHours(summary.billableHours)} faturáveis`,
368
+ icon: BriefcaseBusiness,
369
+ accentClassName:
370
+ 'from-emerald-500/20 via-green-400/10 to-transparent',
371
+ iconContainerClassName: 'bg-emerald-50 text-emerald-700',
372
+ },
373
+ {
374
+ key: 'margin',
375
+ title: 'Margem',
376
+ value: formatPercent(summary.margin),
377
+ description: formatCurrency(summary.profit),
378
+ icon: Target,
379
+ accentClassName:
380
+ 'from-violet-500/20 via-fuchsia-400/10 to-transparent',
381
+ iconContainerClassName: 'bg-violet-50 text-violet-700',
382
+ },
383
+ {
384
+ key: 'utilization',
385
+ title: 'Utilização',
386
+ value: formatPercent(summary.utilization),
387
+ description: 'Horas faturáveis sobre capacidade',
388
+ icon: Clock3,
389
+ layout: 'compact',
390
+ accentClassName:
391
+ 'from-cyan-500/20 via-blue-400/10 to-transparent',
392
+ iconContainerClassName: 'bg-cyan-50 text-cyan-700',
393
+ },
394
+ {
395
+ key: 'free',
396
+ title: 'Capacidade livre',
397
+ value: formatHours(summary.freeHours),
398
+ description: `${formatPercent(100 - summary.allocation)} disponível`,
399
+ icon: CalendarClock,
400
+ layout: 'compact',
401
+ accentClassName:
402
+ 'from-slate-500/20 via-slate-400/10 to-transparent',
403
+ iconContainerClassName: 'bg-slate-100 text-slate-700',
404
+ },
405
+ {
406
+ key: 'overtime',
407
+ title: 'Horas extras',
408
+ value: formatHours(summary.overtimeHours),
409
+ description: `${summary.overloadCount} pessoas em sobrecarga`,
410
+ icon: AlertTriangle,
411
+ layout: 'compact',
412
+ accentClassName:
413
+ 'from-amber-500/20 via-orange-400/10 to-transparent',
414
+ iconContainerClassName: 'bg-amber-50 text-amber-700',
415
+ },
416
+ {
417
+ key: 'scenario',
418
+ title: 'Cenário 12 meses',
419
+ value: scenarioLabels[scenario],
420
+ description: 'Projeção calculada no backend',
421
+ icon: TrendingUp,
422
+ layout: 'compact',
423
+ accentClassName:
424
+ 'from-indigo-500/20 via-blue-400/10 to-transparent',
425
+ iconContainerClassName: 'bg-indigo-50 text-indigo-700',
426
+ },
427
+ ]}
428
+ />
429
+
430
+ <div className="grid gap-4 xl:grid-cols-[1.4fr_0.9fr]">
431
+ <Card>
432
+ <CardHeader>
433
+ <CardTitle>Projeção de custo, valor e margem</CardTitle>
434
+ <CardDescription>
435
+ Simulação mensal para planejamento de capacidade nos próximos 12
436
+ meses.
437
+ </CardDescription>
438
+ </CardHeader>
439
+ <CardContent>
440
+ <ResponsiveContainer width="100%" height={360}>
441
+ <AreaChart data={data?.forecast ?? []}>
442
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
443
+ <XAxis dataKey="month" tick={{ fontSize: 12 }} />
444
+ <YAxis
445
+ tick={{ fontSize: 12 }}
446
+ tickFormatter={(value) => `${Number(value) / 1000}k`}
447
+ />
448
+ <Tooltip content={renderCurrencyTooltip} />
449
+ <Legend />
450
+ <Area
451
+ dataKey="revenue"
452
+ name="Valor gerado"
453
+ stroke={CHART_COLORS.revenue}
454
+ strokeWidth={3}
455
+ fill={CHART_COLORS.revenue}
456
+ fillOpacity={0.12}
457
+ />
458
+ <Area
459
+ dataKey="cost"
460
+ name="Custo"
461
+ stroke={CHART_COLORS.cost}
462
+ strokeWidth={3}
463
+ fill={CHART_COLORS.cost}
464
+ fillOpacity={0.08}
465
+ />
466
+ <Area
467
+ dataKey="profit"
468
+ name="Lucro"
469
+ stroke={CHART_COLORS.profit}
470
+ strokeWidth={2}
471
+ fill="transparent"
472
+ />
473
+ </AreaChart>
474
+ </ResponsiveContainer>
475
+ </CardContent>
476
+ </Card>
477
+
478
+ <Card>
479
+ <CardHeader>
480
+ <CardTitle>Composição de custo</CardTitle>
481
+ <CardDescription>
482
+ Distribuição dos custos usados no cálculo da margem.
483
+ </CardDescription>
484
+ </CardHeader>
485
+ <CardContent>
486
+ <ResponsiveContainer width="100%" height={300}>
487
+ <PieChart>
488
+ <Pie
489
+ data={(data?.costComposition ?? []).filter(
490
+ (item) => item.value > 0
491
+ )}
492
+ dataKey="value"
493
+ nameKey="name"
494
+ innerRadius={58}
495
+ outerRadius={98}
496
+ paddingAngle={3}
497
+ >
498
+ {(data?.costComposition ?? []).map((entry, index) => (
499
+ <Cell
500
+ key={entry.name}
501
+ fill={
502
+ [
503
+ CHART_COLORS.revenue,
504
+ CHART_COLORS.profit,
505
+ CHART_COLORS.internal,
506
+ CHART_COLORS.margin,
507
+ ][index % 4]
508
+ }
509
+ stroke="hsl(var(--background))"
510
+ strokeWidth={2}
511
+ />
512
+ ))}
513
+ </Pie>
514
+ <Legend iconType="circle" wrapperStyle={{ fontSize: 12 }} />
515
+ <Tooltip content={renderCurrencyTooltip} />
516
+ </PieChart>
517
+ </ResponsiveContainer>
518
+ </CardContent>
519
+ </Card>
520
+ </div>
521
+
522
+ <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
523
+ <Card>
524
+ <CardHeader>
525
+ <CardTitle>Capacidade por departamento</CardTitle>
526
+ <CardDescription>
527
+ Horas faturáveis, internas, livres e acima da capacidade.
528
+ </CardDescription>
529
+ </CardHeader>
530
+ <CardContent>
531
+ <ResponsiveContainer width="100%" height={320}>
532
+ <BarChart data={data?.capacityByDepartment ?? []}>
533
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
534
+ <XAxis dataKey="department" tick={{ fontSize: 12 }} />
535
+ <YAxis tick={{ fontSize: 12 }} />
536
+ <Tooltip content={renderNumberTooltip} />
537
+ <Legend />
538
+ <Bar
539
+ dataKey="Faturável"
540
+ stackId="capacity"
541
+ fill={CHART_COLORS.billable}
542
+ />
543
+ <Bar
544
+ dataKey="Interno"
545
+ stackId="capacity"
546
+ fill={CHART_COLORS.internal}
547
+ />
548
+ <Bar
549
+ dataKey="Livre"
550
+ stackId="capacity"
551
+ fill={CHART_COLORS.free}
552
+ />
553
+ <Bar
554
+ dataKey="Sobrecarga"
555
+ stackId="capacity"
556
+ fill={CHART_COLORS.overload}
557
+ radius={[6, 6, 0, 0]}
558
+ />
559
+ </BarChart>
560
+ </ResponsiveContainer>
561
+ </CardContent>
562
+ </Card>
563
+
564
+ <Card>
565
+ <CardHeader>
566
+ <CardTitle>Saúde por departamento</CardTitle>
567
+ <CardDescription>
568
+ Combina margem, alocação, utilização e risco operacional.
569
+ </CardDescription>
570
+ </CardHeader>
571
+ <CardContent>
572
+ <ResponsiveContainer width="100%" height={320}>
573
+ <RadarChart data={data?.health ?? []}>
574
+ <PolarGrid />
575
+ <PolarAngleAxis dataKey="department" tick={{ fontSize: 12 }} />
576
+ <Tooltip content={renderNumberTooltip} />
577
+ <Legend />
578
+ <Radar
579
+ dataKey="margem"
580
+ name="Margem"
581
+ stroke={CHART_COLORS.profit}
582
+ fill={CHART_COLORS.profit}
583
+ fillOpacity={0.18}
584
+ />
585
+ <Radar
586
+ dataKey="alocacao"
587
+ name="Alocação"
588
+ stroke={CHART_COLORS.margin}
589
+ fill={CHART_COLORS.margin}
590
+ fillOpacity={0.12}
591
+ />
592
+ <Radar
593
+ dataKey="saude"
594
+ name="Saúde"
595
+ stroke={CHART_COLORS.revenue}
596
+ fill={CHART_COLORS.revenue}
597
+ fillOpacity={0.1}
598
+ />
599
+ </RadarChart>
600
+ </ResponsiveContainer>
601
+ </CardContent>
602
+ </Card>
603
+ </div>
604
+
605
+ <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
606
+ <Card>
607
+ <CardHeader>
608
+ <CardTitle>Ranking de lucro e custo</CardTitle>
609
+ <CardDescription>
610
+ Colaboradores ordenados por contribuição estimada no período.
611
+ </CardDescription>
612
+ </CardHeader>
613
+ <CardContent>
614
+ <ResponsiveContainer width="100%" height={340}>
615
+ <BarChart
616
+ data={data?.ranking ?? []}
617
+ layout="vertical"
618
+ margin={{ left: 18, right: 16 }}
619
+ >
620
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
621
+ <XAxis
622
+ type="number"
623
+ tick={{ fontSize: 12 }}
624
+ tickFormatter={(value) => `${Number(value) / 1000}k`}
625
+ />
626
+ <YAxis dataKey="name" type="category" width={72} />
627
+ <Tooltip content={renderCurrencyTooltip} />
628
+ <Legend />
629
+ <Bar dataKey="Lucro" fill={CHART_COLORS.profit} radius={6} />
630
+ <Bar dataKey="Custo" fill={CHART_COLORS.cost} radius={6} />
631
+ </BarChart>
632
+ </ResponsiveContainer>
633
+ </CardContent>
634
+ </Card>
635
+
636
+ <div className="grid gap-4 md:grid-cols-2">
637
+ {(data?.planningCards ?? []).map((card, index) => {
638
+ const Icon =
639
+ planningIcons[index % planningIcons.length] ?? UserPlus;
640
+ const colors = [
641
+ 'bg-sky-50 text-sky-700',
642
+ 'bg-emerald-50 text-emerald-700',
643
+ 'bg-amber-50 text-amber-700',
644
+ 'bg-violet-50 text-violet-700',
645
+ ];
646
+ return (
647
+ <Card key={card.title}>
648
+ <CardContent className="space-y-3 p-4">
649
+ <div className="flex items-start justify-between gap-3">
650
+ <div>
651
+ <p className="text-sm font-medium text-muted-foreground">
652
+ {card.title}
653
+ </p>
654
+ <p className="mt-1 text-2xl font-semibold">
655
+ {card.value}
656
+ </p>
657
+ </div>
658
+ <div className={`rounded-xl p-2 ${colors[index % colors.length]}`}>
659
+ <Icon className="size-5" />
660
+ </div>
661
+ </div>
662
+ <p className="text-sm leading-relaxed text-muted-foreground">
663
+ {card.description}
664
+ </p>
665
+ </CardContent>
666
+ </Card>
667
+ );
668
+ })}
669
+ </div>
670
+ </div>
671
+
672
+ <Card>
673
+ <CardHeader>
674
+ <CardTitle>Detalhamento por colaborador</CardTitle>
675
+ <CardDescription>
676
+ Custo, valor gerado, lucro, alocação e recomendação executiva.
677
+ </CardDescription>
678
+ </CardHeader>
679
+ <CardContent className="overflow-x-auto">
680
+ <Table>
681
+ <TableHeader>
682
+ <TableRow>
683
+ <TableHead>Colaborador</TableHead>
684
+ <TableHead>Custo</TableHead>
685
+ <TableHead>Valor gerado</TableHead>
686
+ <TableHead>Lucro</TableHead>
687
+ <TableHead>Horas</TableHead>
688
+ <TableHead>Alocação</TableHead>
689
+ <TableHead>Risco</TableHead>
690
+ <TableHead>Recomendação</TableHead>
691
+ </TableRow>
692
+ </TableHeader>
693
+ <TableBody>
694
+ {(data?.rows ?? []).map((row) => {
695
+ const cost = getTotalCost(row);
696
+ const profit = row.billableValue - cost;
697
+ const allocation = row.availableHours
698
+ ? (row.allocatedHours / row.availableHours) * 100
699
+ : 0;
700
+ const utilization = row.availableHours
701
+ ? (row.billableHours / row.availableHours) * 100
702
+ : 0;
703
+ const status = getAllocationStatus(allocation);
704
+
705
+ return (
706
+ <TableRow key={row.id}>
707
+ <TableCell className="min-w-64">
708
+ <div className="font-medium">{row.name}</div>
709
+ <div className="text-xs text-muted-foreground">
710
+ {row.role} · {row.seniority} · {row.department} ·{' '}
711
+ {row.contractType}
712
+ </div>
713
+ </TableCell>
714
+ <TableCell>{formatCurrency(cost)}</TableCell>
715
+ <TableCell>{formatCurrency(row.billableValue)}</TableCell>
716
+ <TableCell
717
+ className={
718
+ profit >= 0 ? 'text-emerald-700' : 'text-rose-700'
719
+ }
720
+ >
721
+ {formatCurrency(profit)}
722
+ </TableCell>
723
+ <TableCell>
724
+ <div className="min-w-36 space-y-1">
725
+ <div className="flex items-center justify-between text-xs">
726
+ <span>{formatHours(row.billableHours)} fat.</span>
727
+ <span>{formatPercent(utilization)}</span>
728
+ </div>
729
+ <Progress value={Math.min(utilization, 100)} />
730
+ </div>
731
+ </TableCell>
732
+ <TableCell>
733
+ <div className="min-w-32 space-y-1">
734
+ <div className="flex items-center justify-between gap-2">
735
+ <Badge variant="outline" className={status.className}>
736
+ {status.label}
737
+ </Badge>
738
+ <span className="text-xs">
739
+ {formatPercent(allocation)}
740
+ </span>
741
+ </div>
742
+ <Progress value={Math.min(allocation, 100)} />
743
+ </div>
744
+ </TableCell>
745
+ <TableCell>
746
+ <Badge
747
+ variant="outline"
748
+ className={
749
+ row.risk === 'alto'
750
+ ? 'border-rose-200 bg-rose-50 text-rose-700'
751
+ : row.risk === 'médio'
752
+ ? 'border-amber-200 bg-amber-50 text-amber-700'
753
+ : 'border-emerald-200 bg-emerald-50 text-emerald-700'
754
+ }
755
+ >
756
+ {row.risk}
757
+ </Badge>
758
+ </TableCell>
759
+ <TableCell className="min-w-72 text-sm text-muted-foreground">
760
+ {row.recommendation}
761
+ </TableCell>
762
+ </TableRow>
763
+ );
764
+ })}
765
+ </TableBody>
766
+ </Table>
767
+ </CardContent>
768
+ </Card>
769
+ </Page>
770
+ );
771
+ }