Skip to content

Commit f69c056

Browse files
Merge pull request #60 from Mes-Open/react-poc
fix: missing OEE on dashboard
2 parents cf39c56 + 35e860e commit f69c056

47 files changed

Lines changed: 839 additions & 225 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/lang/en.json

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,4 +3030,78 @@
30303030
"This is a draft — editable in place and not yet used for inspections.": "This is a draft — editable in place and not yet used for inspections.",
30313031
"This version is published and immutable. Saving creates a new draft version — the published version stays unchanged so past inspections remain reproducible.": "This version is published and immutable. Saving creates a new draft version — the published version stays unchanged so past inspections remain reproducible.",
30323032
"Version history": "Version history"
3033+
"CLOSED": "CLOSED",
3034+
"+ New Account": "+ New Account",
3035+
"+ New Area": "+ New Area",
3036+
"+ New Class": "+ New Class",
3037+
"+ New Company": "+ New Company",
3038+
"+ New Cost Source": "+ New Cost Source",
3039+
"+ New Crew": "+ New Crew",
3040+
"+ New Division": "+ New Division",
3041+
"+ New Event": "+ New Event",
3042+
"+ New Factory": "+ New Factory",
3043+
"+ New Integration": "+ New Integration",
3044+
"+ New Issue Type": "+ New Issue Type",
3045+
"+ New Line": "+ New Line",
3046+
"+ New Lot": "+ New Lot",
3047+
"+ New Material": "+ New Material",
3048+
"+ New Plan": "+ New Plan",
3049+
"+ New Reason": "+ New Reason",
3050+
"+ New Schedule": "+ New Schedule",
3051+
"+ New Segment": "+ New Segment",
3052+
"+ New Sequence": "+ New Sequence",
3053+
"+ New Site": "+ New Site",
3054+
"+ New Skill": "+ New Skill",
3055+
"+ New Subassembly": "+ New Subassembly",
3056+
"+ New Tool": "+ New Tool",
3057+
"+ New Wage Group": "+ New Wage Group",
3058+
"+ New Work Order": "+ New Work Order",
3059+
"+ New Worker": "+ New Worker",
3060+
"No LOT sequences yet.": "No LOT sequences yet.",
3061+
"No accounts yet.": "No accounts yet.",
3062+
"No anomaly reasons yet.": "No anomaly reasons yet.",
3063+
"No areas yet.": "No areas yet.",
3064+
"No companies yet.": "No companies yet.",
3065+
"No cost sources yet.": "No cost sources yet.",
3066+
"No crews yet.": "No crews yet.",
3067+
"No divisions yet.": "No divisions yet.",
3068+
"No factories yet.": "No factories yet.",
3069+
"No label templates yet.": "No label templates yet.",
3070+
"No materials yet.": "No materials yet.",
3071+
"No production lines yet.": "No production lines yet.",
3072+
"No shifts yet.": "No shifts yet.",
3073+
"No sites yet.": "No sites yet.",
3074+
"No skills yet.": "No skills yet.",
3075+
"No subassemblies yet.": "No subassemblies yet.",
3076+
"No tools yet.": "No tools yet.",
3077+
"No view templates yet.": "No view templates yet.",
3078+
"No wage groups yet.": "No wage groups yet.",
3079+
"No work orders yet.": "No work orders yet.",
3080+
"No workers yet.": "No workers yet.",
3081+
"No workstation types yet.": "No workstation types yet.",
3082+
"Area": "Area",
3083+
"Assigned": "Assigned",
3084+
"Avail / Recv": "Avail / Recv",
3085+
"Base Rate": "Base Rate",
3086+
"Cannot delete — has template steps": "Cannot delete — has template steps",
3087+
"Class": "Class",
3088+
"Configure": "Configure",
3089+
"End": "End",
3090+
"Every": "Every",
3091+
"In BOMs": "In BOMs",
3092+
"Last Login": "Last Login",
3093+
"Lines using": "Lines using",
3094+
"Lot Number": "Lot Number",
3095+
"Make default": "Make default",
3096+
"Next Due": "Next Due",
3097+
"Pad": "Pad",
3098+
"Prio": "Prio",
3099+
"Produced / Planned": "Produced / Planned",
3100+
"Req. Skills": "Req. Skills",
3101+
"Role / Station": "Role / Station",
3102+
"Stations": "Stations",
3103+
"UoM": "UoM",
3104+
"Used": "Used",
3105+
"Users & Accounts": "Users & Accounts",
3106+
"Logging in...": "Logging in..."
30333107
}

