aacbb62ad8
- 신규 테이블 5종 (운영 11133 → RPS 11134 DDL 1:1):
inventory_mgmt / inventory_mgmt_in / inventory_mgmt_out /
inventory_mgmt_out_master / inventory_mgmt_history
- 백엔드 /api/inventory-mng — 리스트·재고등록·자재이동·삭제·이력 +
불출의뢰 생성·접수·자재불출(재고 차감)·삭제. 채번 Rfw-YYYY-seq.
- 프론트 /COMPANY_16/material/{list, issue-request} +
StockRegister / MaterialMove / IssueRequestCreate /
InventoryHistory / IssueDispatch 다이얼로그 5종.
- AdminPageRenderer 등록 + /material/ prefix.
229 lines
9.0 KiB
TypeScript
229 lines
9.0 KiB
TypeScript
"use client";
|
|
|
|
// 자재관리 > 불출의뢰서 — 자재불출 처리 다이얼로그
|
|
// wace 1:1: materialRequestDetailPopUp.jsp / acceptInventoryRequestInfo.do
|
|
// 라인별 OUT_QTY / OUT_DATE / ACQ_USER 입력 후 OUTSTATUS='complete' 처리.
|
|
// 읽기전용(조회) 모드도 지원.
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Save, X } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Input } from "@/components/ui/input";
|
|
import { NumberInput } from "@/components/common/NumberInput";
|
|
import { DateInput } from "@/components/common/DateInput";
|
|
import { SmartSelect } from "@/components/common/SmartSelect";
|
|
import { inventoryMngApi, OptionItem } from "@/lib/api/inventoryMng";
|
|
|
|
interface LineRow {
|
|
objid: string;
|
|
part_no: string;
|
|
part_name: string;
|
|
material: string;
|
|
spec: string;
|
|
location: string;
|
|
sub_location: string;
|
|
request_qty: string;
|
|
out_qty: number | "";
|
|
out_date: string;
|
|
acq_user: string;
|
|
writer_name: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSaved: () => void;
|
|
masterObjid: string;
|
|
readOnly?: boolean; // 미접수/완료 상태에서 조회만
|
|
}
|
|
|
|
export function IssueDispatchDialog({ open, onClose, onSaved, masterObjid, readOnly }: Props) {
|
|
const [master, setMaster] = useState<any | null>(null);
|
|
const [rows, setRows] = useState<LineRow[]>([]);
|
|
const [userOpts, setUserOpts] = useState<OptionItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [bulkUser, setBulkUser] = useState("");
|
|
const [bulkDate, setBulkDate] = useState(new Date().toISOString().slice(0, 10));
|
|
|
|
useEffect(() => {
|
|
if (!open || !masterObjid) return;
|
|
setMaster(null);
|
|
setRows([]);
|
|
setLoading(true);
|
|
(async () => {
|
|
try {
|
|
const [detail, users] = await Promise.all([
|
|
inventoryMngApi.getIssue(masterObjid),
|
|
inventoryMngApi.users(),
|
|
]);
|
|
setMaster(detail.master);
|
|
setRows((detail.lines ?? []).map((l: any) => ({
|
|
objid: l.objid,
|
|
part_no: l.part_no ?? "",
|
|
part_name: l.part_name ?? "",
|
|
material: l.material ?? "",
|
|
spec: l.spec ?? "",
|
|
location: l.location ?? "",
|
|
sub_location: l.sub_location ?? "",
|
|
request_qty: l.request_qty ?? "0",
|
|
out_qty: l.out_qty ? Number(l.out_qty) : "",
|
|
out_date: l.out_date ?? "",
|
|
acq_user: l.acq_user ?? "",
|
|
writer_name: l.writer_name ?? "",
|
|
})));
|
|
setUserOpts(users);
|
|
} catch (e: any) {
|
|
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, [open, masterObjid]);
|
|
|
|
const handleRow = (idx: number, patch: Partial<LineRow>) =>
|
|
setRows(rs => rs.map((r, i) => i === idx ? { ...r, ...patch } : r));
|
|
|
|
const handleApplyBulk = () => {
|
|
setRows(rs => rs.map(r => ({
|
|
...r,
|
|
acq_user: bulkUser || r.acq_user,
|
|
out_date: bulkDate || r.out_date,
|
|
})));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
const lines = rows.filter(r => r.out_qty !== "" && Number(r.out_qty) > 0);
|
|
if (!lines.length) return toast.info("불출수량을 입력해주세요.");
|
|
for (const r of lines) {
|
|
if (Number(r.out_qty) > Number(r.request_qty)) {
|
|
return toast.warning(`${r.part_no}: 의뢰수량(${r.request_qty}) 초과는 불가합니다.`);
|
|
}
|
|
if (!r.out_date) return toast.info(`${r.part_no}: 인계일을 입력해주세요.`);
|
|
if (!r.acq_user) return toast.info(`${r.part_no}: 인수자를 선택해주세요.`);
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
await inventoryMngApi.dispatchIssue({
|
|
master_objid: masterObjid,
|
|
lines: lines.map(r => ({
|
|
objid: r.objid,
|
|
out_qty: Number(r.out_qty),
|
|
out_date: r.out_date,
|
|
acq_user: r.acq_user,
|
|
})),
|
|
});
|
|
toast.success("자재 불출 처리가 완료되었습니다.");
|
|
onSaved();
|
|
onClose();
|
|
} catch (e: any) {
|
|
toast.error(e?.response?.data?.message ?? e?.message ?? "불출 실패");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
<DialogContent className="max-w-6xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{readOnly ? "불출의뢰 상세" : "자재 불출"}
|
|
{master?.inventory_out_no ? ` — ${master.inventory_out_no}` : ""}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{readOnly
|
|
? "불출의뢰 라인을 조회합니다."
|
|
: "라인별 불출수량 / 인계일 / 인수자를 입력하고 저장하세요."}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{master && (
|
|
<div className="grid grid-cols-3 gap-2 text-xs text-muted-foreground">
|
|
<div>의뢰일: <b>{master.request_date ?? "-"}</b></div>
|
|
<div>의뢰자: <b>{master.request_user_name ?? master.request_id ?? "-"}</b></div>
|
|
<div>접수: <b>{master.reception_status === "reception" ? "접수" : "미접수"}</b>
|
|
{master.reception_date ? ` / ${master.reception_date}` : ""}</div>
|
|
<div className="col-span-3">특이사항: {master.remark || "-"}</div>
|
|
</div>
|
|
)}
|
|
|
|
{!readOnly && (
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<span>일괄적용:</span>
|
|
<span>인계일</span>
|
|
<div className="w-[140px]"><DateInput value={bulkDate} onChange={setBulkDate} /></div>
|
|
<span>인수자</span>
|
|
<div className="w-[180px]">
|
|
<SmartSelect options={userOpts} value={bulkUser} onValueChange={setBulkUser} />
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={handleApplyBulk}>적용</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="overflow-x-auto text-xs">
|
|
<table className="w-full border">
|
|
<thead className="bg-muted/50">
|
|
<tr>
|
|
<th className="border px-2 py-1">품번</th>
|
|
<th className="border px-2 py-1">품명</th>
|
|
<th className="border px-2 py-1">규격</th>
|
|
<th className="border px-2 py-1">재질</th>
|
|
<th className="border px-2 py-1">Location</th>
|
|
<th className="border px-2 py-1 text-right">의뢰수량</th>
|
|
<th className="border px-2 py-1 text-right">불출수량</th>
|
|
<th className="border px-2 py-1">인계일</th>
|
|
<th className="border px-2 py-1">인수자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading && <tr><td colSpan={9} className="border px-2 py-3 text-center">조회 중...</td></tr>}
|
|
{!loading && rows.length === 0 && (
|
|
<tr><td colSpan={9} className="border px-2 py-3 text-center">라인이 없습니다.</td></tr>
|
|
)}
|
|
{rows.map((r, i) => (
|
|
<tr key={i}>
|
|
<td className="border px-2 py-1">{r.part_no}</td>
|
|
<td className="border px-2 py-1">{r.part_name}</td>
|
|
<td className="border px-2 py-1">{r.spec}</td>
|
|
<td className="border px-2 py-1">{r.material}</td>
|
|
<td className="border px-2 py-1">{r.location}{r.sub_location ? ` / ${r.sub_location}` : ""}</td>
|
|
<td className="border px-2 py-1 text-right">{Number(r.request_qty ?? 0).toLocaleString()}</td>
|
|
<td className="border px-1 py-1 w-[120px]">
|
|
{readOnly
|
|
? <span>{r.out_qty === "" ? "" : Number(r.out_qty).toLocaleString()}</span>
|
|
: <NumberInput value={r.out_qty}
|
|
onChange={(v) => handleRow(i, { out_qty: v })} />}
|
|
</td>
|
|
<td className="border px-1 py-1 w-[140px]">
|
|
{readOnly ? <span>{r.out_date}</span> :
|
|
<DateInput value={r.out_date} onChange={(v) => handleRow(i, { out_date: v })} />}
|
|
</td>
|
|
<td className="border px-1 py-1 w-[160px]">
|
|
{readOnly ? <span>{r.acq_user}</span> :
|
|
<SmartSelect options={userOpts} value={r.acq_user}
|
|
onValueChange={(v) => handleRow(i, { acq_user: v })} />}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button size="sm" variant="ghost" onClick={onClose} disabled={saving}>
|
|
<X className="h-3.5 w-3.5 mr-1" /> 닫기
|
|
</Button>
|
|
{!readOnly && (
|
|
<Button size="sm" onClick={handleSave} disabled={saving || loading}>
|
|
<Save className="h-3.5 w-3.5 mr-1" /> {saving ? "처리중..." : "자재불출"}
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|