Skip to content

Commit aa14008

Browse files
author
OpenCode
committed
Add UMR donut breakdown to dashboard
1 parent 1b51e2b commit aa14008

6 files changed

Lines changed: 411 additions & 157 deletions

File tree

PIPELINE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
11.1. Se a sincronização remota falhar, manter o snapshot local como pendente e tentar reenviar no boot, no evento `online`, no retorno da tela ao primeiro plano e em ciclo periódico.
1919
11.2. Expor telemetria local de sincronização no painel de configurações, incluindo estado da fila, contagem de falhas e atraso de retentativa.
2020
12. Permitir exportação/importação JSON e exportação CSV.
21-
13. Renderizar o dashboard com intervalo selecionável, atualização ao vivo para qualquer intervalo que inclua `Hoje`, mapa da frota com base satélite do Google Maps, travado para navegação manual e autoajustado aos apontamentos ativos com GPS, além de gráficos de rosca para KPIs e pizza por período/UMR.
21+
13. Renderizar o dashboard com intervalo selecionável, atualização ao vivo para qualquer intervalo que inclua `Hoje`, mapa da frota com base satélite do Google Maps, travado para navegação manual e autoajustado aos apontamentos ativos com GPS, além de gráficos de rosca para KPIs, disponibilidade/utilização por UMR e pizza por período/UMR.
2222
14. Encerrar manualmente o apontamento em aberto ao fim do expediente pelo botão `Encerrar a atividade` abaixo da tabela `Histórico do dia` no fim do módulo de operação.
2323
15. Permitir `Recarregar Atualização do Sistema` para buscar a versão mais recente sem tocar em `localStorage` ou `IndexedDB`.
2424
16. Atualizar automaticamente a PWA em intervalo periódico quando houver conexão, usando rede primeiro para HTML/assets e cache apenas como fallback offline.

SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Sistema de apontamento operacional para UMBs e caminhões, com frontend no GitHu
6161

6262
- Tela de login por usuário.
6363
- Tela operacional.
64-
- Dashboard com intervalo selecionável e gráficos de rosca para KPIs gerais e por UMR, além de gráficos de pizza de códigos por UMR; os percentuais dos gráficos de códigos por UMR são calculados pelo tempo coletado no período, não pela quantidade de apontamentos.
64+
- Dashboard com intervalo selecionável e gráficos de rosca para KPIs gerais, por UMR e para disponibilidade/utilização por UMR, além de gráficos de pizza de códigos por UMR; os percentuais dos gráficos de códigos por UMR são calculados pelo tempo coletado no período, não pela quantidade de apontamentos.
6565
- Botão de recarga do sistema nas telas logadas.
6666
- Backups JSON e CSV.
6767
- Administração restrita de usuários.

src/components/DashboardKpiCharts.jsx

Lines changed: 167 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,33 @@ import { useMemo } from 'react';
22
import { useApp } from '../context/AppContext';
33
import { formatDuration } from '../services/timeService';
44
import { formatUtilization } from '../services/calculationService';
5+
import { StatusChip } from './StatusChip';
56
import { PieChartCard } from './PieChartCard';
67

8+
const ENAEX_COLORS = {
9+
red: '#E20613',
10+
redAlt: '#E3001B',
11+
graphite: '#38424B',
12+
graphiteMedium: '#3D434C',
13+
graphiteDark: '#283138',
14+
gray: '#6B6F75',
15+
light: '#EDEFF2',
16+
white: '#FFFFFF',
17+
};
18+
719
const SEGMENT_COLORS = {
8-
OPERAÇÃO: '#ed0016',
9-
MANUTENÇÃO: '#b97909',
10-
OCIOSIDADE: '#6e7d92',
11-
OUTROS: '#2f6da8',
12-
operation: '#ed0016',
13-
maintenance: '#b97909',
14-
meal: '#168255',
15-
gaps: '#9e0012',
16-
idle: '#6e7d92',
17-
other: '#2f6da8',
20+
OPERAÇÃO: ENAEX_COLORS.red,
21+
MANUTENÇÃO: ENAEX_COLORS.graphite,
22+
OCIOSIDADE: ENAEX_COLORS.gray,
23+
OUTROS: ENAEX_COLORS.graphiteMedium,
24+
operation: ENAEX_COLORS.red,
25+
maintenance: ENAEX_COLORS.graphite,
26+
meal: ENAEX_COLORS.redAlt,
27+
gaps: ENAEX_COLORS.graphiteDark,
28+
idle: ENAEX_COLORS.gray,
29+
other: ENAEX_COLORS.graphiteMedium,
30+
available: ENAEX_COLORS.red,
31+
rest: ENAEX_COLORS.graphiteMedium,
1832
};
1933

