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.
185 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|