feat(procurements+inbounds): 발주서 납품조건 1번 삭제 + 입고 체크리스트
Deploy momo-erp / deploy (push) Successful in 2m16s
Deploy momo-erp / deploy (push) Successful in 2m16s
매입 발주서 납품조건: - '상기 품목의 납기 지연 시...' 1번 항목 삭제. 검수/수량변경 2개만 유지. 입고 처리 체크리스트 (memo 컬럼에 텍스트로 저장 — 스키마 변경 없음): 1) 발주수량 vs 입고수량 일치 (체크박스) 2) 1카톤 N개 일치 (체크박스 + 카톤 단위 입력) 3) 소비기한 (date) 4) 물류창고 입고 최종완료자 (물류팀 4명 select) 5) 특이사항 (textarea) 저장 시 momo_inbounds.memo 에 줄단위 텍스트로 박힘: [수량 일치] Y ✓ [카톤 일치] Y ✓ (1카톤 12개) [소비기한] 2026-11-30 [입고완료자] 이효철 (물류총괄) [특이사항] ...
This commit is contained in:
@@ -42,6 +42,23 @@ export default function InboundsPage() {
|
||||
// 라인별 입력 (창고/입고수량/불량수량)
|
||||
const [inputs, setInputs] = useState<Record<string, { whObjid: string; qtyNormal: number; qtyDefect: number }>>({});
|
||||
|
||||
// 입고 체크리스트
|
||||
const [checklist, setChecklist] = useState({
|
||||
qtyMatch: false, // 1) 발주수량/입고수량 일치
|
||||
cartonMatch: false, // 2) 1카톤 N개 일치
|
||||
cartonSize: "", // 카톤 단위
|
||||
expiryDate: "", // 3) 소비기한
|
||||
completedBy: "", // 4) 물류팀 입고 최종완료자
|
||||
remark: "", // 5) 특이건 메모
|
||||
});
|
||||
// 물류팀 4명 — 임직원(user_type='A') 중 momo4763/momo7529 외 2명까지
|
||||
const LOGISTICS = [
|
||||
{ id: "momo4763", name: "이효철 (물류총괄)" },
|
||||
{ id: "momo7529", name: "유우형 (물류팀장)" },
|
||||
{ id: "momo9431", name: "강상익 (김포지사 총괄)" },
|
||||
{ id: "momo5315", name: "배연진 (경영팀장)" },
|
||||
];
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const body: Record<string, unknown> = {};
|
||||
// 입고 화면은 REQUESTED + PARTIAL 만 보이게
|
||||
@@ -150,6 +167,15 @@ export default function InboundsPage() {
|
||||
});
|
||||
if (!ok.isConfirmed) return;
|
||||
|
||||
// 체크리스트 텍스트화 — memo 에 저장 (스키마 변경 없이)
|
||||
const checklistMemo = [
|
||||
`[수량 일치] ${checklist.qtyMatch ? "Y ✓" : "N"}`,
|
||||
`[카톤 일치] ${checklist.cartonMatch ? `Y ✓ (1카톤 ${checklist.cartonSize || "?"}개)` : "N"}`,
|
||||
`[소비기한] ${checklist.expiryDate || "-"}`,
|
||||
`[입고완료자] ${checklist.completedBy || "-"}`,
|
||||
`[특이사항] ${checklist.remark || "-"}`,
|
||||
].join("\n");
|
||||
|
||||
setBusy(true);
|
||||
let successCnt = 0, failCnt = 0;
|
||||
const errors: string[] = [];
|
||||
@@ -161,6 +187,7 @@ export default function InboundsPage() {
|
||||
procObjid: detail.proc.OBJID,
|
||||
whObjid,
|
||||
lines: whLines,
|
||||
memo: checklistMemo,
|
||||
}),
|
||||
});
|
||||
const j = await res.json();
|
||||
@@ -281,6 +308,9 @@ export default function InboundsPage() {
|
||||
warehouses={warehouses}
|
||||
inputs={inputs}
|
||||
onUpdate={updateInput}
|
||||
checklist={checklist}
|
||||
onChecklistChange={(patch) => setChecklist((p) => ({ ...p, ...patch }))}
|
||||
logistics={LOGISTICS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -290,11 +320,18 @@ export default function InboundsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function InboundForm({ detail, warehouses, inputs, onUpdate }: {
|
||||
interface Checklist {
|
||||
qtyMatch: boolean; cartonMatch: boolean; cartonSize: string;
|
||||
expiryDate: string; completedBy: string; remark: string;
|
||||
}
|
||||
function InboundForm({ detail, warehouses, inputs, onUpdate, checklist, onChecklistChange, logistics }: {
|
||||
detail: { proc: ProcDetail; items: ProcLine[] };
|
||||
warehouses: Warehouse[];
|
||||
inputs: Record<string, { whObjid: string; qtyNormal: number; qtyDefect: number }>;
|
||||
onUpdate: (lineObjid: string, patch: Partial<{ whObjid: string; qtyNormal: number; qtyDefect: number }>) => void;
|
||||
checklist: Checklist;
|
||||
onChecklistChange: (patch: Partial<Checklist>) => void;
|
||||
logistics: { id: string; name: string }[];
|
||||
}) {
|
||||
const editable = detail.proc.STATUS === "PAID" || detail.proc.STATUS === "PARTIAL";
|
||||
return (
|
||||
@@ -386,10 +423,65 @@ function InboundForm({ detail, warehouses, inputs, onUpdate }: {
|
||||
</table>
|
||||
|
||||
{editable && (
|
||||
<>
|
||||
<div className="mt-3 text-[11px] text-slate-500">
|
||||
※ 정상 입고 + 불량은 <b>남은 수량 이하</b>로만 입력 가능합니다. 0으로 두면 그 라인은 입고하지 않습니다.<br />
|
||||
※ 일부 라인만 입고하면 발주서가 <span className="inline-block px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 font-bold">입고중</span>으로 표시되고, 나중에 다시 들어와 마저 입고할 수 있어요.
|
||||
</div>
|
||||
|
||||
{/* 입고 체크리스트 — memo 컬럼에 함께 저장 */}
|
||||
<div className="mt-4 border border-emerald-200 bg-emerald-50/40 rounded-lg p-3 space-y-2">
|
||||
<div className="font-bold text-[12px] text-emerald-800 mb-1">📋 입고 체크리스트</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-[12px] cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 accent-emerald-600"
|
||||
checked={checklist.qtyMatch}
|
||||
onChange={(e) => onChecklistChange({ qtyMatch: e.target.checked })} />
|
||||
<span>1) 발주수량과 입고수량이 일치하나요?</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2 text-[12px]">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 accent-emerald-600"
|
||||
checked={checklist.cartonMatch}
|
||||
onChange={(e) => onChecklistChange({ cartonMatch: e.target.checked })} />
|
||||
<span>2) 1카톤</span>
|
||||
</label>
|
||||
<input type="number" min={0}
|
||||
value={checklist.cartonSize}
|
||||
onChange={(e) => onChecklistChange({ cartonSize: e.target.value })}
|
||||
placeholder="개수"
|
||||
className="w-20 h-7 px-2 border border-slate-300 rounded text-[11px] text-right tabular-nums" />
|
||||
<span>개 일치하나요?</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[12px]">
|
||||
<span className="w-44">3) 소비기한</span>
|
||||
<input type="date" value={checklist.expiryDate}
|
||||
onChange={(e) => onChecklistChange({ expiryDate: e.target.value })}
|
||||
className="h-7 px-2 border border-slate-300 rounded text-[11px]" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[12px]">
|
||||
<span className="w-44">4) 물류창고 입고 최종완료자</span>
|
||||
<select value={checklist.completedBy}
|
||||
onChange={(e) => onChecklistChange({ completedBy: e.target.value })}
|
||||
className="h-7 px-2 border border-slate-300 rounded text-[11px] bg-white">
|
||||
<option value="">-- 선택 --</option>
|
||||
{logistics.map((p) => <option key={p.id} value={p.name}>{p.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="text-[12px]">
|
||||
<div className="mb-1">5) 특이사항</div>
|
||||
<textarea rows={2}
|
||||
value={checklist.remark}
|
||||
onChange={(e) => onChecklistChange({ remark: e.target.value })}
|
||||
placeholder="특이건이 있으면 입력"
|
||||
className="w-full px-2 py-1 border border-slate-300 rounded text-[11px] resize-none" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -481,7 +481,6 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onSetTerm, o
|
||||
|
||||
<p className="mt-4 font-semibold text-[12px]">2. 납품조건</p>
|
||||
<ol className="text-[11px] mt-1 space-y-1 leading-relaxed list-decimal pl-5">
|
||||
<li>상기 품목의 납기 지연 시, 지연일수 매 1일에 대하여 미납 금액의 3/1000을 납품대금 지불 시 우선 공제한다.</li>
|
||||
<li>납품된 물품은 당사의 지정인에게 검수를 받아야 하며, 부적합품은 즉시 납품자의 비용으로 반출하여야 한다.</li>
|
||||
<li>상기 수량 및 규격은 당사의 사정에 의하여 변경될 수 있으며, 납품자는 이에 대하여 이의를 제기할 수 없다.</li>
|
||||
</ol>
|
||||
|
||||
Reference in New Issue
Block a user