Files
vexplor/frontend/components/pop/hardcoded/production/ProcessDetailModal.tsx
SeongHyun Kim 9b7b88ff7c feat: POP 하드코딩 화면 추가 (PC 코드 무변경 재병합)
- POP 전용 39개 파일 추가 (홈/입고/출고/생산)
- 백엔드 INSERT에 id gen_random_uuid 추가 (5개 파일)
- POP 전용 API 7개 추가 (창고/위치/입고/동기화)
- PC 코드 구조/순서/로직 변경 없음 (AppLayout, UserDropdown 미수정)
2026-04-02 17:39:42 +09:00

231 lines
9.4 KiB
TypeScript

"use client";
import React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export interface ProcessStep {
no: number;
name: string;
code: string;
status: "completed" | "in_progress" | "waiting" | "acceptable";
inputQty: number;
goodQty: number;
defectQty: number;
planQty: number;
availableQty: number;
}
interface ProcessDetailModalProps {
open: boolean;
onClose: () => void;
workInstructionNo: string;
totalQty: number;
steps: ProcessStep[];
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function ProcessDetailModal({
open,
onClose,
workInstructionNo,
totalQty,
steps,
}: ProcessDetailModalProps) {
if (!open) return null;
const completedCount = steps.filter((s) => s.status === "completed").length;
const overallPct = steps.length > 0 ? Math.round((completedCount / steps.length) * 100) : 0;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: "rgba(0,0,0,0.5)" }}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
{/* Header */}
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
<div>
<h3 className="text-lg font-bold text-gray-900"> </h3>
<p className="text-xs text-gray-400 mt-0.5">{workInstructionNo}</p>
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500 hover:bg-gray-200 active:scale-95 transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Summary bar */}
<div className="px-5 py-4">
<div className="mb-4 p-4 bg-gray-50 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-gray-900"> </span>
<span className="text-xl font-extrabold text-gray-900">
{totalQty.toLocaleString()} EA
</span>
</div>
<div className="flex items-center gap-2">
<div className="h-3 flex-1 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${overallPct}%` }}
/>
</div>
<span className="text-sm font-bold text-blue-600">
{completedCount}/{steps.length}
</span>
</div>
</div>
</div>
{/* Steps */}
<div className="px-5 pb-4 max-h-[400px] overflow-y-auto -mt-2">
{steps.map((s) => {
const borderColor =
s.status === "completed"
? "border-green-400 bg-green-50"
: s.status === "in_progress"
? "border-blue-400 bg-blue-50"
: s.status === "acceptable"
? "border-amber-400 bg-amber-50"
: "border-gray-200 bg-gray-50";
const dotColor =
s.status === "completed"
? "bg-green-500"
: s.status === "in_progress"
? "bg-blue-500"
: s.status === "acceptable"
? "bg-amber-500"
: "bg-gray-400";
const badge =
s.status === "completed" ? (
<span className="text-xs font-bold px-3 py-1 rounded-full bg-green-100 text-green-700"></span>
) : s.status === "in_progress" ? (
<span className="text-xs font-bold px-3 py-1 rounded-full bg-blue-100 text-blue-700"></span>
) : s.status === "acceptable" ? (
<span className="text-xs font-bold px-3 py-1 rounded-full bg-amber-100 text-amber-700"></span>
) : (
<span className="text-xs font-bold px-3 py-1 rounded-full bg-gray-100 text-gray-500"></span>
);
const barColor =
s.status === "completed"
? "bg-green-500"
: s.status === "in_progress"
? "bg-blue-500"
: s.status === "acceptable"
? "bg-amber-500"
: "bg-gray-300";
const barTextColor =
s.status === "completed"
? "text-green-600"
: s.status === "in_progress"
? "text-blue-600"
: s.status === "acceptable"
? "text-amber-600"
: "text-gray-400";
const pct = s.planQty > 0 ? Math.round((s.inputQty / s.planQty) * 100) : 0;
const unaccept = s.planQty - s.inputQty;
return (
<div key={s.code + "-" + s.no} className={`rounded-xl border-2 ${borderColor} mb-3 overflow-hidden`}>
{/* Header */}
<div className="flex items-center gap-3 p-4 pb-2">
<div
className={`w-10 h-10 rounded-full ${dotColor} text-white flex items-center justify-center text-base font-bold shrink-0`}
>
{s.no}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-base font-bold text-gray-900 truncate">{s.name}</span>
{badge}
</div>
<span className="text-xs text-gray-400">{s.code}</span>
</div>
</div>
{/* Progress bar */}
<div className="px-4 pb-2">
<div className="flex items-center gap-2">
<div className="h-2.5 flex-1 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full ${barColor} rounded-full transition-all`}
style={{ width: `${Math.min(pct, 100)}%` }}
/>
</div>
<span className={`text-xs font-bold ${barTextColor}`}>{pct}%</span>
</div>
</div>
{/* Qty grid */}
<div className="grid grid-cols-4 gap-1 px-4 pb-3">
<div className="text-center py-1.5">
<div className="text-[10px] text-gray-400"></div>
<div className="text-base font-extrabold text-gray-900">{s.planQty.toLocaleString()}</div>
</div>
<div className="text-center py-1.5">
<div className="text-[10px] text-blue-500"></div>
<div className="text-base font-extrabold text-blue-700">{s.inputQty.toLocaleString()}</div>
</div>
<div className="text-center py-1.5">
<div className="text-[10px] text-emerald-500"></div>
<div className="text-base font-extrabold text-emerald-600">{s.goodQty.toLocaleString()}</div>
</div>
{s.status === "completed" || s.status === "in_progress" ? (
<div className="text-center py-1.5">
<div className="text-[10px] text-red-500"></div>
<div className="text-base font-extrabold text-red-600">{s.defectQty.toLocaleString()}</div>
</div>
) : (
<div className="text-center py-1.5">
<div className="text-[10px] text-gray-400"></div>
<div className="text-base font-extrabold text-gray-500">{Math.max(0, unaccept).toLocaleString()}</div>
</div>
)}
</div>
{/* Additional accept qty */}
{s.status === "in_progress" && s.availableQty > 0 && (
<div className="px-4 pb-3 text-right">
<span className="text-xs text-violet-600 font-semibold">
{s.availableQty.toLocaleString()}
</span>
</div>
)}
{s.status === "acceptable" && s.availableQty > 0 && (
<div className="px-4 pb-3 text-right">
<span className="text-xs text-amber-600 font-semibold">
{s.availableQty.toLocaleString()}
</span>
</div>
)}
</div>
);
})}
</div>
{/* Footer */}
<div className="px-5 py-3 border-t border-gray-100">
<button
onClick={onClose}
className="w-full py-3 rounded-xl text-sm font-bold text-white bg-blue-500 active:scale-[0.98] transition-all"
>
</button>
</div>
</div>
</div>
);
}