2034
function formatHours(value, t) {
@@ -105,6 +119,147 @@ function getActivityFootnote(activityName, t) {
105119
return value && value !== '-' ? value : t('common.noActivity');
106120
}
107121

122+
function getAvailabilityMinutes(item, summary) {
123+
const availableMinutes = Number(item?.availableMinutes || summary.periodAvailableMinutes || 0);
124+
const maintenanceMinutes = Number(item?.maintenanceMinutes || 0);
125+
return Math.max(0, availableMinutes - maintenanceMinutes);
126+
}
127+
128+
function getRestMinutes(item, summary) {
129+
const availableMinutes = Number(item?.availableMinutes || summary.periodAvailableMinutes || 0);
130+
const operationMinutes = Number(item?.operationMinutes || 0);
131+
return Math.max(0, availableMinutes - operationMinutes);
132+
}
133+
134+
function buildAvailabilitySegments(item, summary, language, t) {
135+
const availableMinutes = getAvailabilityMinutes(item, summary);
136+
const maintenanceMinutes = Number(item?.maintenanceMinutes || 0);
137+
138+
return [
139+
{
140+
key: 'available',
141+
label: t('dashboard.series.available'),
142+
value: availableMinutes,
143+
detail: formatDuration(availableMinutes, language),
144+
color: SEGMENT_COLORS.available,
145+
},
146+
{
147+
key: 'maintenance',
148+
label: t('dashboard.series.maintenance'),
149+
value: maintenanceMinutes,
150+
detail: formatDuration(maintenanceMinutes, language),
151+
color: SEGMENT_COLORS.maintenance,
152+
},
153+
];
154+
}
155+
156+
function buildUtilizationSegments(item, summary, language, t) {
157+
const operationMinutes = Number(item?.operationMinutes || 0);
158+
const restMinutes = getRestMinutes(item, summary);
159+
160+
return [
161+
{
162+
key: 'operation',
163+
label: t('dashboard.series.operation'),
164+
value: operationMinutes,
165+
detail: formatDuration(operationMinutes, language),
166+
color: SEGMENT_COLORS.operation,
167+
},
168+
{
169+
key: 'rest',
170+
label: t('dashboard.series.rest'),
171+
value: restMinutes,
172+
detail: formatDuration(restMinutes, language),
173+
color: SEGMENT_COLORS.rest,
174+
},
175+
];
176+
}
177+
178+
function buildAvailabilityMetrics(item, summary, t) {
179+
return [
180+
{ label: t('dashboard.series.available'), value: formatHours(getAvailabilityMinutes(item, summary) / 60, t) },
181+
{ label: t('dashboard.series.maintenance'), value: formatHours(Number(item?.maintenanceMinutes || 0) / 60, t) },
182+
];
183+
}
184+
185+
function buildUtilizationMetrics(item, summary, t) {
186+
return [
187+
{ label: t('dashboard.series.operation'), value: formatHours(Number(item?.operationMinutes || 0) / 60, t) },
188+
{ label: t('dashboard.series.rest'), value: formatHours(getRestMinutes(item, summary) / 60, t) },
189+
];
190+
}
191+
192+
function AvailabilityUtilizationByEquipment({ summary }) {
193+
const { language, t } = useApp();
194+
const equipmentCards = useMemo(
195+
() => summary.equipmentMetrics.filter((item) => Number(item.totalMinutes || 0) > 0),
196+
[summary.equipmentMetrics],
197+
);
198+
199+
return (
200+
<section className="card dashboard-block dashboard-kpi-graphs__group dashboard-availability-group">
201+
<div className="card__head">
202+
<div>
203+
<p className="eyebrow">{t('dashboard.sections.availabilityAndUtilizationByEquipment')}</p>
204+
<h2>{t('dashboard.sections.availabilityAndUtilizationByEquipment')}</h2>
205+
</div>
206+
</div>
207+
208+
{!equipmentCards.length ? <p className="empty-state">{t('dashboard.empty.available')}</p> : null}
209+
210+
<div className="dashboard-availability-grid">
211+
{equipmentCards.map((item) => {
212+
const availableMinutes = Number(item.availableMinutes || summary.periodAvailableMinutes || 0);
213+
const availabilityMinutes = getAvailabilityMinutes(item, summary);
214+
const availabilityPercent = availableMinutes > 0 ? Number(((availabilityMinutes / availableMinutes) * 100).toFixed(1)) : 0;
215+
const utilizationPercent = Number(item.utilizationPercent || 0);
216+
const utilizationTone = utilizationPercent >= 75 ? 'success' : utilizationPercent >= 50 ? 'warning' : 'danger';
217+
218+
return (
219+
<article key={item.equipmentId} className="card dashboard-block dashboard-availability-card">
220+
<div className="dashboard-availability-card__head">
221+
<div>
222+
<p className="eyebrow">{item.code}</p>
223+
<h3>{item.plate}</h3>
224+
</div>
225+
<StatusChip tone={utilizationTone}>{formatUtilization(utilizationPercent)}</StatusChip>
226+
</div>
227+
228+
<p className="dashboard-availability-card__copy">{item.currentActivityName || t('common.noActivity')}</p>
229+
230+
<div className="dashboard-availability-card__charts">
231+
<PieChartCard
232+
eyebrow={t('dashboard.sections.physicalIndicators')}
233+
title={t('dashboard.cards.physicalAvailability')}
234+
subtitle={`${t('dashboard.series.available')} x ${t('dashboard.series.maintenance')}`}
235+
centerValue={formatUtilization(availabilityPercent)}
236+
centerLabel={item.code}
237+
segments={buildAvailabilitySegments(item, summary, language, t)}
238+
metrics={buildAvailabilityMetrics(item, summary, t)}
239+
showLegend={false}
240+
className="pie-chart-card--compact pie-chart-card--micro"
241+
/>
242+
243+
<PieChartCard
244+
eyebrow={t('dashboard.sections.availabilityAndUtilization')}
245+
title={t('dashboard.cards.utilization')}
246+
subtitle={`${t('dashboard.series.operation')} x ${t('dashboard.series.rest')}`}
247+
centerValue={formatUtilization(utilizationPercent)}
248+
centerLabel={item.code}
249+
segments={buildUtilizationSegments(item, summary, language, t)}
250+
metrics={buildUtilizationMetrics(item, summary, t)}
251+
showLegend={false}
252+
className="pie-chart-card--compact pie-chart-card--micro"
253+
/>
254+
</div>
255+
</article>
256+
);
257+
})}
258+
</div>
259+
</section>
260+
);
261+
}
262+
108263
export function DashboardKpiCharts({ summary }) {
109264
const { language, t } = useApp();
110265
const equipmentById = useMemo(
@@ -192,6 +347,8 @@ export function DashboardKpiCharts({ summary }) {
192347
})}
193348
</div>
194349
</section>
350+
351+
<AvailabilityUtilizationByEquipment summary={summary} />
195352
</div>
196353
);
197354
}

