Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s
- 구매입고: 검사기준 API 수정, 검사결과 DB 저장, 검사 미완료 확정 차단 - 판매출고: 재고 부족 사전 검증, 수주상세 ship_qty 반영, 에러 메시지 개선 - 공정실행: seq_no 비순차 대응(3곳), 자재투입 자동 창고 매칭 재고차감, 불필요 버튼 제거 - 검사관리+입출고관리: 신규 화면 (quality, inventory) - 공통: ConfirmModal 커스텀 모달 (native confirm 대체)
320 lines
9.3 KiB
TypeScript
320 lines
9.3 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import React, { useEffect, useState } from "react";
|
|
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>
|
|
);
|
|
}
|