@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,809 @@
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
+ Banknote,
34
+ CalendarClock,
35
+ Clock3,
36
+ FolderKanban,
37
+ Gauge,
38
+ Handshake,
39
+ Route,
40
+ Target,
41
+ TrendingUp,
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
+ Line,
56
+ LineChart,
57
+ Pie,
58
+ PieChart,
59
+ PolarAngleAxis,
60
+ PolarGrid,
61
+ Radar,
62
+ RadarChart,
63
+ ResponsiveContainer,
64
+ Tooltip,
65
+ XAxis,
66
+ YAxis,
67
+ } from 'recharts';
68
+ import { OperationsHeader } from '../../_components/operations-header';
69
+ import { fetchOperations } from '../../_lib/api';
70
+ import type {
71
+ OperationsProjectsReport,
72
+ OperationsReportScenario,
73
+ } from '../../_lib/types';
74
+ import { formatCurrency, formatHours } from '../../_lib/utils/format';
75
+
76
+ const CHART_COLORS = {
77
+ revenue: '#2563eb',
78
+ cost: '#dc2626',
79
+ profit: '#16a34a',
80
+ margin: '#7c3aed',
81
+ billable: '#0891b2',
82
+ internal: '#f59e0b',
83
+ rework: '#e11d48',
84
+ free: '#94a3b8',
85
+ planned: '#0f766e',
86
+ actual: '#ea580c',
87
+ };
88
+
89
+ const statusLabels: Record<string, string> = {
90
+ on_track: 'No prazo',
91
+ attention: 'Atenção',
92
+ late: 'Atrasado',
93
+ paused: 'Pausado',
94
+ };
95
+
96
+ const statusClasses: Record<string, string> = {
97
+ on_track: 'border-emerald-200 bg-emerald-50 text-emerald-700',
98
+ attention: 'border-amber-200 bg-amber-50 text-amber-700',
99
+ late: 'border-rose-200 bg-rose-50 text-rose-700',
100
+ paused: 'border-slate-200 bg-slate-100 text-slate-700',
101
+ };
102
+
103
+ const contractLabels: Record<string, string> = {
104
+ fixed_price: 'Preço fechado',
105
+ time_materials: 'Hora / Material',
106
+ retainer: 'Recorrente',
107
+ };
108
+
109
+ const scenarioLabels: Record<OperationsReportScenario, string> = {
110
+ base: 'Base',
111
+ growth: 'Crescimento',
112
+ conservative: 'Conservador',
113
+ };
114
+
115
+ const planningIcons: LucideIcon[] = [
116
+ CalendarClock,
117
+ Handshake,
118
+ Route,
119
+ Target,
120
+ ];
121
+
122
+ function getDefaultRange() {
123
+ const year = new Date().getFullYear();
124
+ return { from: `${year}-01-01`, to: `${year}-12-31` };
125
+ }
126
+
127
+ function formatPercent(value: number) {
128
+ return `${Math.round(Number(value || 0))}%`;
129
+ }
130
+
131
+ function renderCurrencyTooltip({
132
+ active,
133
+ payload,
134
+ label,
135
+ }: TooltipProps<number, string>) {
136
+ if (!active || !payload?.length) return null;
137
+
138
+ return (
139
+ <div className="min-w-52 rounded-md border bg-popover px-3 py-2 text-sm text-popover-foreground shadow-xl">
140
+ <p className="mb-1 font-semibold">{label}</p>
141
+ {payload.map((entry) => (
142
+ <div
143
+ key={String(entry.dataKey ?? entry.name)}
144
+ className="flex items-center justify-between gap-3 py-0.5"
145
+ >
146
+ <span className="flex items-center gap-2 text-muted-foreground">
147
+ <span
148
+ className="size-2.5 rounded-full"
149
+ style={{ backgroundColor: entry.color }}
150
+ />
151
+ {entry.name}
152
+ </span>
153
+ <span className="font-semibold">
154
+ {formatCurrency(Number(entry.value ?? 0))}
155
+ </span>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ function renderNumberTooltip({
163
+ active,
164
+ payload,
165
+ label,
166
+ }: TooltipProps<number, string>) {
167
+ if (!active || !payload?.length) return null;
168
+
169
+ return (
170
+ <div className="min-w-44 rounded-md border bg-popover px-3 py-2 text-sm text-popover-foreground shadow-xl">
171
+ <p className="mb-1 font-semibold">{label}</p>
172
+ {payload.map((entry) => (
173
+ <div
174
+ key={String(entry.dataKey ?? entry.name)}
175
+ className="flex items-center justify-between gap-3 py-0.5"
176
+ >
177
+ <span className="flex items-center gap-2 text-muted-foreground">
178
+ <span
179
+ className="size-2.5 rounded-full"
180
+ style={{ backgroundColor: entry.color }}
181
+ />
182
+ {entry.name}
183
+ </span>
184
+ <span className="font-semibold">{Number(entry.value ?? 0)}</span>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ export default function OperationsProjectReportsPage() {
192
+ const defaults = getDefaultRange();
193
+ const { request } = useApp();
194
+ const [startDate, setStartDate] = useState(defaults.from);
195
+ const [endDate, setEndDate] = useState(defaults.to);
196
+ const [statusFilter, setStatusFilter] = useState('all');
197
+ const [clientFilter, setClientFilter] = useState('all');
198
+ const [scenario, setScenario] = useState<OperationsReportScenario>('base');
199
+
200
+ const { data } = useQuery<OperationsProjectsReport>({
201
+ queryKey: [
202
+ 'operations-projects-report',
203
+ startDate,
204
+ endDate,
205
+ statusFilter,
206
+ clientFilter,
207
+ scenario,
208
+ ],
209
+ queryFn: () => {
210
+ const params = new URLSearchParams({
211
+ from: startDate,
212
+ to: endDate,
213
+ status: statusFilter,
214
+ client: clientFilter,
215
+ scenario,
216
+ });
217
+ return fetchOperations<OperationsProjectsReport>(
218
+ request,
219
+ `/operations/reports/projects?${params.toString()}`
220
+ );
221
+ },
222
+ });
223
+
224
+ const summary = data?.summary ?? {
225
+ contractedRevenue: 0,
226
+ recognizedRevenue: 0,
227
+ realizedCost: 0,
228
+ forecastCost: 0,
229
+ profit: 0,
230
+ margin: 0,
231
+ plannedHours: 0,
232
+ actualHours: 0,
233
+ billableHours: 0,
234
+ reworkHours: 0,
235
+ backlogValue: 0,
236
+ avgDeadline: 0,
237
+ avgAllocation: 0,
238
+ atRisk: 0,
239
+ burnRate: 0,
240
+ };
241
+ const clients = data?.filters.clients ?? [];
242
+
243
+ return (
244
+ <Page>
245
+ <OperationsHeader
246
+ title="Relatório de Projetos"
247
+ description="Dashboard executivo com receita, custo, lucro, prazo, risco e planejamento dos próximos 12 meses."
248
+ current="Relatórios / Projetos"
249
+ />
250
+
251
+ <div className="grid gap-3 rounded-lg border bg-card p-3 md:grid-cols-[1fr_1fr_180px_180px_180px_auto]">
252
+ <div className="space-y-1">
253
+ <span className="text-xs font-medium text-muted-foreground">
254
+ Início
255
+ </span>
256
+ <Input
257
+ type="date"
258
+ value={startDate}
259
+ onChange={(event) => setStartDate(event.target.value)}
260
+ />
261
+ </div>
262
+ <div className="space-y-1">
263
+ <span className="text-xs font-medium text-muted-foreground">Fim</span>
264
+ <Input
265
+ type="date"
266
+ value={endDate}
267
+ onChange={(event) => setEndDate(event.target.value)}
268
+ />
269
+ </div>
270
+ <div className="space-y-1">
271
+ <span className="text-xs font-medium text-muted-foreground">
272
+ Status
273
+ </span>
274
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
275
+ <SelectTrigger className="w-full">
276
+ <SelectValue />
277
+ </SelectTrigger>
278
+ <SelectContent>
279
+ <SelectItem value="all">Todos</SelectItem>
280
+ <SelectItem value="on_track">No prazo</SelectItem>
281
+ <SelectItem value="attention">Atenção</SelectItem>
282
+ <SelectItem value="late">Atrasado</SelectItem>
283
+ <SelectItem value="paused">Pausado</SelectItem>
284
+ </SelectContent>
285
+ </Select>
286
+ </div>
287
+ <div className="space-y-1">
288
+ <span className="text-xs font-medium text-muted-foreground">
289
+ Cliente
290
+ </span>
291
+ <Select value={clientFilter} onValueChange={setClientFilter}>
292
+ <SelectTrigger className="w-full">
293
+ <SelectValue />
294
+ </SelectTrigger>
295
+ <SelectContent>
296
+ <SelectItem value="all">Todos</SelectItem>
297
+ {clients.map((client) => (
298
+ <SelectItem key={client} value={client}>
299
+ {client}
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
+ setStatusFilter('all');
333
+ setClientFilter('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: 'active',
347
+ title: 'Projetos',
348
+ value: data?.rows.length ?? 0,
349
+ description: `${summary.atRisk} em risco operacional`,
350
+ icon: FolderKanban,
351
+ accentClassName: 'from-sky-500/20 via-cyan-400/10 to-transparent',
352
+ iconContainerClassName: 'bg-sky-50 text-sky-700',
353
+ },
354
+ {
355
+ key: 'revenue',
356
+ title: 'Receita reconhecida',
357
+ value: formatCurrency(summary.recognizedRevenue),
358
+ description: `${formatCurrency(summary.contractedRevenue)} contratado`,
359
+ icon: Banknote,
360
+ accentClassName:
361
+ 'from-emerald-500/20 via-green-400/10 to-transparent',
362
+ iconContainerClassName: 'bg-emerald-50 text-emerald-700',
363
+ },
364
+ {
365
+ key: 'cost',
366
+ title: 'Custo realizado',
367
+ value: formatCurrency(summary.realizedCost),
368
+ description: `${formatPercent(summary.burnRate)} de burn rate`,
369
+ icon: Gauge,
370
+ accentClassName: 'from-rose-500/20 via-red-400/10 to-transparent',
371
+ iconContainerClassName: 'bg-rose-50 text-rose-700',
372
+ },
373
+ {
374
+ key: 'profit',
375
+ title: 'Lucro',
376
+ value: formatCurrency(summary.profit),
377
+ description: `${formatPercent(summary.margin)} de margem`,
378
+ icon: TrendingUp,
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: 'hours',
385
+ title: 'Horas consumidas',
386
+ value: formatHours(summary.actualHours),
387
+ description: `${formatHours(summary.billableHours)} faturáveis`,
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: 'deadline',
396
+ title: 'Avanço médio',
397
+ value: formatPercent(summary.avgDeadline),
398
+ description: 'Progresso físico dos projetos',
399
+ icon: CalendarClock,
400
+ layout: 'compact',
401
+ accentClassName:
402
+ 'from-amber-500/20 via-yellow-400/10 to-transparent',
403
+ iconContainerClassName: 'bg-amber-50 text-amber-700',
404
+ },
405
+ {
406
+ key: 'allocation',
407
+ title: 'Alocação média',
408
+ value: formatPercent(summary.avgAllocation),
409
+ description: 'Acima de 95% exige atenção',
410
+ icon: UsersRound,
411
+ layout: 'compact',
412
+ accentClassName:
413
+ 'from-indigo-500/20 via-blue-400/10 to-transparent',
414
+ iconContainerClassName: 'bg-indigo-50 text-indigo-700',
415
+ },
416
+ {
417
+ key: 'backlog',
418
+ title: 'Backlog futuro',
419
+ value: formatCurrency(summary.backlogValue),
420
+ description: `Cenário ${scenarioLabels[scenario]}`,
421
+ icon: Target,
422
+ layout: 'compact',
423
+ accentClassName:
424
+ 'from-slate-500/20 via-slate-400/10 to-transparent',
425
+ iconContainerClassName: 'bg-slate-100 text-slate-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 receita, custo e lucro</CardTitle>
434
+ <CardDescription>
435
+ Simulação mensal para planejamento de portfólio 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="Receita"
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 custos</CardTitle>
481
+ <CardDescription>
482
+ Equipe, infraestrutura, licenças, terceiros e retrabalho.
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.internal,
505
+ CHART_COLORS.margin,
506
+ CHART_COLORS.billable,
507
+ CHART_COLORS.rework,
508
+ ][index % 5]
509
+ }
510
+ stroke="hsl(var(--background))"
511
+ strokeWidth={2}
512
+ />
513
+ ))}
514
+ </Pie>
515
+ <Legend iconType="circle" wrapperStyle={{ fontSize: 12 }} />
516
+ <Tooltip content={renderCurrencyTooltip} />
517
+ </PieChart>
518
+ </ResponsiveContainer>
519
+ </CardContent>
520
+ </Card>
521
+ </div>
522
+
523
+ <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
524
+ <Card>
525
+ <CardHeader>
526
+ <CardTitle>Horas por projeto</CardTitle>
527
+ <CardDescription>
528
+ Separação entre horas faturáveis, internas, retrabalho e saldo.
529
+ </CardDescription>
530
+ </CardHeader>
531
+ <CardContent>
532
+ <ResponsiveContainer width="100%" height={330}>
533
+ <BarChart data={data?.hoursByProject ?? []}>
534
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
535
+ <XAxis dataKey="project" tick={{ fontSize: 12 }} />
536
+ <YAxis tick={{ fontSize: 12 }} />
537
+ <Tooltip content={renderNumberTooltip} />
538
+ <Legend />
539
+ <Bar
540
+ dataKey="Faturável"
541
+ stackId="hours"
542
+ fill={CHART_COLORS.billable}
543
+ />
544
+ <Bar
545
+ dataKey="Interno"
546
+ stackId="hours"
547
+ fill={CHART_COLORS.internal}
548
+ />
549
+ <Bar
550
+ dataKey="Retrabalho"
551
+ stackId="hours"
552
+ fill={CHART_COLORS.rework}
553
+ />
554
+ <Bar
555
+ dataKey="Livre"
556
+ stackId="hours"
557
+ fill={CHART_COLORS.free}
558
+ radius={[6, 6, 0, 0]}
559
+ />
560
+ </BarChart>
561
+ </ResponsiveContainer>
562
+ </CardContent>
563
+ </Card>
564
+
565
+ <Card>
566
+ <CardHeader>
567
+ <CardTitle>Saúde dos projetos</CardTitle>
568
+ <CardDescription>
569
+ Combina margem, prazo, alocação e risco por projeto.
570
+ </CardDescription>
571
+ </CardHeader>
572
+ <CardContent>
573
+ <ResponsiveContainer width="100%" height={330}>
574
+ <RadarChart data={data?.health ?? []}>
575
+ <PolarGrid />
576
+ <PolarAngleAxis dataKey="project" tick={{ fontSize: 12 }} />
577
+ <Tooltip content={renderNumberTooltip} />
578
+ <Legend />
579
+ <Radar
580
+ dataKey="margem"
581
+ name="Margem"
582
+ stroke={CHART_COLORS.profit}
583
+ fill={CHART_COLORS.profit}
584
+ fillOpacity={0.16}
585
+ />
586
+ <Radar
587
+ dataKey="prazo"
588
+ name="Prazo"
589
+ stroke={CHART_COLORS.revenue}
590
+ fill={CHART_COLORS.revenue}
591
+ fillOpacity={0.12}
592
+ />
593
+ <Radar
594
+ dataKey="saude"
595
+ name="Saúde"
596
+ stroke={CHART_COLORS.margin}
597
+ fill={CHART_COLORS.margin}
598
+ fillOpacity={0.1}
599
+ />
600
+ </RadarChart>
601
+ </ResponsiveContainer>
602
+ </CardContent>
603
+ </Card>
604
+ </div>
605
+
606
+ <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
607
+ <Card>
608
+ <CardHeader>
609
+ <CardTitle>Ranking de lucro e custo</CardTitle>
610
+ <CardDescription>
611
+ Projetos ordenados por contribuição estimada no período.
612
+ </CardDescription>
613
+ </CardHeader>
614
+ <CardContent>
615
+ <ResponsiveContainer width="100%" height={340}>
616
+ <BarChart
617
+ data={data?.ranking ?? []}
618
+ layout="vertical"
619
+ margin={{ left: 56, right: 16 }}
620
+ >
621
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
622
+ <XAxis
623
+ type="number"
624
+ tick={{ fontSize: 12 }}
625
+ tickFormatter={(value) => `${Number(value) / 1000}k`}
626
+ />
627
+ <YAxis dataKey="name" type="category" width={120} />
628
+ <Tooltip content={renderCurrencyTooltip} />
629
+ <Legend />
630
+ <Bar dataKey="Lucro" fill={CHART_COLORS.profit} radius={6} />
631
+ <Bar dataKey="Custo" fill={CHART_COLORS.cost} radius={6} />
632
+ </BarChart>
633
+ </ResponsiveContainer>
634
+ </CardContent>
635
+ </Card>
636
+
637
+ <Card>
638
+ <CardHeader>
639
+ <CardTitle>Planejado vs realizado</CardTitle>
640
+ <CardDescription>
641
+ Tendência mensal de consumo de horas frente ao plano.
642
+ </CardDescription>
643
+ </CardHeader>
644
+ <CardContent>
645
+ <ResponsiveContainer width="100%" height={340}>
646
+ <LineChart data={data?.progress ?? []}>
647
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
648
+ <XAxis dataKey="month" tick={{ fontSize: 12 }} />
649
+ <YAxis tick={{ fontSize: 12 }} />
650
+ <Tooltip content={renderNumberTooltip} />
651
+ <Legend />
652
+ <Line
653
+ type="monotone"
654
+ dataKey="Planejado"
655
+ stroke={CHART_COLORS.planned}
656
+ strokeWidth={3}
657
+ dot={false}
658
+ />
659
+ <Line
660
+ type="monotone"
661
+ dataKey="Realizado"
662
+ stroke={CHART_COLORS.actual}
663
+ strokeWidth={3}
664
+ dot={false}
665
+ />
666
+ </LineChart>
667
+ </ResponsiveContainer>
668
+ </CardContent>
669
+ </Card>
670
+ </div>
671
+
672
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
673
+ {(data?.planningCards ?? []).map((card, index) => {
674
+ const Icon =
675
+ planningIcons[index % planningIcons.length] ?? CalendarClock;
676
+ const colors = [
677
+ 'bg-amber-50 text-amber-700',
678
+ 'bg-sky-50 text-sky-700',
679
+ 'bg-emerald-50 text-emerald-700',
680
+ 'bg-violet-50 text-violet-700',
681
+ ];
682
+ return (
683
+ <Card key={card.title}>
684
+ <CardContent className="space-y-3 p-4">
685
+ <div className="flex items-start justify-between gap-3">
686
+ <div>
687
+ <p className="text-sm font-medium text-muted-foreground">
688
+ {card.title}
689
+ </p>
690
+ <p className="mt-1 text-2xl font-semibold">
691
+ {card.value}
692
+ </p>
693
+ </div>
694
+ <div className={`rounded-xl p-2 ${colors[index % colors.length]}`}>
695
+ <Icon className="size-5" />
696
+ </div>
697
+ </div>
698
+ <p className="text-sm leading-relaxed text-muted-foreground">
699
+ {card.description}
700
+ </p>
701
+ </CardContent>
702
+ </Card>
703
+ );
704
+ })}
705
+ </div>
706
+
707
+ <Card>
708
+ <CardHeader>
709
+ <CardTitle>Detalhamento por projeto</CardTitle>
710
+ <CardDescription>
711
+ Receita, custo, lucro, horas, prazo, risco e recomendação executiva.
712
+ </CardDescription>
713
+ </CardHeader>
714
+ <CardContent className="overflow-x-auto">
715
+ <Table>
716
+ <TableHeader>
717
+ <TableRow>
718
+ <TableHead>Projeto</TableHead>
719
+ <TableHead>Status</TableHead>
720
+ <TableHead>Receita</TableHead>
721
+ <TableHead>Custo</TableHead>
722
+ <TableHead>Lucro</TableHead>
723
+ <TableHead>Horas</TableHead>
724
+ <TableHead>Prazo</TableHead>
725
+ <TableHead>Risco</TableHead>
726
+ <TableHead>Recomendação</TableHead>
727
+ </TableRow>
728
+ </TableHeader>
729
+ <TableBody>
730
+ {(data?.rows ?? []).map((row) => {
731
+ const profit = row.recognizedRevenue - row.realizedCost;
732
+ const margin = row.recognizedRevenue
733
+ ? (profit / row.recognizedRevenue) * 100
734
+ : 0;
735
+ const hourUsage = row.plannedHours
736
+ ? (row.actualHours / row.plannedHours) * 100
737
+ : 0;
738
+ return (
739
+ <TableRow key={row.id}>
740
+ <TableCell className="min-w-72">
741
+ <div className="font-medium">{row.name}</div>
742
+ <div className="text-xs text-muted-foreground">
743
+ {row.client} · {row.manager} · {row.squad} ·{' '}
744
+ {contractLabels[row.contractType]}
745
+ </div>
746
+ </TableCell>
747
+ <TableCell>
748
+ <Badge
749
+ variant="outline"
750
+ className={statusClasses[row.status]}
751
+ >
752
+ {statusLabels[row.status]}
753
+ </Badge>
754
+ </TableCell>
755
+ <TableCell>{formatCurrency(row.recognizedRevenue)}</TableCell>
756
+ <TableCell>{formatCurrency(row.realizedCost)}</TableCell>
757
+ <TableCell
758
+ className={
759
+ profit >= 0 ? 'text-emerald-700' : 'text-rose-700'
760
+ }
761
+ >
762
+ <div>{formatCurrency(profit)}</div>
763
+ <div className="text-xs">{formatPercent(margin)}</div>
764
+ </TableCell>
765
+ <TableCell>
766
+ <div className="min-w-36 space-y-1">
767
+ <div className="flex items-center justify-between text-xs">
768
+ <span>{formatHours(row.actualHours)}</span>
769
+ <span>{formatPercent(hourUsage)}</span>
770
+ </div>
771
+ <Progress value={Math.min(hourUsage, 100)} />
772
+ </div>
773
+ </TableCell>
774
+ <TableCell>
775
+ <div className="min-w-32 space-y-1">
776
+ <div className="flex items-center justify-between text-xs">
777
+ <span>Físico</span>
778
+ <span>{formatPercent(row.physicalProgress)}</span>
779
+ </div>
780
+ <Progress value={row.physicalProgress} />
781
+ </div>
782
+ </TableCell>
783
+ <TableCell>
784
+ <Badge
785
+ variant="outline"
786
+ className={
787
+ row.risk === 'alto'
788
+ ? 'border-rose-200 bg-rose-50 text-rose-700'
789
+ : row.risk === 'médio'
790
+ ? 'border-amber-200 bg-amber-50 text-amber-700'
791
+ : 'border-emerald-200 bg-emerald-50 text-emerald-700'
792
+ }
793
+ >
794
+ {row.risk}
795
+ </Badge>
796
+ </TableCell>
797
+ <TableCell className="min-w-72 text-sm text-muted-foreground">
798
+ {row.recommendation}
799
+ </TableCell>
800
+ </TableRow>
801
+ );
802
+ })}
803
+ </TableBody>
804
+ </Table>
805
+ </CardContent>
806
+ </Card>
807
+ </Page>
808
+ );
809
+ }