주문서 수주통합등록 다이얼로그 wace G2 1:1 재작성
기존 RPS 폼이 wace estimateAndOrderRegistFormPopup과 크게 어긋남(헤더 항목 잘못 배치 + 라인 누락). 통째 재작성. 수주통합 기본정보 — wace 헤더 9개 정확 일치: 영업번호(자동채번) / 주문유형* / 국내해외* / 고객사* / 유무상* / 접수일* / 견적환종 / 견적환율 / 발주번호 / 발주일* (이전 RPS의 요청납기/통화/수주상태/담당자(PM ID)/출하방법/고객사요청사항 헤더 배치 폐지 — wace 통합폼에 없음) 품목정보 — wace 라인 13컬럼 + Total 합계 행: No / 제품구분* / 품번* / 품명* / S/N / 요청납기 / 고객요청사항 / 반납사유 / 수주수량* / 수주단가 / 수주공급가액 / 수주부가세 / 수주총액 / 삭제 (이전 누락: 제품구분 / S/N / 요청납기 / 고객요청사항 / 반납사유) 추가 컴포넌트: - 품번/품명: PartSelect (part_mng 8,176건 검색 + 자동 매핑) - 제품구분/반납사유: CommCodeSelect (0000001 / 0001810) - S/N: 별도 다이얼로그(테이블 + 연속번호생성) — wace fn_openItemSnPopup / fn_openItemSequentialSnPopup 1:1 - 합계: useMemo로 라인 수량/공급가액/부가세/총액 실시간 집계 EMPTY_ITEM 라인 기본값에 product/serials/due_date/return_reason/customer_request 추가. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,13 @@ const CONTRACT_RESULTS = [
|
||||
{ value: "0000965", label: "Cancel" },
|
||||
];
|
||||
|
||||
// wace estimateAndOrderRegistFormPopup 라인 — 제품구분/S/N/요청납기/반납사유/고객요청사항 포함
|
||||
const EMPTY_ITEM: OrderItem = {
|
||||
seq: 1, part_objid: "", part_no: "", part_name: "", quantity: 1,
|
||||
seq: 1,
|
||||
product: "", part_objid: "", part_no: "", part_name: "",
|
||||
serials: [],
|
||||
due_date: "", return_reason: "", customer_request: "",
|
||||
quantity: 1,
|
||||
order_quantity: "", order_unit_price: "0", order_supply_price: "0",
|
||||
order_vat: "0", order_total_amount: "0",
|
||||
};
|
||||
@@ -98,8 +103,18 @@ export default function SalesOrderPage() {
|
||||
items: [],
|
||||
});
|
||||
|
||||
// 품목 검색 모달
|
||||
// 품목 검색 모달 (라인별 진입)
|
||||
const [itemDialogOpen, setItemDialogOpen] = useState(false);
|
||||
const [itemSearchTargetIdx, setItemSearchTargetIdx] = useState<number | null>(null);
|
||||
|
||||
// S/N 관리 모달 + 연속번호 생성 (wace fn_openItemSnPopup / fn_openItemSequentialSnPopup)
|
||||
const [serialDialogOpen, setSerialDialogOpen] = useState(false);
|
||||
const [serialDialogIdx, setSerialDialogIdx] = useState<number | null>(null);
|
||||
const [serialDraft, setSerialDraft] = useState<string[]>([]);
|
||||
const [serialInput, setSerialInput] = useState("");
|
||||
const [seqDialogOpen, setSeqDialogOpen] = useState(false);
|
||||
const [seqStartNo, setSeqStartNo] = useState("");
|
||||
const [seqCount, setSeqCount] = useState("");
|
||||
|
||||
// 첨부파일 다이얼로그 (주문서첨부 클립 컬럼 클릭 시)
|
||||
const [attachDialogOpen, setAttachDialogOpen] = useState(false);
|
||||
@@ -363,6 +378,63 @@ export default function SalesOrderPage() {
|
||||
const addItem = () => setForm((prev) => ({ ...prev, items: [...(prev.items ?? []), { ...EMPTY_ITEM, seq: (prev.items?.length ?? 0) + 1 }] }));
|
||||
const removeItem = (idx: number) => setForm((prev) => ({ ...prev, items: (prev.items ?? []).filter((_, i) => i !== idx).map((it, i) => ({ ...it, seq: i + 1 })) }));
|
||||
|
||||
// S/N 관리 (wace fn_openItemSnPopup) — 견적관리와 동일 패턴
|
||||
const openSerialDialog = (idx: number) => {
|
||||
const item = form.items?.[idx];
|
||||
setSerialDialogIdx(idx);
|
||||
setSerialDraft([...(item?.serials ?? [])]);
|
||||
setSerialInput("");
|
||||
setSerialDialogOpen(true);
|
||||
};
|
||||
const addSerialDraft = () => {
|
||||
const v = serialInput.trim();
|
||||
if (!v) { toast.warning("S/N을 입력해주세요."); return; }
|
||||
if (serialDraft.includes(v)) { toast.warning("이미 등록된 S/N입니다."); return; }
|
||||
setSerialDraft((prev) => [...prev, v]);
|
||||
setSerialInput("");
|
||||
};
|
||||
const removeSerialDraft = (i: number) => setSerialDraft((prev) => prev.filter((_, k) => k !== i));
|
||||
const applySerialDraft = () => {
|
||||
if (serialDialogIdx === null) return;
|
||||
updateItem(serialDialogIdx, "serials", [...serialDraft]);
|
||||
setSerialDialogOpen(false);
|
||||
};
|
||||
const openSeqDialog = () => { setSeqStartNo(""); setSeqCount(""); setSeqDialogOpen(true); };
|
||||
const generateSequentialSn = () => {
|
||||
const startNo = seqStartNo.trim();
|
||||
const count = parseInt(seqCount, 10);
|
||||
if (!startNo) { toast.warning("시작 번호를 입력해주세요."); return; }
|
||||
if (!count || count < 1) { toast.warning("생성 개수를 1 이상 입력해주세요."); return; }
|
||||
if (count > 100) { toast.warning("최대 100개까지만 생성 가능합니다."); return; }
|
||||
const m = startNo.match(/^(.*?)(\d+)$/);
|
||||
if (!m) { toast.warning("형식이 올바르지 않습니다. 마지막에 숫자가 있어야 합니다."); return; }
|
||||
const prefix = m[1]; const startNum = parseInt(m[2], 10); const numLen = m[2].length;
|
||||
setSerialDraft((prev) => {
|
||||
const next = [...prev];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const sn = prefix + String(startNum + i).padStart(numLen, "0");
|
||||
if (!next.includes(sn)) next.push(sn);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setSeqDialogOpen(false);
|
||||
};
|
||||
|
||||
// 라인 합계 자동 계산용 헬퍼
|
||||
const formatNum = (v: any) => {
|
||||
const n = Number(String(v ?? "0").replace(/,/g, ""));
|
||||
return isNaN(n) ? 0 : n;
|
||||
};
|
||||
const lineTotal = useMemo(() => {
|
||||
const items = form.items ?? [];
|
||||
return items.reduce((acc, it) => ({
|
||||
qty: acc.qty + formatNum(it.order_quantity),
|
||||
supply: acc.supply + formatNum(it.order_supply_price),
|
||||
vat: acc.vat + formatNum(it.order_vat),
|
||||
total: acc.total + formatNum(it.order_total_amount),
|
||||
}), { qty: 0, supply: 0, vat: 0, total: 0 });
|
||||
}, [form.items]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-4 gap-4">
|
||||
{ConfirmDialogComponent}
|
||||
@@ -489,107 +561,194 @@ export default function SalesOrderPage() {
|
||||
/>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="!max-w-[95vw] w-[95vw] max-h-[92vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dialogMode === "create" ? "주문서 등록" : "주문서 수정"}</DialogTitle>
|
||||
<DialogDescription>주문 헤더 + 라인을 입력합니다.</DialogDescription>
|
||||
<DialogTitle>영업관리 _ 주문서관리 _ 수주통합등록</DialogTitle>
|
||||
<DialogDescription className="sr-only">수주통합 기본정보 + 품목정보를 입력합니다. (wace estimateAndOrderRegistFormPopup 1:1)</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 수주통합 기본정보 — wace 헤더 9개 (영업번호 자동채번 표시 포함) */}
|
||||
<fieldset className="border rounded-md p-3">
|
||||
<legend className="text-sm font-semibold px-2">주문 헤더</legend>
|
||||
<legend className="text-sm font-semibold px-2">수주통합 기본정보</legend>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div><Label className="text-xs">영업번호 (자동채번)</Label>
|
||||
<Input readOnly className="bg-muted/30"
|
||||
value={form.contract_no ?? ""}
|
||||
placeholder="저장 시 자동 부여됩니다" /></div>
|
||||
<div><Label className="text-xs">발주번호</Label>
|
||||
<Input value={form.po_no ?? ""} onChange={(e) => setForm({ ...form, po_no: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">발주일 <span className="text-rose-600">*</span></Label>
|
||||
<Input type="date" value={form.order_date ?? ""} onChange={(e) => setForm({ ...form, order_date: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">요청납기</Label>
|
||||
<Input type="date" value={form.req_del_date ?? ""} onChange={(e) => setForm({ ...form, req_del_date: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">고객사</Label>
|
||||
<CustomerSelect
|
||||
value={form.customer_objid ?? ""}
|
||||
onValueChange={(v) => setForm({ ...form, customer_objid: v })}
|
||||
/></div>
|
||||
<div><Label className="text-xs">접수일</Label>
|
||||
<Input type="date" value={form.receipt_date ?? ""} onChange={(e) => setForm({ ...form, receipt_date: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">통화</Label>
|
||||
<Input value={form.contract_currency ?? "KRW"} onChange={(e) => setForm({ ...form, contract_currency: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">환율</Label>
|
||||
<Input value={form.exchange_rate ?? ""} onChange={(e) => setForm({ ...form, exchange_rate: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">유/무상</Label>
|
||||
<Select value={form.paid_type ?? "paid"} onValueChange={(v) => setForm({ ...form, paid_type: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<div>
|
||||
<Label className="text-xs">영업번호 (자동채번)</Label>
|
||||
<Input readOnly className="bg-muted/30" value={form.contract_no ?? ""} placeholder="저장 시 자동 부여" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">주문유형 <span className="text-rose-600">*</span></Label>
|
||||
<CommCodeSelect groupId="0000167" value={form.category_cd ?? ""}
|
||||
onValueChange={(v) => setForm({ ...form, category_cd: v })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">국내/해외 <span className="text-rose-600">*</span></Label>
|
||||
<CommCodeSelect groupId="0001219" value={form.area_cd ?? ""}
|
||||
onValueChange={(v) => setForm({ ...form, area_cd: v })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">고객사 <span className="text-rose-600">*</span></Label>
|
||||
<CustomerSelect value={form.customer_objid ?? ""}
|
||||
onValueChange={(v) => setForm({ ...form, customer_objid: v })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">유/무상 <span className="text-rose-600">*</span></Label>
|
||||
<Select value={form.paid_type || undefined}
|
||||
onValueChange={(v) => setForm({ ...form, paid_type: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="paid">유상</SelectItem>
|
||||
<SelectItem value="free">무상</SelectItem>
|
||||
</SelectContent>
|
||||
</Select></div>
|
||||
<div><Label className="text-xs">수주상태</Label>
|
||||
<Select value={form.contract_result || undefined} onValueChange={(v) => setForm({ ...form, contract_result: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONTRACT_RESULTS.filter((o) => o.value).map((o) => (<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select></div>
|
||||
<div><Label className="text-xs">담당자(PM ID)</Label>
|
||||
<Input value={form.pm_user_id ?? ""} onChange={(e) => setForm({ ...form, pm_user_id: e.target.value })} /></div>
|
||||
<div><Label className="text-xs">출하방법</Label>
|
||||
<Input value={form.shipping_method ?? ""} onChange={(e) => setForm({ ...form, shipping_method: e.target.value })} /></div>
|
||||
<div className="col-span-4"><Label className="text-xs">고객사 요청사항</Label>
|
||||
<Textarea rows={2} value={form.customer_request ?? ""} onChange={(e) => setForm({ ...form, customer_request: e.target.value })} /></div>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">접수일 <span className="text-rose-600">*</span></Label>
|
||||
<Input type="date" value={form.receipt_date ?? ""}
|
||||
onChange={(e) => setForm({ ...form, receipt_date: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">견적환종</Label>
|
||||
<CommCodeSelect groupId="0001533" value={form.contract_currency ?? ""}
|
||||
onValueChange={(v) => setForm({ ...form, contract_currency: v })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">견적환율</Label>
|
||||
<Input value={form.exchange_rate ?? ""}
|
||||
onChange={(e) => setForm({ ...form, exchange_rate: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">발주번호</Label>
|
||||
<Input value={form.po_no ?? ""}
|
||||
onChange={(e) => setForm({ ...form, po_no: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">발주일 <span className="text-rose-600">*</span></Label>
|
||||
<Input type="date" value={form.order_date ?? ""}
|
||||
onChange={(e) => setForm({ ...form, order_date: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* 품목정보 — wace 13컬럼 + Total 합계 행 */}
|
||||
<fieldset className="border rounded-md p-3 space-y-2">
|
||||
<legend className="text-sm font-semibold px-2">주문 라인</legend>
|
||||
<legend className="text-sm font-semibold px-2 flex items-center justify-between w-full">
|
||||
<span>품목정보</span>
|
||||
</legend>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="p-2 w-10">#</th>
|
||||
<th className="p-2 w-32">품목 ID</th>
|
||||
<th className="p-2">품번</th>
|
||||
<th className="p-2">품명</th>
|
||||
<th className="p-2 w-24">수주수량</th>
|
||||
<th className="p-2 w-28">단가</th>
|
||||
<th className="p-2 w-28">공급가액</th>
|
||||
<th className="p-2 w-24">부가세</th>
|
||||
<th className="p-2 w-28">총액</th>
|
||||
<th className="p-2 w-28">납기</th>
|
||||
<th className="p-2 w-10"></th>
|
||||
<th className="p-2 w-10 whitespace-nowrap">No</th>
|
||||
<th className="p-2 w-28 whitespace-nowrap">제품구분 <span className="text-rose-600">*</span></th>
|
||||
<th className="p-2 w-40 whitespace-nowrap">품번 <span className="text-rose-600">*</span></th>
|
||||
<th className="p-2 w-56 whitespace-nowrap">품명 <span className="text-rose-600">*</span></th>
|
||||
<th className="p-2 w-40 whitespace-nowrap">S/N</th>
|
||||
<th className="p-2 w-36 whitespace-nowrap">요청납기</th>
|
||||
<th className="p-2 min-w-[220px] whitespace-nowrap">고객요청사항</th>
|
||||
<th className="p-2 w-28 whitespace-nowrap">반납사유</th>
|
||||
<th className="p-2 w-24 whitespace-nowrap text-right">수주수량 <span className="text-rose-600">*</span></th>
|
||||
<th className="p-2 w-28 whitespace-nowrap text-right">수주단가</th>
|
||||
<th className="p-2 w-32 whitespace-nowrap text-right">수주공급가액</th>
|
||||
<th className="p-2 w-28 whitespace-nowrap text-right">수주부가세</th>
|
||||
<th className="p-2 w-32 whitespace-nowrap text-right">수주총액</th>
|
||||
<th className="p-2 w-12 whitespace-nowrap">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(form.items ?? []).map((it, idx) => (
|
||||
<tr key={idx} className="border-t">
|
||||
<td className="p-1 text-center">{it.seq}</td>
|
||||
<td className="p-1"><Input className="h-8" value={it.part_objid} onChange={(e) => updateItem(idx, "part_objid", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8" value={it.part_no} onChange={(e) => updateItem(idx, "part_no", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8" value={it.part_name} onChange={(e) => updateItem(idx, "part_name", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_quantity ?? ""} onChange={(e) => updateItem(idx, "order_quantity", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_unit_price ?? ""} onChange={(e) => updateItem(idx, "order_unit_price", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_supply_price ?? ""} onChange={(e) => updateItem(idx, "order_supply_price", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_vat ?? ""} onChange={(e) => updateItem(idx, "order_vat", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_total_amount ?? ""} onChange={(e) => updateItem(idx, "order_total_amount", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8" type="date" value={it.due_date ?? ""} onChange={(e) => updateItem(idx, "due_date", e.target.value)} /></td>
|
||||
<td className="p-1 text-center"><Button variant="ghost" size="icon" onClick={() => removeItem(idx)}><Trash2 className="w-3 h-3" /></Button></td>
|
||||
<td className="p-1">
|
||||
<CommCodeSelect groupId="0000001"
|
||||
value={it.product ?? ""}
|
||||
onValueChange={(v) => updateItem(idx, "product", v)} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<PartSelect mode="partNo" value={it.part_objid}
|
||||
onValueChange={(partObjId, row) => {
|
||||
setForm((prev) => {
|
||||
const items = [...(prev.items ?? [])];
|
||||
items[idx] = { ...items[idx], part_objid: partObjId,
|
||||
part_no: row?.item_number ?? "",
|
||||
part_name: row?.item_name ?? items[idx].part_name };
|
||||
return { ...prev, items };
|
||||
});
|
||||
}} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<PartSelect mode="partName" value={it.part_objid}
|
||||
onValueChange={(partObjId, row) => {
|
||||
setForm((prev) => {
|
||||
const items = [...(prev.items ?? [])];
|
||||
items[idx] = { ...items[idx], part_objid: partObjId,
|
||||
part_no: row?.item_number ?? items[idx].part_no,
|
||||
part_name: row?.item_name ?? "" };
|
||||
return { ...prev, items };
|
||||
});
|
||||
}} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<Input className="h-8" readOnly
|
||||
value={(it.serials ?? []).join(", ")}
|
||||
onClick={() => openSerialDialog(idx)}
|
||||
placeholder="클릭하여 S/N 추가" />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<Input className="h-8" type="date" value={it.due_date ?? ""}
|
||||
onChange={(e) => updateItem(idx, "due_date", e.target.value)} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<Textarea className="min-h-[34px] resize-y text-xs" rows={1}
|
||||
value={it.customer_request ?? ""}
|
||||
onChange={(e) => updateItem(idx, "customer_request", e.target.value)} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<CommCodeSelect groupId="0001810"
|
||||
value={it.return_reason ?? ""}
|
||||
onValueChange={(v) => updateItem(idx, "return_reason", v)} />
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<Input className="h-8 text-right" type="number" min={0}
|
||||
value={it.order_quantity ?? ""}
|
||||
onChange={(e) => updateItem(idx, "order_quantity", e.target.value)} />
|
||||
</td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_unit_price ?? ""}
|
||||
onChange={(e) => updateItem(idx, "order_unit_price", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_supply_price ?? ""}
|
||||
onChange={(e) => updateItem(idx, "order_supply_price", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_vat ?? ""}
|
||||
onChange={(e) => updateItem(idx, "order_vat", e.target.value)} /></td>
|
||||
<td className="p-1"><Input className="h-8 text-right" value={it.order_total_amount ?? ""}
|
||||
onChange={(e) => updateItem(idx, "order_total_amount", e.target.value)} /></td>
|
||||
<td className="p-1 text-center">
|
||||
<Button variant="ghost" size="icon" onClick={() => removeItem(idx)}>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!form.items || form.items.length === 0) && (
|
||||
<tr><td colSpan={11} className="p-3 text-center text-muted-foreground">라인이 없습니다.</td></tr>
|
||||
<tr><td colSpan={14} className="p-3 text-center text-muted-foreground">품목이 없습니다.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
{(form.items?.length ?? 0) > 0 && (
|
||||
<tfoot>
|
||||
<tr className="bg-muted/30 font-semibold">
|
||||
<td colSpan={8} className="p-2 text-center">Total</td>
|
||||
<td className="p-2 text-right">{lineTotal.qty.toLocaleString()}</td>
|
||||
<td className="p-2"></td>
|
||||
<td className="p-2 text-right">{lineTotal.supply.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="p-2 text-right">{lineTotal.vat.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="p-2 text-right">{lineTotal.total.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="p-2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={addItem}>
|
||||
<Plus className="w-3 h-3 mr-1" />라인 추가
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setItemDialogOpen(true)}>
|
||||
<Search className="w-3 h-3 mr-1" />품목 검색
|
||||
<Plus className="w-3 h-3 mr-1" />품목 추가
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -603,6 +762,82 @@ export default function SalesOrderPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* S/N 관리 — wace fn_openItemSnPopup */}
|
||||
<Dialog open={serialDialogOpen} onOpenChange={setSerialDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">S/N 관리</DialogTitle>
|
||||
<DialogDescription className="sr-only">시리얼 번호 추가/삭제 및 연속번호 생성</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="max-h-[300px] overflow-y-auto border rounded">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="border px-3 py-2 w-16 text-center">번호</th>
|
||||
<th className="border px-3 py-2 text-center">S/N</th>
|
||||
<th className="border px-3 py-2 w-20 text-center">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{serialDraft.length === 0 ? (
|
||||
<tr><td colSpan={3} className="border px-3 py-8 text-center text-muted-foreground">등록된 S/N이 없습니다.</td></tr>
|
||||
) : serialDraft.map((s, i) => (
|
||||
<tr key={i}>
|
||||
<td className="border px-3 py-1.5 text-center">{i + 1}</td>
|
||||
<td className="border px-3 py-1.5">{s}</td>
|
||||
<td className="border px-3 py-1.5 text-center">
|
||||
<Button size="sm" variant="outline" onClick={() => removeSerialDraft(i)}>삭제</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input value={serialInput} onChange={(e) => setSerialInput(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addSerialDraft(); } }}
|
||||
placeholder="S/N 입력" />
|
||||
<Button onClick={addSerialDraft} type="button">추가</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-center">
|
||||
<Button variant="outline" onClick={openSeqDialog}>연속번호생성</Button>
|
||||
<Button onClick={applySerialDraft}>확인</Button>
|
||||
<Button variant="outline" onClick={() => setSerialDialogOpen(false)}>취소</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 연속번호 생성 — wace fn_openItemSequentialSnPopup */}
|
||||
<Dialog open={seqDialogOpen} onOpenChange={setSeqDialogOpen}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>연속번호 생성</DialogTitle>
|
||||
<DialogDescription className="sr-only">시작번호와 생성개수로 연속 S/N 일괄 추가</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-xs">시작번호 <span className="text-rose-600">*</span></Label>
|
||||
<Input value={seqStartNo} onChange={(e) => setSeqStartNo(e.target.value)} placeholder="예: ITEM-001" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">생성개수 <span className="text-rose-600">*</span></Label>
|
||||
<Input type="number" min={1} max={100} value={seqCount}
|
||||
onChange={(e) => setSeqCount(e.target.value)} placeholder="예: 10" />
|
||||
</div>
|
||||
<div className="bg-muted/40 rounded p-2 text-[11px] leading-5 text-muted-foreground">
|
||||
예: ITEM-001, 개수 3 → ITEM-001, ITEM-002, ITEM-003<br />
|
||||
※ 최대 100개까지 생성 가능
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSeqDialogOpen(false)}>취소</Button>
|
||||
<Button onClick={generateSequentialSn}>생성</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 첨부파일 다이얼로그 — 주문서첨부 클립 컬럼 클릭 시 */}
|
||||
{attachContext && (
|
||||
<AttachmentDialog
|
||||
|
||||
Reference in New Issue
Block a user