Files
vexplor/frontend/components/pop/hardcoded/production/AcceptProcessModal.tsx
SeongHyun Kim 089f1e4b4c feat: MES 공정 화면 (작업지시목록 + 공정작업상세 + 타이머 + 실적 + 입고)
- WorkOrderList: 탭 필터(전체/접수가능/진행중/대기/완료), 공정카드, 접수
- ProcessWork: 타이머(시작/정지/재개/종료), 실적입력(양품/불량), 확정, 생산입고
- ProcessTimer: HH:MM:SS 실시간, 상태별 색상
- DefectTypeModal: 불량유형 선택 + 수량 + 처리방법
- AcceptProcessModal: 접수 수량 키패드
- cmux 검증: 화면 표시 OK, 탭 필터 OK, API 연동 OK
2026-04-02 09:33:53 +09:00

185 lines
5.8 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
interface AcceptProcessModalProps {
open: boolean;
onClose: () => void;
onConfirm: (qty: number) => void;
maxQty: number;
processName: string;
seqNo: string;
loading?: boolean;
}
/* ------------------------------------------------------------------ */
/* Numpad Keys */
/* ------------------------------------------------------------------ */
const KEYS = [
{ label: "7", action: "7" },
{ label: "8", action: "8" },
{ label: "9", action: "9" },
{ label: "\u2190", action: "backspace" },
{ label: "4", action: "4" },
{ label: "5", action: "5" },
{ label: "6", action: "6" },
{ label: "C", action: "clear" },
{ label: "1", action: "1" },
{ label: "2", action: "2" },
{ label: "3", action: "3" },
{ label: "MAX", action: "max" },
];
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function AcceptProcessModal({
open,
onClose,
onConfirm,
maxQty,
processName,
seqNo,
loading = false,
}: AcceptProcessModalProps) {
const [qty, setQty] = useState("0");
useEffect(() => {
if (open) {
setQty("0");
}
}, [open]);
const qtyNum = parseInt(qty, 10) || 0;
const isOverMax = qtyNum > maxQty;
const handleKey = useCallback(
(key: string) => {
setQty((prev) => {
switch (key) {
case "backspace":
return prev.length <= 1 ? "0" : prev.slice(0, -1);
case "clear":
return "0";
case "max":
return String(maxQty);
default: {
const next = prev === "0" ? key : prev + key;
const num = parseInt(next, 10);
if (isNaN(num)) return prev;
return next;
}
}
});
},
[maxQty]
);
const handleConfirm = () => {
const finalQty = Math.min(qtyNum, maxQty);
if (finalQty <= 0) return;
onConfirm(finalQty);
};
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Overlay */}
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
{/* Panel */}
<div className="relative bg-white w-[90%] max-w-[360px] rounded-2xl shadow-2xl z-10 overflow-hidden">
{/* Header */}
<div
className="px-4 py-3"
style={{ background: "linear-gradient(135deg, #f59e0b 0%, #d97706 100%)" }}
>
<div className="flex items-center justify-between">
<span className="text-white font-bold text-base"> </span>
<span className="text-white/80 text-xs bg-white/20 px-2.5 py-1 rounded-full">
{maxQty.toLocaleString()} EA
</span>
</div>
<p className="text-white/80 text-xs mt-1">
{seqNo}: {processName}
</p>
</div>
{/* Body */}
<div className="p-4">
<p className="text-center text-sm font-semibold text-gray-700 mb-1">
</p>
<p className="text-center text-xs text-gray-400 mb-3">
(EA)
</p>
{/* Display */}
<input
type="text"
readOnly
value={qtyNum.toLocaleString()}
className={`w-full px-4 py-3 text-right text-3xl font-bold border-2 rounded-xl bg-gray-50 mb-3 ${
isOverMax ? "border-red-300 text-red-500" : "border-gray-200 text-gray-900"
}`}
style={{ fontVariantNumeric: "tabular-nums" }}
/>
{isOverMax && (
<p className="text-xs text-red-500 text-center mb-2 font-medium">
({maxQty.toLocaleString()})
</p>
)}
{/* Numpad */}
<div className="grid grid-cols-4 gap-2.5">
{KEYS.map((key) => (
<button
key={key.action}
onClick={() => handleKey(key.action)}
className={`h-14 rounded-xl text-lg font-semibold active:scale-95 transition-all ${
key.action === "backspace" || key.action === "clear"
? "bg-amber-100 text-amber-700 hover:bg-amber-200"
: key.action === "max"
? "bg-blue-100 text-blue-700 hover:bg-blue-200 text-sm"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
}`}
>
{key.label}
</button>
))}
{/* Bottom row */}
<button
onClick={() => handleKey("0")}
className="col-span-2 h-14 rounded-xl text-lg font-semibold bg-gray-100 text-gray-900 hover:bg-gray-200 active:scale-95 transition-all"
>
0
</button>
<button
onClick={handleConfirm}
disabled={qtyNum <= 0 || loading}
className="col-span-2 h-14 rounded-xl text-lg font-bold text-white active:scale-95 transition-all disabled:opacity-40"
style={{
background:
qtyNum <= 0 || loading
? "#9ca3af"
: "linear-gradient(135deg, #f59e0b 0%, #d97706 100%)",
}}
>
{loading ? "처리중..." : "접수"}
</button>
</div>
</div>
</div>
</div>
);
}