src/components/PieChartCard.jsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from 'react';
22
import { useApp } from '../context/AppContext';
33

4-
const DEFAULT_COLORS = ['#245f9e', '#1f8a57', '#c28a15', '#bf3d3d', '#6b7280', '#8b5cf6', '#0ea5e9', '#f97316'];
4+
const DEFAULT_COLORS = ['#E20613', '#E3001B', '#38424B', '#3D434C', '#283138', '#6B6F75', '#E20613', '#E3001B'];
55

66
function getNumericValue(segment) {
77
return Number(segment?.value ?? segment?.count ?? segment?.minutes ?? 0);
@@ -17,6 +17,7 @@ export function PieChartCard({
1717
emptyMessage = '',
1818
footnote = '',
1919
metrics = [],
20+
showLegend = true,
2021
className = '',
2122
}) {
2223
const { t } = useApp();
@@ -74,18 +75,20 @@ export function PieChartCard({
7475
</div>
7576
</div>
7677

77-
<div className="pie-chart-card__legend">
78-
{normalized.map((segment) => (
79-
<div key={segment.key || segment.label || segment.name} className="pie-chart-card__legend-item">
80-
<span className="pie-chart-card__swatch" style={{ background: segment.color }} />
81-
<div className="pie-chart-card__legend-meta">
82-
<strong>{segment.label || segment.name || segment.code || t('chart.noTitle')}</strong>
83-
<small>{segment.detail || segment.subtitle || `${segment.value}`}</small>
78+
{showLegend ? (
79+
<div className="pie-chart-card__legend">
80+
{normalized.map((segment) => (
81+
<div key={segment.key || segment.label || segment.name} className="pie-chart-card__legend-item">
82+
<span className="pie-chart-card__swatch" style={{ background: segment.color }} />
83+
<div className="pie-chart-card__legend-meta">
84+
<strong>{segment.label || segment.name || segment.code || t('chart.noTitle')}</strong>
85+
<small>{segment.detail || segment.subtitle || `${segment.value}`}</small>
86+
</div>
87+
<span className="pie-chart-card__percent">{Number(segment.percent || 0).toFixed(1)}%</span>
8488
</div>
85-
<span className="pie-chart-card__percent">{Number(segment.percent || 0).toFixed(1)}%</span>
86-
</div>
87-
))}
88-
</div>
89+
))}
90+
</div>
91+
) : null}
8992
</div>
9093
)}
9194

src/i18n/messages.js

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)