backend/lang/pl.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,4 +3030,77 @@
30303030
"This is a draft — editable in place and not yet used for inspections.": "To szkic — edytowalny w miejscu i jeszcze nieużywany do inspekcji.",
30313031
"This version is published and immutable. Saving creates a new draft version — the published version stays unchanged so past inspections remain reproducible.": "Ta wersja jest opublikowana i niezmienna. Zapis utworzy nową wersję-szkic — opublikowana wersja pozostaje bez zmian, aby przeszłe inspekcje były odtwarzalne.",
30323032
"Version history": "Historia wersji"
3033+
"CLOSED": "ZAMKNIĘTE",
3034+
"+ New Account": "+ Nowe konto",
3035+
"+ New Area": "+ Nowy obszar",
3036+
"+ New Class": "+ Nowa klasa",
3037+
"+ New Company": "+ Nowa firma",
3038+
"+ New Cost Source": "+ Nowe źródło kosztów",
3039+
"+ New Crew": "+ Nowa brygada",
3040+
"+ New Division": "+ Nowy wydział",
3041+
"+ New Event": "+ Nowe zdarzenie",
3042+
"+ New Factory": "+ Nowy zakład",
3043+
"+ New Integration": "+ Nowa integracja",
3044+
"+ New Issue Type": "+ Nowy typ problemu",
3045+
"+ New Line": "+ Nowa linia",
3046+
"+ New Lot": "+ Nowa partia",
3047+
"+ New Material": "+ Nowy materiał",
3048+
"+ New Plan": "+ Nowy plan",
3049+
"+ New Reason": "+ Nowa przyczyna",
3050+
"+ New Schedule": "+ Nowy harmonogram",
3051+
"+ New Segment": "+ Nowy segment",
3052+
"+ New Sequence": "+ Nowa sekwencja",
3053+
"+ New Site": "+ Nowy zakład",
3054+
"+ New Skill": "+ Nowa umiejętność",
3055+
"+ New Subassembly": "+ Nowy podzespół",
3056+
"+ New Tool": "+ Nowe narzędzie",
3057+
"+ New Wage Group": "+ Nowa grupa płacowa",
3058+
"+ New Work Order": "+ Nowe zlecenie",
3059+
"+ New Worker": "+ Nowy pracownik",
3060+
"No LOT sequences yet.": "Brak sekwencji LOT.",
3061+
"No accounts yet.": "Brak kont.",
3062+
"No anomaly reasons yet.": "Brak przyczyn anomalii.",
3063+
"No areas yet.": "Brak obszarów.",
3064+
"No companies yet.": "Brak firm.",
3065+
"No cost sources yet.": "Brak źródeł kosztów.",
3066+
"No crews yet.": "Brak brygad.",
3067+
"No divisions yet.": "Brak wydziałów.",
3068+
"No factories yet.": "Brak zakładów.",
3069+
"No label templates yet.": "Brak szablonów etykiet.",
3070+
"No materials yet.": "Brak materiałów.",
3071+
"No production lines yet.": "Brak linii produkcyjnych.",
3072+
"No shifts yet.": "Brak zmian.",
3073+
"No sites yet.": "Brak zakładów.",
3074+
"No skills yet.": "Brak umiejętności.",
3075+
"No subassemblies yet.": "Brak podzespołów.",
3076+
"No tools yet.": "Brak narzędzi.",
3077+
"No view templates yet.": "Brak szablonów widoków.",
3078+
"No wage groups yet.": "Brak grup płacowych.",
3079+
"No work orders yet.": "Brak zleceń.",
3080+
"No workers yet.": "Brak pracowników.",
3081+
"No workstation types yet.": "Brak typów stanowisk.",
3082+
"Area": "Obszar",
3083+
"Assigned": "Przypisane",
3084+
"Avail / Recv": "Dost. / Przyj.",
3085+
"Base Rate": "Stawka podstawowa",
3086+
"Cannot delete — has template steps": "Nie można usunąć — ma kroki szablonu",
3087+
"Class": "Klasa",
3088+
"Configure": "Konfiguruj",
3089+
"End": "Koniec",
3090+
"Every": "Co",
3091+
"In BOMs": "W BOM",
3092+
"Last Login": "Ostatnie logowanie",
3093+
"Lines using": "Linie używające",
3094+
"Lot Number": "Numer partii",
3095+
"Make default": "Ustaw domyślny",
3096+
"Next Due": "Następny termin",
3097+
"Pad": "Dopełnienie",
3098+
"Prio": "Prio",
3099+
"Produced / Planned": "Wyprodukowano / Plan",
3100+
"Req. Skills": "Wym. umiejętności",
3101+
"Role / Station": "Rola / Stanowisko",
3102+
"Stations": "Stanowiska",
3103+
"UoM": "JM",
3104+
"Used": "Użyte",
3105+
"Users & Accounts": "Użytkownicy i konta"
30333106
}

