Files
wace_rps/frontend/components/material/IssueRequestCreateDialog.tsx
T
hjjeong aacbb62ad8 자재관리 2메뉴 풀-CRUD + 액션 (자재리스트 + 불출의뢰서)
- 신규 테이블 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.
2026-05-19 11:25:15 +09:00

185 lines
7.1 KiB
TypeScript

"use client";
// 자재관리 > 자재리스트 — 불출의뢰 등록 다이얼로그
// wace 1:1: materialRequestFormPopUp.jsp / saveInventoryRequest.do
// 선택된 자재(inventory_mgmt) 후보를 불러와 request_qty 입력 → 마스터+라인 저장
// inventory_out_no 채번: Rfw-YYYY-seq
import React, { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Save, X } from "lucide-react";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { NumberInput } from "@/components/common/NumberInput";
import { DateInput } from "@/components/common/DateInput";
import { inventoryMngApi } from "@/lib/api/inventoryMng";
interface CandidateRow {
objid: string;
project_no: string;
unit: string;
unit_name: string;
part_no: string;
part_name: string;
material: string;
spec: string;
location: string;
sub_location: string;
use_cnt: number;
request_qty: number | "";
}
interface Props {
open: boolean;
onClose: () => void;
onSaved: () => void;
parentObjids: string[]; // 자재리스트에서 선택된 inventory_mgmt.objid
}
export function IssueRequestCreateDialog({ open, onClose, onSaved, parentObjids }: Props) {
const [rows, setRows] = useState<CandidateRow[]>([]);
const [remark, setRemark] = useState("");
const [requestDate, setRequestDate] = useState(new Date().toISOString().slice(0, 10));
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open || !parentObjids.length) return;
setRemark("");
setRequestDate(new Date().toISOString().slice(0, 10));
(async () => {
setLoading(true);
try {
const list = await inventoryMngApi.candidates(parentObjids);
setRows(list.map((r) => ({
objid: r.objid,
project_no: r.project_no ?? "",
unit: r.unit ?? "",
unit_name: r.unit_name ?? "",
part_no: r.part_no ?? "",
part_name: r.part_name ?? "",
material: r.material ?? "",
spec: r.spec ?? "",
location: r.location ?? "",
sub_location: r.sub_location ?? "",
use_cnt: Number(r.use_cnt ?? 0),
request_qty: "",
})));
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "후보 조회 실패");
} finally {
setLoading(false);
}
})();
}, [open, parentObjids]);
const handleRow = (idx: number, patch: Partial<CandidateRow>) =>
setRows(rs => rs.map((r, i) => i === idx ? { ...r, ...patch } : r));
const handleSave = async () => {
const targets = rows.filter((r) => r.request_qty !== "" && Number(r.request_qty) > 0);
if (!targets.length) return toast.info("불출의뢰수량을 입력해주세요.");
for (const r of targets) {
if (Number(r.request_qty) > r.use_cnt) {
return toast.warning(`${r.part_no}: 보유수량(${r.use_cnt}) 초과는 불가합니다.`);
}
}
setSaving(true);
try {
const res = await inventoryMngApi.saveIssue({
request_date: requestDate,
remark,
lines: targets.map((r) => ({
parent_objid: r.objid,
request_qty: Number(r.request_qty),
unit: r.unit,
})),
});
toast.success(`불출의뢰가 생성되었습니다. (${res.inventory_out_no})`);
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> </DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="space-y-1">
<Label></Label>
<DateInput value={requestDate} onChange={setRequestDate} />
</div>
<div />
<div className="col-span-2 space-y-1">
<Label></Label>
<Textarea rows={2} value={remark} onChange={(e) => setRemark(e.target.value)} />
</div>
</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"></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>
</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.project_no}</td>
<td className="border px-2 py-1">{r.unit_name || r.unit}</td>
<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">{r.use_cnt.toLocaleString()}</td>
<td className="border px-1 py-1 w-[130px]">
<NumberInput value={r.request_qty}
onChange={(v) => handleRow(i, { request_qty: 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>
<Button size="sm" onClick={handleSave} disabled={saving || loading || !rows.length}>
<Save className="h-3.5 w-3.5 mr-1" /> {saving ? "저장중..." : "불출의뢰"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}