changes
This commit is contained in:
55
components/recap/RecapDowntimeTop.tsx
Normal file
55
components/recap/RecapDowntimeTop.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, BarChart } from "recharts";
|
||||
import { useI18n } from "@/lib/i18n/useI18n";
|
||||
|
||||
type Row = {
|
||||
reasonLabel: string;
|
||||
minutes: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
rows: Row[];
|
||||
};
|
||||
|
||||
export default function RecapDowntimeTop({ rows }: Props) {
|
||||
const { t } = useI18n();
|
||||
const data = rows.slice(0, 3).map((row) => ({ ...row, label: row.reasonLabel.slice(0, 20) }));
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className="mb-3 text-sm font-semibold text-white">{t("recap.downtime.title")}</div>
|
||||
{data.length === 0 ? (
|
||||
<div className="text-sm text-zinc-400">{t("recap.empty.production")}</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-[170px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.08)" />
|
||||
<XAxis dataKey="label" tick={{ fill: "#a1a1aa", fontSize: 11 }} />
|
||||
<YAxis tick={{ fill: "#a1a1aa", fontSize: 11 }} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: "rgba(0,0,0,0.85)", border: "1px solid rgba(255,255,255,0.12)" }}
|
||||
labelStyle={{ color: "#e4e4e7" }}
|
||||
/>
|
||||
<Bar dataKey="minutes" fill="#34d399" radius={[6, 6, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{data.map((row) => (
|
||||
<div key={row.reasonLabel} className="flex items-center justify-between text-xs text-zinc-300">
|
||||
<span className="truncate">{row.reasonLabel}</span>
|
||||
<span>
|
||||
{row.minutes.toFixed(1)} min · {row.count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
components/recap/RecapKpiRow.tsx
Normal file
37
components/recap/RecapKpiRow.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/lib/i18n/useI18n";
|
||||
|
||||
type Props = {
|
||||
oeeAvg: number | null;
|
||||
goodParts: number;
|
||||
totalStops: number;
|
||||
scrapParts: number;
|
||||
};
|
||||
|
||||
function fmtPct(v: number | null) {
|
||||
if (v == null || Number.isNaN(v)) return "--";
|
||||
return `${v.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
export default function RecapKpiRow({ oeeAvg, goodParts, totalStops, scrapParts }: Props) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const items = [
|
||||
{ label: t("recap.kpi.oee"), value: fmtPct(oeeAvg), valueClass: "text-emerald-400" },
|
||||
{ label: t("recap.kpi.good"), value: String(goodParts), valueClass: "text-white" },
|
||||
{ label: t("recap.kpi.stops"), value: String(totalStops), valueClass: totalStops > 0 ? "text-amber-400" : "text-white" },
|
||||
{ label: t("recap.kpi.scrap"), value: String(scrapParts), valueClass: scrapParts > 0 ? "text-red-400" : "text-white" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className={`text-2xl font-semibold ${item.valueClass}`}>{item.value}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-wide text-zinc-400">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
components/recap/RecapMachineStatus.tsx
Normal file
46
components/recap/RecapMachineStatus.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/lib/i18n/useI18n";
|
||||
import type { RecapMachine } from "@/lib/recap/types";
|
||||
|
||||
type Props = {
|
||||
machine: RecapMachine | null;
|
||||
};
|
||||
|
||||
export default function RecapMachineStatus({ machine }: Props) {
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
if (!machine) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className="text-sm text-zinc-400">{t("recap.empty.production")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isStopped = (machine.downtime.ongoingStopMin ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className="mb-3 text-sm font-semibold text-white">{t("recap.machine.title")}</div>
|
||||
<ul className="space-y-2 text-sm text-zinc-200">
|
||||
<li>
|
||||
<span className={isStopped ? "text-red-400" : "text-emerald-400"}>
|
||||
{isStopped ? t("recap.machine.stopped") : t("recap.machine.running")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={machine.workOrders.moldChangeInProgress ? "text-amber-400" : "text-zinc-300"}>
|
||||
{t("recap.machine.mold")}: {machine.workOrders.moldChangeInProgress ? t("common.yes") : t("common.no")}
|
||||
</span>
|
||||
</li>
|
||||
<li className="text-zinc-400">
|
||||
{t("recap.machine.lastHeartbeat")}: {machine.heartbeat.lastSeenAt ? new Date(machine.heartbeat.lastSeenAt).toLocaleString(locale) : "--"}
|
||||
</li>
|
||||
<li className="text-zinc-400">
|
||||
{t("recap.machine.uptime")}: {machine.heartbeat.uptimePct == null ? "--" : `${machine.heartbeat.uptimePct.toFixed(1)}%`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
components/recap/RecapProductionBySku.tsx
Normal file
45
components/recap/RecapProductionBySku.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/lib/i18n/useI18n";
|
||||
import type { RecapSkuRow } from "@/lib/recap/types";
|
||||
|
||||
type Props = {
|
||||
rows: RecapSkuRow[];
|
||||
};
|
||||
|
||||
export default function RecapProductionBySku({ rows }: Props) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className="mb-3 text-sm font-semibold text-white">{t("recap.production.title")}</div>
|
||||
{rows.length === 0 ? (
|
||||
<div className="text-sm text-zinc-400">{t("recap.empty.production")}</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-5 gap-2 border-b border-white/10 pb-2 text-xs uppercase tracking-wide text-zinc-400">
|
||||
<div>SKU</div>
|
||||
<div>{t("recap.production.good")}</div>
|
||||
<div>{t("recap.production.scrap")}</div>
|
||||
<div>{t("recap.production.target")}</div>
|
||||
<div>{t("recap.production.progress")}</div>
|
||||
</div>
|
||||
{rows.slice(0, 8).map((row) => {
|
||||
const pct = row.progressPct == null ? "--" : `${Math.round(row.progressPct)}%`;
|
||||
return (
|
||||
<div key={row.sku} className="grid grid-cols-5 gap-2 text-sm text-zinc-200">
|
||||
<div className="truncate">{row.sku}</div>
|
||||
<div>{row.good}</div>
|
||||
<div className={row.scrap > 0 ? "text-red-400" : "text-zinc-200"}>{row.scrap}</div>
|
||||
<div>{row.target ?? "--"}</div>
|
||||
<div>
|
||||
<span className="text-emerald-400">{pct}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
components/recap/RecapWorkOrderStatus.tsx
Normal file
57
components/recap/RecapWorkOrderStatus.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/lib/i18n/useI18n";
|
||||
import type { RecapMachine } from "@/lib/recap/types";
|
||||
|
||||
type Props = {
|
||||
workOrders: RecapMachine["workOrders"];
|
||||
};
|
||||
|
||||
export default function RecapWorkOrderStatus({ workOrders }: Props) {
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
|
||||
<div className="mb-3 text-sm font-semibold text-white">{t("recap.workOrders.title")}</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="text-xs uppercase tracking-wide text-zinc-400">{t("recap.workOrders.active")}</div>
|
||||
{!workOrders.active ? (
|
||||
<div className="mt-1 text-sm text-zinc-400">{t("recap.workOrders.none")}</div>
|
||||
) : (
|
||||
<div className="mt-2 rounded-xl border border-white/10 bg-black/30 p-3 text-sm text-zinc-200">
|
||||
<div className="font-medium text-white">{workOrders.active.id}</div>
|
||||
<div className="text-zinc-400">SKU: {workOrders.active.sku || "--"}</div>
|
||||
<div className="mt-2 h-2 rounded-full bg-white/10">
|
||||
<div
|
||||
className="h-2 rounded-full bg-emerald-400"
|
||||
style={{ width: `${Math.max(0, Math.min(100, workOrders.active.progressPct ?? 0))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-zinc-400">
|
||||
{t("recap.workOrders.startedAt")}: {workOrders.active.startedAt ? new Date(workOrders.active.startedAt).toLocaleString(locale) : "--"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-zinc-400">{t("recap.workOrders.completed")}</div>
|
||||
{workOrders.completed.length === 0 ? (
|
||||
<div className="mt-1 text-sm text-zinc-400">{t("recap.workOrders.none")}</div>
|
||||
) : (
|
||||
<div className="mt-2 space-y-2">
|
||||
{workOrders.completed.slice(0, 4).map((row) => (
|
||||
<div key={row.id} className="rounded-xl border border-white/10 bg-black/30 p-3 text-xs text-zinc-300">
|
||||
<div className="font-medium text-white">{row.id}</div>
|
||||
<div>SKU: {row.sku || "--"}</div>
|
||||
<div>{t("recap.workOrders.goodParts")}: {row.goodParts}</div>
|
||||
<div>{t("recap.workOrders.duration")}: {row.durationHrs.toFixed(2)}h</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user