backend/resources/css/app.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,27 @@
7373
}
7474
}
7575

76+
/* ── Sidebar nav scrollbar ──────────────────────────────────────────────── */
77+
/* Slim, theme-matched scrollbar for the dark slate sidebar — overlays the */
78+
/* track so it doesn't reserve a chunky gutter, and only shows the thumb. */
79+
.sidebar-scroll {
80+
scrollbar-width: thin; /* Firefox */
81+
scrollbar-color: rgb(71 85 105) transparent; /* thumb / track (slate-600) */
82+
}
83+
.sidebar-scroll::-webkit-scrollbar {
84+
width: 6px;
85+
}
86+
.sidebar-scroll::-webkit-scrollbar-track {
87+
background: transparent;
88+
}
89+
.sidebar-scroll::-webkit-scrollbar-thumb {
90+
background-color: rgb(71 85 105); /* slate-600 */
91+
border-radius: 9999px;
92+
}
93+
.sidebar-scroll:hover::-webkit-scrollbar-thumb {
94+
background-color: rgb(100 116 139); /* slate-500 on hover */
95+
}
96+
7697
/* ── Global dark mode overrides for inline Tailwind classes ─────────────── */
7798
/* Cover hardcoded classes in blade templates without modifying each file */
7899

backend/resources/js/Pages/admin/Dashboard.jsx

Lines changed: 114 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -261,62 +261,128 @@ function KpiCard({ href, label, value, accent, hint }) {
261261
}
262262

263263
function OeeOverview({ records, lines }) {
264-
const lineById = useMemo(() => new Map(lines.map((l) => [String(l.id), l])), [lines]);
264+
// Today's OEE per line. Mirrors the old Blade widget: one gauge card per
265+
// line, N/A where there's no record for today.
265266
const todayStr = new Date().toISOString().slice(0, 10);
266-
const today = records.filter((r) => r.record_date === todayStr);
267-
const yesterday = records.filter((r) => r.record_date !== todayStr);
268-
const byLineToday = new Map(today.map((r) => [String(r.line_id), r]));
269-
const byLineYesterday = new Map(yesterday.map((r) => [String(r.line_id), r]));
270-
const lineIds = Array.from(
271-
new Set([...byLineToday.keys(), ...byLineYesterday.keys()]),
272-
).sort();
267+
const byLineToday = useMemo(
268+
() => new Map(records.filter((r) => r.record_date === todayStr).map((r) => [String(r.line_id), r])),
269+
[records, todayStr],
270+
);
271+
const sortedLines = useMemo(
272+
() => [...lines].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''))),
273+
[lines],
274+
);
273275

