525 lines
18 KiB
TypeScript
525 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { OrderCustomerSearch } from "./OrderCustomerSearch";
|
|
import { OrderItemRepeaterTable } from "./OrderItemRepeaterTable";
|
|
import { toast } from "sonner";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface OrderRegistrationModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSuccess?: () => void;
|
|
}
|
|
|
|
export function OrderRegistrationModal({
|
|
open,
|
|
onOpenChange,
|
|
onSuccess,
|
|
}: OrderRegistrationModalProps) {
|
|
// 입력 방식
|
|
const [inputMode, setInputMode] = useState<string>("customer_first");
|
|
|
|
// 판매 유형 (국내/해외)
|
|
const [salesType, setSalesType] = useState<string>("domestic");
|
|
|
|
// 단가 기준 (기준단가/거래처별단가)
|
|
const [priceType, setPriceType] = useState<string>("standard");
|
|
|
|
// 폼 데이터
|
|
const [formData, setFormData] = useState<any>({
|
|
customerCode: "",
|
|
customerName: "",
|
|
contactPerson: "",
|
|
deliveryDestination: "",
|
|
deliveryAddress: "",
|
|
deliveryDate: "",
|
|
memo: "",
|
|
// 무역 정보 (해외 판매 시)
|
|
incoterms: "",
|
|
paymentTerms: "",
|
|
currency: "KRW",
|
|
portOfLoading: "",
|
|
portOfDischarge: "",
|
|
hsCode: "",
|
|
});
|
|
|
|
// 선택된 품목 목록
|
|
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
|
|
|
// 저장 중
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// 저장 처리
|
|
const handleSave = async () => {
|
|
try {
|
|
// 유효성 검사
|
|
if (!formData.customerCode) {
|
|
toast.error("거래처를 선택해주세요");
|
|
return;
|
|
}
|
|
|
|
if (selectedItems.length === 0) {
|
|
toast.error("품목을 추가해주세요");
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
|
|
// 수주 등록 API 호출
|
|
const orderData: any = {
|
|
inputMode,
|
|
salesType,
|
|
priceType,
|
|
customerCode: formData.customerCode,
|
|
contactPerson: formData.contactPerson,
|
|
deliveryDestination: formData.deliveryDestination,
|
|
deliveryAddress: formData.deliveryAddress,
|
|
deliveryDate: formData.deliveryDate,
|
|
items: selectedItems,
|
|
memo: formData.memo,
|
|
};
|
|
|
|
// 해외 판매 시 무역 정보 추가
|
|
if (salesType === "export") {
|
|
orderData.tradeInfo = {
|
|
incoterms: formData.incoterms,
|
|
paymentTerms: formData.paymentTerms,
|
|
currency: formData.currency,
|
|
portOfLoading: formData.portOfLoading,
|
|
portOfDischarge: formData.portOfDischarge,
|
|
hsCode: formData.hsCode,
|
|
};
|
|
}
|
|
|
|
const response = await apiClient.post("/orders", orderData);
|
|
|
|
if (response.data.success) {
|
|
toast.success("수주가 등록되었습니다");
|
|
onOpenChange(false);
|
|
onSuccess?.();
|
|
|
|
// 폼 초기화
|
|
resetForm();
|
|
} else {
|
|
toast.error(response.data.message || "수주 등록에 실패했습니다");
|
|
}
|
|
} catch (error: any) {
|
|
console.error("수주 등록 오류:", error);
|
|
toast.error(
|
|
error.response?.data?.message || "수주 등록 중 오류가 발생했습니다"
|
|
);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
// 취소 처리
|
|
const handleCancel = () => {
|
|
onOpenChange(false);
|
|
resetForm();
|
|
};
|
|
|
|
// 폼 초기화
|
|
const resetForm = () => {
|
|
setInputMode("customer_first");
|
|
setSalesType("domestic");
|
|
setPriceType("standard");
|
|
setFormData({
|
|
customerCode: "",
|
|
customerName: "",
|
|
contactPerson: "",
|
|
deliveryDestination: "",
|
|
deliveryAddress: "",
|
|
deliveryDate: "",
|
|
memo: "",
|
|
incoterms: "",
|
|
paymentTerms: "",
|
|
currency: "KRW",
|
|
portOfLoading: "",
|
|
portOfDischarge: "",
|
|
hsCode: "",
|
|
});
|
|
setSelectedItems([]);
|
|
};
|
|
|
|
// 전체 금액 계산
|
|
const totalAmount = selectedItems.reduce(
|
|
(sum, item) => sum + (item.amount || 0),
|
|
0
|
|
);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[1200px] max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">수주 등록</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
새로운 수주를 등록합니다
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6">
|
|
{/* 상단 셀렉트 박스 3개 */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
{/* 입력 방식 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="inputMode" className="text-xs sm:text-sm flex items-center gap-1">
|
|
<span className="text-amber-500">📝</span> 입력 방식
|
|
</Label>
|
|
<Select value={inputMode} onValueChange={setInputMode}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="입력 방식 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="customer_first">거래처 우선</SelectItem>
|
|
<SelectItem value="quotation">견대 방식</SelectItem>
|
|
<SelectItem value="unit_price">단가 방식</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 판매 유형 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="salesType" className="text-xs sm:text-sm flex items-center gap-1">
|
|
<span className="text-blue-500">🌏</span> 판매 유형
|
|
</Label>
|
|
<Select value={salesType} onValueChange={setSalesType}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="판매 유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="domestic">국내 판매</SelectItem>
|
|
<SelectItem value="export">해외 판매</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 단가 기준 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="priceType" className="text-xs sm:text-sm flex items-center gap-1">
|
|
<span className="text-green-500">💰</span> 단가 방식
|
|
</Label>
|
|
<Select value={priceType} onValueChange={setPriceType}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="단가 방식 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="standard">기준 단가</SelectItem>
|
|
<SelectItem value="customer">거래처별 단가</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 거래처 정보 (항상 표시) */}
|
|
{inputMode === "customer_first" && (
|
|
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 space-y-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-700">
|
|
<span>🏢</span>
|
|
<span>거래처 정보</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* 거래처 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">거래처 *</Label>
|
|
<OrderCustomerSearch
|
|
value={formData.customerCode}
|
|
onChange={(code, fullData) => {
|
|
setFormData({
|
|
...formData,
|
|
customerCode: code || "",
|
|
customerName: fullData?.customer_name || "",
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 담당자 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="contactPerson" className="text-xs sm:text-sm">
|
|
담당자
|
|
</Label>
|
|
<Input
|
|
id="contactPerson"
|
|
placeholder="담당자"
|
|
value={formData.contactPerson}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, contactPerson: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 납품처 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="deliveryDestination" className="text-xs sm:text-sm">
|
|
납품처
|
|
</Label>
|
|
<Input
|
|
id="deliveryDestination"
|
|
placeholder="납품처"
|
|
value={formData.deliveryDestination}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, deliveryDestination: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 납품장소 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="deliveryAddress" className="text-xs sm:text-sm">
|
|
납품장소
|
|
</Label>
|
|
<Input
|
|
id="deliveryAddress"
|
|
placeholder="납품장소"
|
|
value={formData.deliveryAddress}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, deliveryAddress: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{inputMode === "quotation" && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">견대 번호 *</Label>
|
|
<Input
|
|
placeholder="견대 번호를 입력하세요"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{inputMode === "unit_price" && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">단가 방식 설정</Label>
|
|
<Input
|
|
placeholder="단가 정보 입력"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 추가된 품목 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">추가된 품목</Label>
|
|
<OrderItemRepeaterTable
|
|
value={selectedItems}
|
|
onChange={setSelectedItems}
|
|
/>
|
|
</div>
|
|
|
|
{/* 전체 금액 표시 */}
|
|
{selectedItems.length > 0 && (
|
|
<div className="flex justify-end">
|
|
<div className="text-sm sm:text-base font-semibold">
|
|
전체 금액: {totalAmount.toLocaleString()}원
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 무역 정보 (해외 판매 시에만 표시) */}
|
|
{salesType === "export" && (
|
|
<div className="rounded-lg border border-blue-200 bg-blue-50/50 p-4 space-y-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-blue-700">
|
|
<span>🌏</span>
|
|
<span>무역 정보</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
{/* 인코텀즈 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="incoterms" className="text-xs sm:text-sm">
|
|
인코텀즈
|
|
</Label>
|
|
<Select
|
|
value={formData.incoterms}
|
|
onValueChange={(value) =>
|
|
setFormData({ ...formData, incoterms: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="EXW">EXW</SelectItem>
|
|
<SelectItem value="FOB">FOB</SelectItem>
|
|
<SelectItem value="CIF">CIF</SelectItem>
|
|
<SelectItem value="DDP">DDP</SelectItem>
|
|
<SelectItem value="DAP">DAP</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 결제 조건 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="paymentTerms" className="text-xs sm:text-sm">
|
|
결제 조건
|
|
</Label>
|
|
<Select
|
|
value={formData.paymentTerms}
|
|
onValueChange={(value) =>
|
|
setFormData({ ...formData, paymentTerms: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="advance">선결제</SelectItem>
|
|
<SelectItem value="cod">착불</SelectItem>
|
|
<SelectItem value="lc">신용장(L/C)</SelectItem>
|
|
<SelectItem value="net30">NET 30</SelectItem>
|
|
<SelectItem value="net60">NET 60</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 통화 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="currency" className="text-xs sm:text-sm">
|
|
통화
|
|
</Label>
|
|
<Select
|
|
value={formData.currency}
|
|
onValueChange={(value) =>
|
|
setFormData({ ...formData, currency: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="통화 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="KRW">KRW (원)</SelectItem>
|
|
<SelectItem value="USD">USD (달러)</SelectItem>
|
|
<SelectItem value="EUR">EUR (유로)</SelectItem>
|
|
<SelectItem value="JPY">JPY (엔)</SelectItem>
|
|
<SelectItem value="CNY">CNY (위안)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
{/* 선적항 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="portOfLoading" className="text-xs sm:text-sm">
|
|
선적항
|
|
</Label>
|
|
<Input
|
|
id="portOfLoading"
|
|
placeholder="선적항"
|
|
value={formData.portOfLoading}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, portOfLoading: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 도착항 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="portOfDischarge" className="text-xs sm:text-sm">
|
|
도착항
|
|
</Label>
|
|
<Input
|
|
id="portOfDischarge"
|
|
placeholder="도착항"
|
|
value={formData.portOfDischarge}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, portOfDischarge: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* HS Code */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="hsCode" className="text-xs sm:text-sm">
|
|
HS Code
|
|
</Label>
|
|
<Input
|
|
id="hsCode"
|
|
placeholder="HS Code"
|
|
value={formData.hsCode}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, hsCode: e.target.value })
|
|
}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 메모 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="memo" className="text-xs sm:text-sm">
|
|
메모
|
|
</Label>
|
|
<Textarea
|
|
id="memo"
|
|
placeholder="메모를 입력하세요"
|
|
value={formData.memo}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, memo: e.target.value })
|
|
}
|
|
className="text-xs sm:text-sm"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCancel}
|
|
disabled={isSaving}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{isSaving ? "저장 중..." : "저장"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|