Mobile friendly, lint correction, typescript error clear
This commit is contained in:
21
app/(app)/reports/loading.tsx
Normal file
21
app/(app)/reports/loading.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function ReportsLoading() {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 space-y-6">
|
||||
<div className="h-8 w-56 rounded-lg bg-white/5" />
|
||||
<div className="grid gap-4 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<div key={idx} className="h-24 rounded-2xl border border-white/10 bg-white/5" />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 lg:grid-cols-[2fr_1fr]">
|
||||
<div className="h-80 rounded-2xl border border-white/10 bg-white/5" />
|
||||
<div className="h-80 rounded-2xl border border-white/10 bg-white/5" />
|
||||
</div>
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, idx) => (
|
||||
<div key={idx} className="h-24 rounded-2xl border border-white/10 bg-white/5" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,17 +68,19 @@ type ReportPayload = {
|
||||
type MachineOption = { id: string; name: string };
|
||||
type FilterOptions = { workOrders: string[]; skus: string[] };
|
||||
type Translator = (key: string, vars?: Record<string, string | number>) => string;
|
||||
type TooltipPayload<T> = { payload?: T; name?: string; value?: number | string };
|
||||
type SimpleTooltipProps<T> = {
|
||||
active?: boolean;
|
||||
payload?: Array<TooltipPayload<T>>;
|
||||
label?: string | number;
|
||||
};
|
||||
type CycleHistogramRow = ReportPayload["distribution"]["cycleTime"][number];
|
||||
|
||||
function fmtPct(v?: number | null) {
|
||||
if (v === null || v === undefined || Number.isNaN(v)) return "--";
|
||||
return `${v.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function fmtNum(v?: number | null) {
|
||||
if (v === null || v === undefined || Number.isNaN(v)) return "--";
|
||||
return `${Math.round(v)}`;
|
||||
}
|
||||
|
||||
function fmtDuration(sec?: number | null) {
|
||||
if (!sec) return "--";
|
||||
const h = Math.floor(sec / 3600);
|
||||
@@ -104,7 +106,7 @@ function formatTickLabel(ts: string, range: RangeKey) {
|
||||
return `${month}-${day}`;
|
||||
}
|
||||
|
||||
function CycleTooltip({ active, payload, t }: any) {
|
||||
function CycleTooltip({ active, payload, t }: SimpleTooltipProps<CycleHistogramRow> & { t: Translator }) {
|
||||
if (!active || !payload?.length) return null;
|
||||
const p = payload[0]?.payload;
|
||||
if (!p) return null;
|
||||
@@ -141,7 +143,7 @@ function CycleTooltip({ active, payload, t }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
function DowntimeTooltip({ active, payload, t }: any) {
|
||||
function DowntimeTooltip({ active, payload, t }: SimpleTooltipProps<{ name?: string; value?: number }> & { t: Translator }) {
|
||||
if (!active || !payload?.length) return null;
|
||||
const row = payload[0]?.payload ?? {};
|
||||
const label = row.name ?? payload[0]?.name ?? "";
|
||||
@@ -157,6 +159,15 @@ function DowntimeTooltip({ active, payload, t }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
function toMachineOption(value: unknown): MachineOption | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
const id = typeof record.id === "string" ? record.id : "";
|
||||
const name = typeof record.name === "string" ? record.name : "";
|
||||
if (!id || !name) return null;
|
||||
return { id, name };
|
||||
}
|
||||
|
||||
function buildCsv(report: ReportPayload, t: Translator) {
|
||||
const rows = new Map<string, Record<string, string | number>>();
|
||||
const addSeries = (series: ReportTrendPoint[], key: string) => {
|
||||
@@ -386,13 +397,29 @@ export default function ReportsPage() {
|
||||
const res = await fetch("/api/machines", { cache: "no-store" });
|
||||
const json = await res.json();
|
||||
if (!alive) return;
|
||||
setMachines((json?.machines ?? []).map((m: any) => ({ id: m.id, name: m.name })));
|
||||
const rows: unknown[] = Array.isArray(json?.machines) ? json.machines : [];
|
||||
const options: MachineOption[] = [];
|
||||
rows.forEach((row) => {
|
||||
const option = toMachineOption(row);
|
||||
if (option) options.push(option);
|
||||
});
|
||||
setMachines(options);
|
||||
} catch {
|
||||
if (!alive) return;
|
||||
setMachines([]);
|
||||
}
|
||||
}
|
||||
|
||||
loadMachines();
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -402,7 +429,10 @@ export default function ReportsPage() {
|
||||
if (workOrderId) params.set("workOrderId", workOrderId);
|
||||
if (sku) params.set("sku", sku);
|
||||
|
||||
const res = await fetch(`/api/reports?${params.toString()}`, { cache: "no-store" });
|
||||
const res = await fetch(`/api/reports?${params.toString()}`, {
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!alive) return;
|
||||
if (!res.ok || json?.ok === false) {
|
||||
@@ -420,21 +450,25 @@ export default function ReportsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
loadMachines();
|
||||
load();
|
||||
return () => {
|
||||
alive = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [range, machineId, workOrderId, sku]);
|
||||
}, [range, machineId, workOrderId, sku, t]);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
async function loadFilters() {
|
||||
try {
|
||||
const params = new URLSearchParams({ range });
|
||||
if (machineId) params.set("machineId", machineId);
|
||||
const res = await fetch(`/api/reports/filters?${params.toString()}`, { cache: "no-store" });
|
||||
const res = await fetch(`/api/reports/filters?${params.toString()}`, {
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!alive) return;
|
||||
if (!res.ok || json?.ok === false) {
|
||||
@@ -454,6 +488,7 @@ export default function ReportsPage() {
|
||||
loadFilters();
|
||||
return () => {
|
||||
alive = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [range, machineId]);
|
||||
|
||||
@@ -536,23 +571,23 @@ export default function ReportsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-white">{t("reports.title")}</h1>
|
||||
<p className="text-sm text-zinc-400">{t("reports.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto">
|
||||
<button
|
||||
onClick={handleExportCsv}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10"
|
||||
className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10 sm:w-auto"
|
||||
>
|
||||
{t("reports.exportCsv")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportPdf}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10"
|
||||
className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white hover:bg-white/10 sm:w-auto"
|
||||
>
|
||||
{t("reports.exportPdf")}
|
||||
</button>
|
||||
@@ -681,7 +716,10 @@ export default function ReportsPage() {
|
||||
const row = payload?.[0]?.payload;
|
||||
return row?.ts ? new Date(row.ts).toLocaleString(locale) : "";
|
||||
}}
|
||||
formatter={(val: any) => [`${Number(val).toFixed(1)}%`, "OEE"]}
|
||||
formatter={(val: number | string | undefined) => [
|
||||
val == null ? "--" : `${Number(val).toFixed(1)}%`,
|
||||
"OEE",
|
||||
]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="value" stroke="#34d399" dot={false} strokeWidth={2} />
|
||||
</LineChart>
|
||||
@@ -761,7 +799,10 @@ export default function ReportsPage() {
|
||||
const row = payload?.[0]?.payload;
|
||||
return row?.ts ? new Date(row.ts).toLocaleString(locale) : "";
|
||||
}}
|
||||
formatter={(val: any) => [`${Number(val).toFixed(1)}%`, t("reports.scrapRate")]}
|
||||
formatter={(val: number | string | undefined) => [
|
||||
val == null ? "--" : `${Number(val).toFixed(1)}%`,
|
||||
t("reports.scrapRate"),
|
||||
]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="value" stroke="#f97316" dot={false} strokeWidth={2} />
|
||||
</LineChart>
|
||||
|
||||
Reference in New Issue
Block a user