274276
return (
275277
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-5">
276-
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100 mb-3">{__('OEE Overview')}</h2>
277-
<table className="w-full text-sm">
278-
<thead>
279-
<tr className="text-left text-gray-500 dark:text-gray-400 border-b">
280-
<th className="py-2">{__('Line')}</th>
281-
<th className="py-2">{__('Today OEE')}</th>
282-
<th className="py-2">{__('Today A × P × Q')}</th>
283-
<th className="py-2">{__('Yesterday OEE')}</th>
284-
</tr>
285-
</thead>
286-
<tbody>
287-
{lineIds.map((id) => {
288-
const t = byLineToday.get(id);
289-
const y = byLineYesterday.get(id);
290-
const line = lineById.get(id);
291-
return (
292-
<tr key={id} className="border-b last:border-0">
293-
<td className="py-2 font-medium">
294-
{line?.name ?? __('Line :id', { id })}
295-
</td>
296-
<td className="py-2">
297-
<OeeBadge value={t?.oee_pct} />
298-
</td>
299-
<td className="py-2 text-gray-600 dark:text-gray-300">
300-
{t ? `${pct(t.availability_pct)} · ${pct(t.performance_pct)} · ${pct(t.quality_pct)}` : '—'}
301-
</td>
302-
<td className="py-2">
303-
<OeeBadge value={y?.oee_pct} muted />
304-
</td>
305-
</tr>
306-
);
307-
})}
308-
</tbody>
309-
</table>
278+
<div className="flex items-center justify-between mb-3">
279+
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100">{__('OEE Overview')}</h2>
280+
<a href="/admin/oee" className="text-sm text-blue-600 dark:text-blue-300 hover:underline">
281+
{__('Full report')}
282+
</a>
283+
</div>
284+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
285+
{sortedLines.map((line) => {
286+
const r = byLineToday.get(String(line.id));
287+
const value = r?.oee_pct != null ? Number(r.oee_pct) : null;
288+
return (
289+
<div
290+
key={line.id}
291+
className={`p-3 rounded-lg border flex flex-col items-center ${oeeCardClass(value)}`}
292+
>
293+
<p className="text-sm font-medium text-gray-700 dark:text-gray-200 text-center mb-2 truncate w-full">
294+
{line.name ?? __('Line :id', { id: line.id })}
295+
</p>
296+
<OeeGauge value={value} />
297+
{r ? (
298+
<div className="flex gap-2 text-xs text-gray-500 dark:text-gray-400 mt-2">
299+
<span>A: {pct(r.availability_pct)}</span>
300+
<span>P: {r.performance_pct != null ? pct(r.performance_pct) : '-'}</span>
301+
<span>Q: {pct(r.quality_pct)}</span>
302+
</div>
303+
) : (
304+
<div className="text-xs text-gray-400 dark:text-gray-500 mt-2">&nbsp;</div>
305+
)}
306+
</div>
307+
);
308+
})}
309+
</div>
310310
</div>
311311
);
312312
}
313313

