Files
vexplor/frontend/components/pop/hardcoded/quality/QualityHome.tsx

220 lines
9.4 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { apiClient } from "@/lib/api/client";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
interface RecentItem {
id: string;
itemName: string;
itemCode: string;
inspectionType: string;
judgment: string;
judgmentColor: string;
judgmentLabel: string;
time: string;
}
interface KpiData {
todayTotal: number;
todayPass: number;
todayFail: number;
passRate: number;
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
function getJudgmentStyle(j: string): { color: string; label: string } {
if (j === "합격" || j === "pass") return { color: "text-green-600 bg-green-50", label: "합격" };
if (j === "불합격" || j === "fail") return { color: "text-red-600 bg-red-50", label: "불합격" };
return { color: "text-amber-600 bg-amber-50", label: "대기" };
}
const MENU_ITEMS = [
{
id: "inspection",
title: "검사관리",
gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)",
shadowColor: "rgba(139,92,246,.3)",
icon: (
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z" />
</svg>
),
href: "/pop/quality/inspection",
},
];
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function QualityHome() {
const router = useRouter();
const [kpi, setKpi] = useState<KpiData>({ todayTotal: 0, todayPass: 0, todayFail: 0, passRate: 0 });
const [recentItems, setRecentItems] = useState<RecentItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const today = new Date().toISOString().slice(0, 10);
const res = await apiClient.post("/table-management/tables/inspection_result_mng/data", {
page: 1,
pageSize: 500,
});
const rows: any[] = res.data?.data?.data ?? res.data?.data ?? res.data?.rows ?? [];
const todayRows = rows.filter((r: any) => (r.created_date || "").slice(0, 10) === today);
const total = todayRows.length;
const pass = todayRows.filter((r: any) => r.overall_judgment === "합격" || r.overall_judgment === "pass").length;
const fail = todayRows.filter((r: any) => r.overall_judgment === "불합격" || r.overall_judgment === "fail").length;
const passRate = total > 0 ? Math.round((pass / total) * 100) : 0;
setKpi({ todayTotal: total, todayPass: pass, todayFail: fail, passRate });
// 최근 5건
const sorted = [...rows].sort((a: any, b: any) => (b.created_date || "").localeCompare(a.created_date || ""));
const top5 = sorted.slice(0, 5).map((r: any, idx: number) => {
const js = getJudgmentStyle(r.overall_judgment || r.judgment || "");
return {
id: `${r.id || idx}`,
itemName: r.item_name || "-",
itemCode: r.item_code || "",
inspectionType: r.inspection_type || "",
judgment: r.overall_judgment || "",
judgmentColor: js.color,
judgmentLabel: js.label,
time: r.created_date ? new Date(r.created_date).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }) : "--:--",
};
});
setRecentItems(top5);
} catch {
// empty
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return (
<div className="flex flex-col gap-5">
{/* Back + Title */}
<div className="flex items-center gap-3">
<button
onClick={() => router.push("/pop/home")}
className="w-10 h-10 rounded-xl bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight"></h1>
<p className="text-xs text-gray-400 mt-0.5"> </p>
</div>
</div>
{/* KPI */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 sm:p-5">
<div className="grid grid-cols-4 gap-0">
<KpiCell value={loading ? "-" : kpi.todayTotal.toLocaleString()} label="금일 검사" color="text-gray-900" />
<KpiCell value={loading ? "-" : kpi.todayPass.toLocaleString()} label="합격" color="text-green-600" />
<KpiCell value={loading ? "-" : kpi.todayFail.toLocaleString()} label="불합격" color="text-red-600" />
<KpiCell value={loading ? "-" : `${kpi.passRate}%`} label="합격률" color="text-violet-600" />
</div>
</div>
{/* Menu Icons */}
<section>
<div className="flex items-center gap-2 mb-3">
<div className="w-1 h-5 rounded-full bg-violet-500" />
<h2 className="text-base sm:text-lg font-bold text-gray-900"> </h2>
</div>
<div className="flex flex-wrap justify-start gap-x-5 gap-y-4 sm:gap-x-6 sm:gap-y-5">
{MENU_ITEMS.map((item) => (
<div
key={item.id}
className="flex flex-col items-center gap-2 w-16 sm:w-[72px] cursor-pointer group"
style={{ WebkitTapHighlightColor: "transparent" }}
onClick={() => router.push(item.href)}
>
<div
className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl flex items-center justify-center transition-transform duration-150 group-hover:scale-105 group-active:scale-[0.93]"
style={{ background: item.gradient, boxShadow: `0 4px 12px ${item.shadowColor}` }}
>
{item.icon}
</div>
<span className="text-[11px] sm:text-xs font-semibold text-gray-700 text-center leading-tight">
{item.title}
</span>
</div>
))}
</div>
</section>
{/* Recent Activity */}
<section>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 sm:p-5">
<div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
<h3 className="text-base sm:text-lg font-bold text-gray-900"> </h3>
<span className="text-xs text-gray-400"> 5</span>
</div>
<div className="flex flex-col gap-2">
{loading ? (
<div className="text-center py-8 text-sm text-gray-400"> ...</div>
) : recentItems.length === 0 ? (
<div className="text-center py-8 text-sm text-gray-400"> </div>
) : (
recentItems.map((item) => (
<div key={item.id} className="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors">
<span className="text-xs font-semibold text-gray-400 min-w-[44px] text-right" style={{ fontVariantNumeric: "tabular-nums" }}>
{item.time}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-900 truncate">
{item.itemName}
{item.itemCode ? ` (${item.itemCode})` : ""}
</span>
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded-full shrink-0 ${item.judgmentColor}`}>
{item.judgmentLabel}
</span>
</div>
<div className="text-xs text-gray-400 mt-0.5 truncate">{item.inspectionType}</div>
</div>
</div>
))
)}
</div>
</div>
</section>
</div>
);
}
function KpiCell({ value, label, color }: { value: string; label: string; color: string }) {
return (
<div className="flex flex-col items-center py-2">
<span
className={`text-2xl sm:text-3xl font-extrabold leading-none ${color}`}
style={{ fontVariantNumeric: "tabular-nums", letterSpacing: "-0.02em" }}
>
{value}
</span>
<span className="text-[11px] font-medium text-gray-400 mt-1">{label}</span>
</div>
);
}