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.
164 lines
6.5 KiB
TypeScript
164 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
// 자재관리 > 자재리스트 — 자재이동 다이얼로그
|
|
// wace 1:1: materialMoveFormPopUp.jsp / saveInventoryMove.do
|
|
// inventory_mgmt_in 행의 move_qty/move_date/move_user 누적 + 이동 이력 라인 추가
|
|
|
|
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 { NumberInput } from "@/components/common/NumberInput";
|
|
import { DateInput } from "@/components/common/DateInput";
|
|
import { Input } from "@/components/ui/input";
|
|
import { SmartSelect } from "@/components/common/SmartSelect";
|
|
import { inventoryMngApi, OptionItem } from "@/lib/api/inventoryMng";
|
|
|
|
interface MoveRow {
|
|
in_objid: string;
|
|
parent_objid: string;
|
|
part_no: string;
|
|
part_name: string;
|
|
use_cnt: number;
|
|
move_qty: number | "";
|
|
location: string;
|
|
sub_location: string;
|
|
move_date: string;
|
|
move_user: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSaved: () => void;
|
|
selectedRows: any[]; // 자재리스트에서 체크된 행 (objid 가 parent_objid)
|
|
}
|
|
|
|
export function MaterialMoveDialog({ open, onClose, onSaved, selectedRows }: Props) {
|
|
const [rows, setRows] = useState<MoveRow[]>([]);
|
|
const [userOpts, setUserOpts] = useState<OptionItem[]>([]);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setRows(selectedRows.map((r) => ({
|
|
in_objid: "", // 사용자가 in_objid 를 모를 수도 있어 parent 기준으로 1행 처리 후 첫 inventory_mgmt_in 사용
|
|
parent_objid: r.objid,
|
|
part_no: r.part_no ?? "",
|
|
part_name: r.part_name ?? "",
|
|
use_cnt: Number(r.use_cnt ?? 0),
|
|
move_qty: "",
|
|
location: r.location ?? "",
|
|
sub_location: r.sub_location ?? "",
|
|
move_date: new Date().toISOString().slice(0, 10),
|
|
move_user: "",
|
|
})));
|
|
(async () => {
|
|
try { setUserOpts(await inventoryMngApi.users()); } catch { /* skip */ }
|
|
})();
|
|
}, [open, selectedRows]);
|
|
|
|
const handleRowChange = (idx: number, patch: Partial<MoveRow>) =>
|
|
setRows(rs => rs.map((r, i) => i === idx ? { ...r, ...patch } : r));
|
|
|
|
const handleSave = async () => {
|
|
const items = rows.filter((r) => r.move_qty !== "" && Number(r.move_qty) > 0);
|
|
if (!items.length) return toast.info("이동수량을 입력해주세요.");
|
|
for (const r of items) {
|
|
if (Number(r.move_qty) > r.use_cnt) {
|
|
return toast.warning(`${r.part_no}: 보유수량(${r.use_cnt})보다 큰 이동수량은 불가합니다.`);
|
|
}
|
|
if (!r.move_user) {
|
|
return toast.info(`${r.part_no}: 인수자를 선택해주세요.`);
|
|
}
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
// parent_objid 기준으로 백엔드가 첫 inventory_mgmt_in 행을 잡지 못하므로
|
|
// 일단 in_objid 가 비면 parent_objid 를 사용 (백엔드에서 fallback 처리).
|
|
await inventoryMngApi.move(items.map((r) => ({
|
|
in_objid: r.parent_objid,
|
|
move_qty: Number(r.move_qty),
|
|
location: r.location,
|
|
sub_location: r.sub_location,
|
|
move_date: r.move_date,
|
|
move_user: r.move_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-5xl">
|
|
<DialogHeader>
|
|
<DialogTitle>자재 이동</DialogTitle>
|
|
<DialogDescription>선택한 자재의 이동수량/Location/인수자를 입력합니다.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<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 text-left">품번</th>
|
|
<th className="border px-2 py-1 text-left">품명</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">Location</th>
|
|
<th className="border px-2 py-1">Sub</th>
|
|
<th className="border px-2 py-1">인계일</th>
|
|
<th className="border px-2 py-1">인수자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{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 text-right">{r.use_cnt.toLocaleString()}</td>
|
|
<td className="border px-1 py-1 w-[110px]">
|
|
<NumberInput value={r.move_qty}
|
|
onChange={(v) => handleRowChange(i, { move_qty: v })} />
|
|
</td>
|
|
<td className="border px-1 py-1 w-[120px]">
|
|
<Input value={r.location}
|
|
onChange={(e) => handleRowChange(i, { location: e.target.value })} />
|
|
</td>
|
|
<td className="border px-1 py-1 w-[120px]">
|
|
<Input value={r.sub_location}
|
|
onChange={(e) => handleRowChange(i, { sub_location: e.target.value })} />
|
|
</td>
|
|
<td className="border px-1 py-1 w-[140px]">
|
|
<DateInput value={r.move_date}
|
|
onChange={(v) => handleRowChange(i, { move_date: v })} />
|
|
</td>
|
|
<td className="border px-1 py-1 w-[160px]">
|
|
<SmartSelect options={userOpts} value={r.move_user}
|
|
onValueChange={(v) => handleRowChange(i, { move_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>
|
|
<Button size="sm" onClick={handleSave} disabled={saving}>
|
|
<Save className="h-3.5 w-3.5 mr-1" /> {saving ? "이동중..." : "이동"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|