Skip to content

Commit 341a007

Browse files
author
OpenCode
committed
Show KPI totals by UMR
1 parent a1bb79e commit 341a007

3 files changed

Lines changed: 197 additions & 39 deletions

File tree

src/components/DashboardCards.jsx

Lines changed: 187 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,137 @@ function formatHours(value, t) {
88
return `${Number(value || 0).toFixed(2)} ${t('common.hoursLabel').toLowerCase()}`;
99
}
1010

11+
function calculateStopMinutes(item) {
12+
return Number(item.maintenanceMinutes || 0) + Number(item.idleMinutes || 0) + Number(item.otherMinutes || 0);
13+
}
14+
15+
function calculateAvailabilityPercent(availableMinutes, maintenanceMinutes) {
16+
const totalMinutes = Number(availableMinutes || 0);
17+
18+
if (!totalMinutes) {
19+
return 0;
20+
}
21+
22+
return Number((((totalMinutes - Number(maintenanceMinutes || 0)) / totalMinutes) * 100).toFixed(1));
23+
}
24+
25+
function calculateClosedCount(item) {
26+
return Math.max(0, Number(item.count || 0) - Number(item.openCount || 0));
27+
}
28+
29+
function EquipmentKpiTable({ summary, t }) {
30+
const headers = {
31+
equipment: t('registrations.equipment.code'),
32+
plate: t('registrations.equipment.plate'),
33+
open: t('dashboard.cards.open'),
34+
totalHours: t('dashboard.cards.totalHours'),
35+
stop: t('dashboard.cards.stop'),
36+
maintenance: t('dashboard.cards.maintenance'),
37+
closed: t('dashboard.cards.closed'),
38+
availability: t('dashboard.cards.physicalAvailability'),
39+
utilization: t('dashboard.cards.fleetIU'),
40+
};
41+
42+
return (
43+
<section className="card table-card dashboard-block">
44+
<div className="card__head">
45+
<div>
46+
<p className="eyebrow">{t('dashboard.sections.kpisByEquipment')}</p>
47+
<h2>{t('dashboard.sections.kpisByEquipment')}</h2>
48+
</div>
49+
</div>
50+
51+
<div className="table-wrap">
52+
<table className="records-table dashboard-kpi-table">
53+
<thead>
54+
<tr className="dashboard-kpi-table__total">
55+
<th scope="col">{headers.equipment}</th>
56+
<th scope="col">{headers.plate}</th>
57+
<th scope="col">{headers.open}</th>
58+
<th scope="col">{headers.totalHours}</th>
59+
<th scope="col">{headers.stop}</th>
60+
<th scope="col">{headers.maintenance}</th>
61+
<th scope="col">{headers.closed}</th>
62+
<th scope="col">{headers.availability}</th>
63+
<th scope="col">{headers.utilization}</th>
64+
</tr>
65+
</thead>
66+
67+
<tbody>
68+
{summary.equipmentMetrics.map((item) => {
69+
const stopMinutes = calculateStopMinutes(item);
70+
const availabilityPercent = calculateAvailabilityPercent(item.availableMinutes || summary.periodAvailableMinutes, item.maintenanceMinutes);
71+
72+
return (
73+
<tr key={item.equipmentId}>
74+
<td data-label={headers.equipment}>
75+
<strong>{item.code}</strong>
76+
<small>{item.description}</small>
77+
</td>
78+
<td data-label={headers.plate}>
79+
<strong>{item.plate}</strong>
80+
</td>
81+
<td data-label={headers.open}>
82+
<strong>{item.openCount}</strong>
83+
</td>
84+
<td data-label={headers.totalHours}>
85+
<strong>{formatHours((item.totalMinutes || 0) / 60, t)}</strong>
86+
</td>
87+
<td data-label={headers.stop}>
88+
<strong>{formatHours(stopMinutes / 60, t)}</strong>
89+
</td>
90+
<td data-label={headers.maintenance}>
91+
<strong>{formatHours((item.maintenanceMinutes || 0) / 60, t)}</strong>
92+
</td>
93+
<td data-label={headers.closed}>
94+
<strong>{calculateClosedCount(item)}</strong>
95+
</td>
96+
<td data-label={headers.availability}>
97+
<strong>{availabilityPercent.toFixed(1)}%</strong>
98+
</td>
99+
<td data-label={headers.utilization}>
100+
<strong>{formatUtilization(item.utilizationPercent)}</strong>
101+
</td>
102+
</tr>
103+
);
104+
})}
105+
106+
<tr>
107+
<td data-label={headers.equipment}>
108+
<strong>{t('dashboard.labels.total')}</strong>
109+
</td>
110+
<td data-label={headers.plate}>
111+
<strong>-</strong>
112+
</td>
113+
<td data-label={headers.open}>
114+
<strong>{summary.openCount}</strong>
115+
</td>
116+
<td data-label={headers.totalHours}>
117+
<strong>{formatHours(summary.totalHours, t)}</strong>
118+
</td>
119+
<td data-label={headers.stop}>
120+
<strong>{formatHours(summary.stopHours, t)}</strong>
121+
</td>
122+
<td data-label={headers.maintenance}>
123+
<strong>{formatHours(summary.maintenanceHours, t)}</strong>
124+
</td>
125+
<td data-label={headers.closed}>
126+
<strong>{summary.closedCount}</strong>
127+
</td>
128+
<td data-label={headers.availability}>
129+
<strong>{formatUtilization(summary.physicalAvailabilityPercent)}</strong>
130+
</td>
131+
<td data-label={headers.utilization}>
132+
<strong>{formatUtilization(summary.physicalUtilizationPercent)}</strong>
133+
</td>
134+
</tr>
135+
</tbody>
136+
</table>
137+
</div>
138+
</section>
139+
);
140+
}
141+
11142
function BarList({ title, items, emptyMessage, locale, t }) {
12143
const max = Math.max(...items.map((item) => item.minutes || 0), 0);
13144

@@ -159,6 +290,62 @@ export function DashboardCards({ summary }) {
159290

160291
return (
161292
<div className="dashboard-layout">
293+
<section className="card dashboard-block">
294+
<div className="card__head">
295+
<div>
296+
<p className="eyebrow">{t('dashboard.sections.kpisOverview')}</p>
297+
<h2>{t('dashboard.sections.kpisOverview')}</h2>
298+
</div>
299+
<StatusChip tone="info">{t('dashboard.labels.total')}</StatusChip>
300+
</div>
301+
302+
<div className="stats-grid">
303+
<article className="card stat-card stat-card--success">
304+
<p>{t('dashboard.cards.open')}</p>
305+
<strong>{summary.activeEquipmentCount}</strong>
306+
<small>{t('dashboard.cards.openSub')}</small>
307+
</article>
308+
309+
<article className="card stat-card">
310+
<p>{t('dashboard.cards.totalHours')}</p>
311+
<strong>{summary.totalHours.toFixed(2)} h</strong>
312+
<small>{t('dashboard.cards.totalHoursSub', { count: summary.totalRecords })}</small>
313+
</article>
314+
315+
<article className="card stat-card stat-card--danger">
316+
<p>{t('dashboard.cards.stop')}</p>
317+
<strong>{summary.stopHours.toFixed(2)} h</strong>
318+
<small>{t('dashboard.cards.stopSub')}</small>
319+
</article>
320+
321+
<article className="card stat-card stat-card--warning">
322+
<p>{t('dashboard.cards.maintenance')}</p>
323+
<strong>{summary.maintenanceHours.toFixed(2)} h</strong>
324+
<small>{t('dashboard.cards.maintenanceSub')}</small>
325+
</article>
326+
327+
<article className="card stat-card">
328+
<p>{t('dashboard.cards.closed')}</p>
329+
<strong>{summary.closedCount}</strong>
330+
<small>{t('dashboard.cards.closedSub')}</small>
331+
</article>
332+
333+
<article className="card stat-card">
334+
<p>{t('dashboard.cards.physicalAvailability')}</p>
335+
<strong>{formatUtilization(summary.physicalAvailabilityPercent)}</strong>
336+
<small>{t('dashboard.cards.physicalAvailabilitySub')}</small>
337+
</article>
338+
339+
<article className="card stat-card stat-card--info">
340+
<p>{t('dashboard.cards.fleetIU')}</p>
341+
<strong>{utilizationAverage.toFixed(1)}%</strong>
342+
<small>{t('dashboard.cards.fleetIUSub')}</small>
343+
</article>
344+
</div>
345+
</section>
346+
347+
<EquipmentKpiTable summary={summary} t={t} />
348+
162349
<section className="card dashboard-block">
163350
<div className="card__head">
164351
<div>
@@ -187,44 +374,6 @@ export function DashboardCards({ summary }) {
187374
</div>
188375
</section>
189376

190-
<section className="stats-grid">
191-
<article className="card stat-card stat-card--success">
192-
<p>{t('dashboard.cards.open')}</p>
193-
<strong>{summary.activeEquipmentCount}</strong>
194-
<small>{t('dashboard.cards.openSub')}</small>
195-
</article>
196-
<article className="card stat-card">
197-
<p>{t('dashboard.cards.totalHours')}</p>
198-
<strong>{summary.totalHours.toFixed(2)} h</strong>
199-
<small>{t('dashboard.cards.totalHoursSub', { count: summary.totalRecords })}</small>
200-
</article>
201-
<article className="card stat-card stat-card--danger">
202-
<p>{t('dashboard.cards.stop')}</p>
203-
<strong>{summary.stopHours.toFixed(2)} h</strong>
204-
<small>{t('dashboard.cards.stopSub')}</small>
205-
</article>
206-
<article className="card stat-card stat-card--warning">
207-
<p>{t('dashboard.cards.maintenance')}</p>
208-
<strong>{summary.maintenanceHours.toFixed(2)} h</strong>
209-
<small>{t('dashboard.cards.maintenanceSub')}</small>
210-
</article>
211-
<article className="card stat-card">
212-
<p>{t('dashboard.cards.closed')}</p>
213-
<strong>{summary.closedCount}</strong>
214-
<small>{t('dashboard.cards.closedSub')}</small>
215-
</article>
216-
<article className="card stat-card">
217-
<p>{t('dashboard.cards.physicalAvailability')}</p>
218-
<strong>{formatUtilization(summary.physicalAvailabilityPercent)}</strong>
219-
<small>{t('dashboard.cards.physicalAvailabilitySub')}</small>
220-
</article>
221-
<article className="card stat-card stat-card--info">
222-
<p>{t('dashboard.cards.fleetIU')}</p>
223-
<strong>{utilizationAverage.toFixed(1)}%</strong>
224-
<small>{t('dashboard.cards.fleetIUSub')}</small>
225-
</article>
226-
</section>
227-
228377
<section className="card dashboard-block">
229378
<div className="card__head">
230379
<div>

src/i18n/messages.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/styles.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3916,3 +3916,11 @@ label > span {
39163916
.app-header.card .subtitle {
39173917
color: rgba(255, 255, 255, 0.68);
39183918
}
3919+
3920+
.dashboard-kpi-table__total td {
3921+
background: rgba(237, 0, 22, 0.04);
3922+
}
3923+
3924+
.dashboard-kpi-table__total td strong {
3925+
color: var(--text);
3926+
}

0 commit comments

Comments
 (0)