feat(orders): ITEM 수량/비고 인라인 즉시저장 + 왼쪽 리스트 실시간 갱신
Deploy momo-erp / deploy (push) Successful in 2m35s
Deploy momo-erp / deploy (push) Successful in 2m35s
1) ITEM 라인 수량을 QtyInput 인라인 인풋으로 (REQUESTED/APPROVED 상태). onBlur/Enter 시 /api/m/orders/items/update 호출 → 자동 저장. 2) 비고(REMARK) 는 이미 onBlur 자동 저장이었음. 출고요청/출고완료 모두 editable 이라 동작. 3) ExtraRow(택배/용차) 의 V(저장) 버튼 제거. onBlur 시 자동 저장으로 변경 — label/단가/수량 어느 인풋이든 포커스 떠나면 자동 commit. 4) 모든 라인 수정 작업 (saveRemark/saveItemQty/upsertExtra/deleteExtra) 에서 onReload + onReloadList 동시 호출 → 왼쪽 발주 리스트의 합계도 즉시 반영. 부수: 운영 DB 의 모든 user_info 비밀번호를 '1' 로 reset (사용자 요청).
This commit is contained in:
@@ -456,24 +456,35 @@ function StatementPreview({
|
||||
});
|
||||
};
|
||||
|
||||
// 비고 저장
|
||||
// 비고 저장 — 명세표 + 왼쪽 리스트 모두 갱신
|
||||
const saveRemark = async (lineObjid: string, remark: string) => {
|
||||
const res = await fetch("/api/m/orders/items/remark", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ lineObjid, remark }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
if (j.success) { onReload(); onReloadList(); }
|
||||
else Swal.fire({ icon: "error", title: "비고 저장 실패", text: j.message });
|
||||
};
|
||||
|
||||
// ITEM 라인 수량 즉시 저장 — items/update API
|
||||
const saveItemQty = async (lineObjid: string, qty: number) => {
|
||||
const res = await fetch("/api/m/orders/items/update", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, lines: [{ objid: lineObjid, qty }] }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) { onReload(); onReloadList(); }
|
||||
else Swal.fire({ icon: "error", title: "수량 저장 실패", text: j.message });
|
||||
};
|
||||
|
||||
const upsertExtra = async (line: { objid?: string; kind: "DELIVERY" | "CHARTER"; label: string; unitPrice: number; qty: number }) => {
|
||||
const res = await fetch("/api/m/orders/lines/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, lines: [line] }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
if (j.success) { onReload(); onReloadList(); }
|
||||
else Swal.fire({ icon: "error", title: "저장 실패", text: j.message });
|
||||
};
|
||||
const deleteExtra = async (objid: string) => {
|
||||
@@ -484,7 +495,7 @@ function StatementPreview({
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, lines: [{ objid, kind: "CHARTER", unitPrice: 0, qty: 1, delete: true }] }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
if (j.success) { onReload(); onReloadList(); }
|
||||
else Swal.fire({ icon: "error", title: "삭제 실패", text: j.message });
|
||||
};
|
||||
const addNewExtra = (kind: "DELIVERY" | "CHARTER") => {
|
||||
@@ -671,7 +682,11 @@ function StatementPreview({
|
||||
<td className={`border border-slate-300 px-1.5 py-1 text-right js-no-export ${lack ? "text-rose-700 font-bold" : "text-slate-600"}`}>
|
||||
{isExtra ? "-" : fmt(it.STOCK_QTY)}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{fmt(it.QTY)}</td>
|
||||
<td className="border border-slate-300 px-1 py-0.5 text-right">
|
||||
{editable
|
||||
? <QtyInput initial={Number(it.QTY)} onSave={(q) => saveItemQty(it.OBJID, q)} />
|
||||
: fmt(it.QTY)}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{fmt(it.UNIT_PRICE)}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{fmt(it.SUPPLY_AMOUNT)}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{it.IS_TAX_FREE === "Y" ? "-" : fmt(it.VAT_AMOUNT)}</td>
|
||||
@@ -752,10 +767,15 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark }
|
||||
const total = Math.round(unitPrice * qty);
|
||||
const supply = Math.round(total / 1.1);
|
||||
const vat = total - supply;
|
||||
const isDelivery = line.KIND === "DELIVERY";
|
||||
|
||||
// onBlur 시 자동 저장 (값이 바뀐 경우만). V 버튼 제거.
|
||||
const commit = () => {
|
||||
const dirty = label !== (line.EXTRA_LABEL || line.ITEM_NAME)
|
||||
|| unitPrice !== Number(line.UNIT_PRICE)
|
||||
|| qty !== Number(line.QTY);
|
||||
const isDelivery = line.KIND === "DELIVERY";
|
||||
if (dirty && qty > 0 && unitPrice >= 0) onSave({ label, unitPrice, qty });
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className={isDelivery ? "bg-orange-50" : "bg-sky-50"}>
|
||||
@@ -767,6 +787,8 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark }
|
||||
<input
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }}
|
||||
className="h-6 px-1.5 border border-slate-200 rounded text-[11px] bg-white w-[calc(100%-50px)] inline"
|
||||
/>
|
||||
</td>
|
||||
@@ -775,11 +797,15 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark }
|
||||
<td className="border border-slate-300 px-1 py-1 text-right">
|
||||
<input type="number" min={1} value={qty}
|
||||
onChange={(e) => setQty(Number(e.target.value))}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }}
|
||||
className="w-full h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white" />
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1 py-1 text-right">
|
||||
<input type="number" min={0} value={unitPrice}
|
||||
onChange={(e) => setUnitPrice(Number(e.target.value))}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }}
|
||||
className="w-full h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white" />
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right tabular-nums">{Number(supply).toLocaleString("ko-KR")}</td>
|
||||
@@ -791,21 +817,9 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark }
|
||||
: <span className="text-slate-600 text-[10px]">{line.REMARK || ""}</span>}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1 py-1 text-center">
|
||||
<div className="inline-flex gap-0.5">
|
||||
{dirty && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSave({ label, unitPrice, qty })}
|
||||
className="text-emerald-700 hover:text-emerald-900"
|
||||
title="저장"
|
||||
>
|
||||
<Check size={12} />
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={onDelete} className="text-rose-500 hover:text-rose-700" title="삭제">
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -900,3 +914,24 @@ function RemarkInput({ initial, onSave }: { initial: string; onSave: (r: string)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ITEM 라인 수량 인라인 인풋 — onBlur / Enter 시 자동 저장
|
||||
function QtyInput({ initial, onSave }: { initial: number; onSave: (q: number) => void }) {
|
||||
const [val, setVal] = useState(String(initial));
|
||||
useEffect(() => { setVal(String(initial)); }, [initial]);
|
||||
const commit = () => {
|
||||
const n = Number(val);
|
||||
if (!Number.isFinite(n) || n <= 0) { setVal(String(initial)); return; }
|
||||
if (n === initial) return;
|
||||
onSave(n);
|
||||
};
|
||||
return (
|
||||
<input
|
||||
type="number" min={1} value={val}
|
||||
onChange={(e) => setVal(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }}
|
||||
className="w-16 h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user