Files
wace_rps/frontend/components/material/MaterialMoveDialog.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

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>
);
}