- WorkOrderList: 탭 필터(전체/접수가능/진행중/대기/완료), 공정카드, 접수 - ProcessWork: 타이머(시작/정지/재개/종료), 실적입력(양품/불량), 확정, 생산입고 - ProcessTimer: HH:MM:SS 실시간, 상태별 색상 - DefectTypeModal: 불량유형 선택 + 수량 + 처리방법 - AcceptProcessModal: 접수 수량 키패드 - cmux 검증: 화면 표시 OK, 탭 필터 OK, API 연동 OK
185 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|