314-
function OeeBadge({ value, muted }) {
315-
if (value == null) return <span className="text-gray-400 dark:text-gray-500"></span>;
316-
const n = Number(value);
317-
const color =
318-
n >= 85 ? 'text-green-600 dark:text-green-300' : n >= 60 ? 'text-yellow-600' : 'text-red-600 dark:text-red-300';
319-
return <span className={`font-bold ${muted ? 'text-gray-500 dark:text-gray-400' : color}`}>{n.toFixed(1)}%</span>;
314+
// OEE banding — mirrors backend/app/Support/OeeBand.php (red < 65 ≤ yellow < 85 ≤ green).
315+
const OEE_RED_BELOW = 65;
316+
const OEE_GREEN_AT_LEAST = 85;
317+
318+
function oeeColor(value) {
319+
if (value == null) return 'gray';
320+
if (value >= OEE_GREEN_AT_LEAST) return 'green';
321+
if (value >= OEE_RED_BELOW) return 'yellow';
322+
return 'red';
323+
}
324+
325+
function oeeCardClass(value) {
326+
return {
327+
green: 'border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-800',
328+
yellow: 'border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20 dark:border-yellow-800',
329+
red: 'border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800',
330+
gray: 'border-gray-200 bg-gray-50 dark:bg-slate-700 dark:border-slate-600',
331+
}[oeeColor(value)];
332+
}
333+
334+
function oeeTextClass(value) {
335+
return {
336+
green: 'text-green-600 dark:text-green-400',
337+
yellow: 'text-yellow-600 dark:text-yellow-400',
338+
red: 'text-red-600 dark:text-red-400',
339+
gray: 'text-gray-500 dark:text-gray-400',
340+
}[oeeColor(value)];
341+
}
342+
343+
/**
344+
* Semicircle OEE gauge with red/yellow/green zones and a needle — React port of
345+
* the `<x-oee-gauge>` Blade component. Points lie on a unit semicircle centered
346+
* at (50,50), r=40: p=0 → (10,50), p=50 → (50,10), p=100 → (90,50).
347+
*/
348+
function OeeGauge({ value, size = 104 }) {
349+
const hasValue = value != null;
350+
const p = hasValue ? Math.max(0, Math.min(100, Number(value))) : 0;
351+
const pointAt = (q, r = 40) => {
352+
const a = (q / 100) * Math.PI;
353+
return [50 - r * Math.cos(a), 50 - r * Math.sin(a)];
354+
};
355+
const [rEndX, rEndY] = pointAt(OEE_RED_BELOW);
356+
const [yEndX, yEndY] = pointAt(OEE_GREEN_AT_LEAST);
357+
const [gEndX, gEndY] = pointAt(100);
358+
const [needleX, needleY] = pointAt(p, 35);
359+
360+
return (
361+
<div className="inline-flex flex-col items-center" style={{ width: size }}>
362+
<svg viewBox="0 0 100 60" className="w-full h-auto" aria-hidden="true">
363+
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="currentColor" strokeWidth="10" className="text-gray-200 dark:text-slate-600" />
364+
<path d={`M 10 50 A 40 40 0 0 1 ${rEndX} ${rEndY}`} fill="none" stroke="#ef4444" strokeWidth="10" />
365+
<path d={`M ${rEndX} ${rEndY} A 40 40 0 0 1 ${yEndX} ${yEndY}`} fill="none" stroke="#eab308" strokeWidth="10" />
366+
<path d={`M ${yEndX} ${yEndY} A 40 40 0 0 1 ${gEndX} ${gEndY}`} fill="none" stroke="#22c55e" strokeWidth="10" />
367+
{hasValue ? (
368+
<>
369+
<line x1="50" y1="50" x2={needleX} y2={needleY} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" className="text-gray-800 dark:text-gray-100" />
370+
<circle cx="50" cy="50" r="2.2" fill="currentColor" className="text-gray-800 dark:text-gray-100" />
371+
</>
372+
) : (
373+
<circle cx="50" cy="50" r="2.2" fill="currentColor" className="text-gray-400" />
374+
)}
375+
</svg>
376+
<div className="-mt-2 text-center leading-tight">
377+
<div className={`font-bold ${oeeTextClass(hasValue ? p : null)}`} style={{ fontSize: size * 0.18 }}>
378+
{hasValue ? `${p.toFixed(1)}%` : 'N/A'}
379+
</div>
380+
<div className="text-gray-500 dark:text-gray-400 uppercase tracking-wide" style={{ fontSize: size * 0.085 }}>
381+
OEE
382+
</div>
383+
</div>
384+
</div>
385+
);
320386
}
321387

322388
function InboundQcOverview({ stats }) {

backend/resources/js/Pages/admin/anomaly-reasons/Index.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ export default function AnomalyReasonsIndex() {
1414
];
1515

1616
const actions = (r) => [
17-
{ label: 'Edit', href: `/admin/anomaly-reasons/${r.id}/edit` },
17+
{ label: 'Edit', icon: 'edit', href: `/admin/anomaly-reasons/${r.id}/edit` },
1818
{
1919
label: r.is_active ? 'Deactivate' : 'Activate',
20+
icon: r.is_active ? 'deactivate' : 'activate',
2021
onClick: () => router.post(`/admin/anomaly-reasons/${r.id}/toggle-active`, {}, { preserveScroll: true }),
2122
},
2223
{
2324
label: 'Delete',
24-
className: 'text-red-600 hover:underline',
25+
icon: 'delete',
26+
variant: 'danger',
2527
onClick: () => {
2628
if (confirm(`Delete anomaly reason "${r.name}"?`)) {
2729
router.delete(`/admin/anomaly-reasons/${r.id}`, { preserveScroll: true });

backend/resources/js/Pages/admin/areas/Index.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ export default function AreasIndex() {
1414
];
1515

1616
const actions = (r) => [
17-
{ label: 'Edit', href: `/admin/areas/${r.id}/edit` },
17+
{ label: 'Edit', icon: 'edit', href: `/admin/areas/${r.id}/edit` },
1818
{
1919
label: r.is_active ? 'Deactivate' : 'Activate',
20+
icon: r.is_active ? 'deactivate' : 'activate',
2021
onClick: () => router.post(`/admin/areas/${r.id}/toggle-active`, {}, { preserveScroll: true }),
2122
},
2223
{
2324
label: 'Delete',
24-
className: 'text-red-600 hover:underline',
25+
icon: 'delete',
26+
variant: 'danger',
2527
onClick: () => {
2628
if (confirm(`Delete area "${r.name}"?`)) {
2729
router.delete(`/admin/areas/${r.id}`, { preserveScroll: true });

0 commit comments

Comments
 (0)