e51f5f7b69
- backend purchaseInboundService 신설 — getInboundFormInit / saveInboundForm
(arrival_plan UPSERT 트랜잭션) / saveDeadlineInfo (8필드 일괄 UPDATE) /
closeArrival (이미 마감된 건 차단) + listWarehouseOptions / listAcctCodeOptions
- backend routes — GET /inbound-form/:pomObjid / POST /inbound-form/save /
POST /arrival/deadline / POST /arrival/close + 옵션 2개
- InboundFormDialog 신설 — wace deliveryAcceptanceFormPopUp_new.jsp 1:1
(좌 발주품목 read-only + 우 차수별 입고입력 + 미입고 일괄적용)
- DeadlineInfoDialog 신설 — wace swal 모달 1:1 (8필드 일괄, 단건 시 prefill)
- inbound 페이지 입고등록 / inbound-by-date 마감정보입력+매입마감 연결
- 입고등록 master SELECT 함정 수정 — RPS 에 POM.delivery_status 없어 reception_status fallback
- DataGrid 다중 frozen 누적 left 계산 인프라 추가 (frozenLeftPx props 보강)
— shadcn Table 기반이라 진짜 column pinning 불가 (자연 위치 도달 후 sticky),
입고 3페이지의 frozen 부여는 일단 제거. 진짜 pinning 은 별도 작업
166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
// 구매관리 > 입고일별 입고관리 > 마감정보입력 다이얼로그
|
|
// wace 원본: purchaseCloseList.jsp:75-246 swal 모달 1:1
|
|
// - 다중 행 선택 → 8필드 일괄 UPDATE
|
|
// - 단건 선택 시 그리드 행에서 기존 값 자동 채움 (호출자가 prefill 로 전달)
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter, DialogHeader } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
|
import { DateInput } from "@/components/common/DateInput";
|
|
import { NumberInput } from "@/components/common/NumberInput";
|
|
import { purchaseApi, DeadlineInfoPayload } from "@/lib/api/purchase";
|
|
|
|
// wace purchaseCloseList.jsp:490-499 하드코딩 옵션
|
|
const FOREIGN_TYPE_OPTS: SmartSelectOption[] = [
|
|
{ code: "0001220", label: "국내" },
|
|
{ code: "0001221", label: "해외" },
|
|
];
|
|
const TAX_TYPE_OPTS: SmartSelectOption[] = [
|
|
{ code: "0900218", label: "과세매입" },
|
|
{ code: "0900219", label: "영세매입" },
|
|
{ code: "0900220", label: "수입" },
|
|
];
|
|
|
|
export interface DeadlinePrefill {
|
|
taxType?: string;
|
|
taxInvoiceDate?: string;
|
|
exportDeclNo?: string;
|
|
loadingDate?: string;
|
|
foreignType?: string;
|
|
duty?: string;
|
|
exchangeRate?: string;
|
|
importVat?: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSaved?: () => void;
|
|
/** 선택된 arrival_plan.OBJID 목록 */
|
|
objIds: string[];
|
|
/** 단건 선택 시 기존 값 자동 채움 */
|
|
prefill?: DeadlinePrefill;
|
|
}
|
|
|
|
export function DeadlineInfoDialog({ open, onClose, onSaved, objIds, prefill }: Props) {
|
|
const [saving, setSaving] = useState(false);
|
|
const [form, setForm] = useState<DeadlinePrefill>({});
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setForm({
|
|
taxType: prefill?.taxType ?? "",
|
|
taxInvoiceDate: prefill?.taxInvoiceDate ?? "",
|
|
exportDeclNo: prefill?.exportDeclNo ?? "",
|
|
loadingDate: prefill?.loadingDate ?? "",
|
|
foreignType: prefill?.foreignType ?? "",
|
|
duty: prefill?.duty ?? "",
|
|
exchangeRate: prefill?.exchangeRate ?? "",
|
|
importVat: prefill?.importVat ?? "",
|
|
});
|
|
}, [open, prefill]);
|
|
|
|
const handleSave = async () => {
|
|
if (objIds.length === 0) {
|
|
toast.warning("선택된 입고건이 없습니다");
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const payload: DeadlineInfoPayload = {
|
|
objIds,
|
|
taxType: form.taxType ?? "",
|
|
taxInvoiceDate: form.taxInvoiceDate,
|
|
exportDeclNo: form.exportDeclNo,
|
|
loadingDate: form.loadingDate,
|
|
foreignType: form.foreignType,
|
|
duty: form.duty,
|
|
exchangeRate: form.exchangeRate,
|
|
importVat: form.importVat,
|
|
};
|
|
const r = await purchaseApi.saveArrivalDeadline(payload);
|
|
toast.success(`마감정보 저장 완료 (${r.updated}건)`);
|
|
onSaved?.();
|
|
onClose();
|
|
} catch (e: any) {
|
|
toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(v) => { if (!v && !saving) onClose(); }}>
|
|
<DialogContent className="max-w-[600px] bg-white">
|
|
<DialogHeader>
|
|
<DialogTitle>마감정보입력</DialogTitle>
|
|
<DialogDescription>선택된 {objIds.length}건의 입고건에 마감정보를 일괄 적용합니다</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-2">
|
|
<FieldRow label="국내/해외">
|
|
<SmartSelect options={FOREIGN_TYPE_OPTS} value={form.foreignType ?? ""}
|
|
onValueChange={(v) => setForm({ ...form, foreignType: v })} />
|
|
</FieldRow>
|
|
<FieldRow label="환율">
|
|
<NumberInput value={form.exchangeRate || ""} decimals={2}
|
|
onChange={(v) => setForm({ ...form, exchangeRate: String(v) })}
|
|
className="h-8 text-[12px]" />
|
|
</FieldRow>
|
|
<FieldRow label="과세구분">
|
|
<SmartSelect options={TAX_TYPE_OPTS} value={form.taxType ?? ""}
|
|
onValueChange={(v) => setForm({ ...form, taxType: v })} />
|
|
</FieldRow>
|
|
<FieldRow label="세금계산서발행일">
|
|
<DateInput value={form.taxInvoiceDate ?? ""}
|
|
onChange={(v) => setForm({ ...form, taxInvoiceDate: v })} />
|
|
</FieldRow>
|
|
<FieldRow label="수출신고필증신고번호">
|
|
<Input value={form.exportDeclNo ?? ""}
|
|
onChange={(e) => setForm({ ...form, exportDeclNo: e.target.value })}
|
|
className="h-8 text-[12px]" />
|
|
</FieldRow>
|
|
<FieldRow label="선적일자">
|
|
<DateInput value={form.loadingDate ?? ""}
|
|
onChange={(v) => setForm({ ...form, loadingDate: v })} />
|
|
</FieldRow>
|
|
<FieldRow label="관세">
|
|
<NumberInput value={form.duty || ""} decimals={0}
|
|
onChange={(v) => setForm({ ...form, duty: String(v) })}
|
|
className="h-8 text-[12px]" />
|
|
</FieldRow>
|
|
<FieldRow label="수입부가세">
|
|
<NumberInput value={form.importVat || ""} decimals={0}
|
|
onChange={(v) => setForm({ ...form, importVat: String(v) })}
|
|
className="h-8 text-[12px]" />
|
|
</FieldRow>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose} disabled={saving}>닫기</Button>
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null}
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function FieldRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="grid grid-cols-[180px_1fr] items-center gap-2 border-b border-gray-100 py-1.5">
|
|
<Label className="text-[12px] font-semibold text-right pr-3">{label}</Label>
|
|
<div>{children}</div>
|
|
</div>
|
|
);
|
|
}
|