Files
sam-react-prod/src/components/orders/OrderSalesDetailEdit.tsx

566 lines
20 KiB
TypeScript
Raw Normal View History

"use client";
/**
* (Edit Mode)
*
* - ()
* - / ( )
* - ( )
* - ( )
*/
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { FileText, AlertTriangle } from "lucide-react";
import { toast } from "sonner";
import { PageLayout } from "@/components/organisms/PageLayout";
import { PageHeader } from "@/components/organisms/PageHeader";
import { FormActions } from "@/components/organisms/FormActions";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { formatAmount } from "@/utils/formatAmount";
import {
OrderItem,
getOrderById,
updateOrder,
type OrderStatus,
} from "@/components/orders";
// 수정 폼 데이터
interface EditFormData {
// 읽기전용 정보
lotNumber: string;
quoteNumber: string;
client: string;
siteName: string;
manager: string;
contact: string;
status: OrderStatus;
// 수정 가능 정보
expectedShipDate: string;
expectedShipDateUndecided: boolean;
deliveryRequestDate: string;
deliveryMethod: string;
shippingCost: string;
receiver: string;
receiverContact: string;
address: string;
addressDetail: string;
remarks: string;
// 품목 (수정 제한)
items: OrderItem[];
canEditItems: boolean;
subtotal: number;
discountRate: number;
totalAmount: number;
}
// 배송방식 옵션
const DELIVERY_METHODS = [
{ value: "direct", label: "직접배차" },
{ value: "pickup", label: "상차" },
{ value: "courier", label: "택배" },
];
// 운임비용 옵션
const SHIPPING_COSTS = [
{ value: "free", label: "무료" },
{ value: "prepaid", label: "선불" },
{ value: "collect", label: "착불" },
{ value: "negotiable", label: "협의" },
];
// 상태 뱃지 헬퍼
function getOrderStatusBadge(status: OrderStatus) {
const statusConfig: Record<OrderStatus, { label: string; className: string }> = {
order_registered: { label: "수주등록", className: "bg-gray-100 text-gray-700 border-gray-200" },
order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" },
production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" },
rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" },
work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" },
shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" },
cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" },
};
const config = statusConfig[status];
return (
<BadgeSm className={config.className}>
{config.label}
</BadgeSm>
);
}
interface OrderSalesDetailEditProps {
orderId: string;
}
export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
const router = useRouter();
const [form, setForm] = useState<EditFormData | null>(null);
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
// 데이터 로드 (API)
useEffect(() => {
async function loadOrder() {
try {
setLoading(true);
const result = await getOrderById(orderId);
if (result.success && result.data) {
const order = result.data;
// 상태에 따라 품목 수정 가능 여부 결정
const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes(
order.status
);
// Order 데이터를 EditFormData로 변환
setForm({
lotNumber: order.lotNumber,
quoteNumber: order.quoteNumber || "",
client: order.client,
siteName: order.siteName,
manager: order.manager || "",
contact: order.contact || "",
status: order.status,
expectedShipDate: order.expectedShipDate || "",
expectedShipDateUndecided: !order.expectedShipDate,
deliveryRequestDate: order.deliveryRequestDate || "",
deliveryMethod: order.deliveryMethod || "",
shippingCost: order.shippingCost || "",
receiver: order.receiver || "",
receiverContact: order.receiverContact || "",
address: order.address || "",
addressDetail: order.addressDetail || "",
remarks: order.remarks || "",
items: order.items || [],
canEditItems,
subtotal: order.subtotal || order.amount,
discountRate: order.discountRate || 0,
totalAmount: order.amount,
});
} else {
toast.error(result.error || "수주 정보를 불러오는데 실패했습니다.");
router.push("/sales/order-management-sales");
}
} catch (error) {
console.error("Error loading order:", error);
toast.error("수주 정보를 불러오는 중 오류가 발생했습니다.");
router.push("/sales/order-management-sales");
} finally {
setLoading(false);
}
}
loadOrder();
}, [orderId, router]);
const handleCancel = () => {
// V2 패턴: ?mode=view로 이동 (기본값이므로 쿼리 없이 이동)
router.push(`/sales/order-management-sales/${orderId}`);
};
const handleSave = async () => {
if (!form) return;
// 유효성 검사
if (!form.deliveryRequestDate) {
toast.error("납품요청일을 입력해주세요.");
return;
}
if (!form.receiver.trim()) {
toast.error("수신(반장/업체)을 입력해주세요.");
return;
}
if (!form.receiverContact.trim()) {
toast.error("수신처 연락처를 입력해주세요.");
return;
}
setIsSaving(true);
try {
// API 연동
const result = await updateOrder(orderId, {
clientId: undefined, // 기존 값 유지
siteName: form.siteName,
expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate,
deliveryRequestDate: form.deliveryRequestDate,
deliveryMethod: form.deliveryMethod,
shippingCost: form.shippingCost,
receiver: form.receiver,
receiverContact: form.receiverContact,
address: form.address,
addressDetail: form.addressDetail,
remarks: form.remarks,
items: form.items.map((item) => ({
itemId: item.id ? parseInt(item.id, 10) : undefined,
itemCode: item.itemCode,
itemName: item.itemName,
specification: item.spec,
quantity: item.quantity,
unit: item.unit,
unitPrice: item.unitPrice,
})),
});
if (result.success) {
toast.success("수주가 수정되었습니다.");
// V2 패턴: 저장 후 view 모드로 이동
router.push(`/sales/order-management-sales/${orderId}`);
} else {
toast.error(result.error || "수주 수정에 실패했습니다.");
}
} catch (error) {
console.error("Error updating order:", error);
toast.error("수주 수정 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
};
if (loading || !form) {
return (
<PageLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
</PageLayout>
);
}
return (
<PageLayout>
{/* 헤더 */}
<PageHeader
title="수주 수정"
icon={FileText}
actions={
<div className="flex items-center gap-2">
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{form.lotNumber}
</code>
{getOrderStatusBadge(form.status)}
</div>
}
/>
<div className="space-y-6">
{/* 기본 정보 (읽기전용) */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<span className="text-sm font-normal text-muted-foreground">()</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.lotNumber}</p>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.quoteNumber}</p>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.manager}</p>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.client}</p>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.siteName}</p>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-sm"></Label>
<p className="font-medium">{form.contact}</p>
</div>
</div>
</CardContent>
</Card>
{/* 수주/배송 정보 (편집 가능) */}
<Card>
<CardHeader>
<CardTitle className="text-lg">/ </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 출고예정일 */}
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Input
type="date"
value={form.expectedShipDate}
onChange={(e) =>
setForm({ ...form, expectedShipDate: e.target.value })
}
disabled={form.expectedShipDateUndecided}
className="flex-1"
/>
<div className="flex items-center gap-2">
<Checkbox
id="expectedShipDateUndecided"
checked={form.expectedShipDateUndecided}
onCheckedChange={(checked) =>
setForm({
...form,
expectedShipDateUndecided: checked as boolean,
expectedShipDate: checked ? "" : form.expectedShipDate,
})
}
/>
<Label
htmlFor="expectedShipDateUndecided"
className="text-sm font-normal"
>
</Label>
</div>
</div>
</div>
{/* 납품요청일 */}
<div className="space-y-2">
<Label>
<span className="text-red-500">*</span>
</Label>
<Input
type="date"
value={form.deliveryRequestDate}
onChange={(e) =>
setForm({ ...form, deliveryRequestDate: e.target.value })
}
/>
</div>
{/* 배송방식 */}
<div className="space-y-2">
<Label></Label>
<Select
key={`deliveryMethod-${form.deliveryMethod}`}
value={form.deliveryMethod}
onValueChange={(value) =>
setForm({ ...form, deliveryMethod: value })
}
>
<SelectTrigger>
<SelectValue placeholder="배송방식 선택" />
</SelectTrigger>
<SelectContent>
{DELIVERY_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 운임비용 */}
<div className="space-y-2">
<Label></Label>
<Select
key={`shippingCost-${form.shippingCost}`}
value={form.shippingCost}
onValueChange={(value) =>
setForm({ ...form, shippingCost: value })
}
>
<SelectTrigger>
<SelectValue placeholder="운임비용 선택" />
</SelectTrigger>
<SelectContent>
{SHIPPING_COSTS.map((cost) => (
<SelectItem key={cost.value} value={cost.value}>
{cost.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 수신(반장/업체) */}
<div className="space-y-2">
<Label>
(/) <span className="text-red-500">*</span>
</Label>
<Input
value={form.receiver}
onChange={(e) =>
setForm({ ...form, receiver: e.target.value })
}
/>
</div>
{/* 수신처 연락처 */}
<div className="space-y-2">
<Label>
<span className="text-red-500">*</span>
</Label>
<Input
value={form.receiverContact}
onChange={(e) =>
setForm({ ...form, receiverContact: e.target.value })
}
/>
</div>
{/* 수신처 주소 */}
<div className="space-y-2 md:col-span-2">
<Label> </Label>
<Input
value={form.address}
onChange={(e) =>
setForm({ ...form, address: e.target.value })
}
placeholder="주소"
className="mb-2"
/>
<Input
value={form.addressDetail}
onChange={(e) =>
setForm({ ...form, addressDetail: e.target.value })
}
placeholder="상세주소"
/>
</div>
</div>
</CardContent>
</Card>
{/* 비고 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={form.remarks}
onChange={(e) => setForm({ ...form, remarks: e.target.value })}
placeholder="특이사항을 입력하세요"
rows={4}
/>
</CardContent>
</Card>
{/* 품목 내역 */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
{!form.canEditItems && (
<span className="flex items-center gap-1 text-sm font-normal text-orange-600">
<AlertTriangle className="h-4 w-4" />
</span>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[60px] text-center">No</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>(mm)</TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{form.items.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-center">{index + 1}</TableCell>
<TableCell>
<code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded">
{item.itemCode}
</code>
</TableCell>
<TableCell>{item.itemName}</TableCell>
<TableCell>{item.type || "-"}</TableCell>
<TableCell>{item.symbol || "-"}</TableCell>
<TableCell>{item.spec}</TableCell>
<TableCell className="text-center">{item.quantity}</TableCell>
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-right">
{formatAmount(item.unitPrice)}
</TableCell>
<TableCell className="text-right font-medium">
{formatAmount(item.amount)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* 합계 */}
<div className="flex flex-col items-end gap-2 pt-4 mt-4 border-t">
<div className="flex items-center gap-4 text-sm">
<span className="text-muted-foreground">:</span>
<span className="w-32 text-right">
{formatAmount(form.subtotal)}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="text-muted-foreground">:</span>
<span className="w-32 text-right">{form.discountRate}%</span>
</div>
<div className="flex items-center gap-4 text-lg font-semibold">
<span>:</span>
<span className="w-32 text-right text-green-600">
{formatAmount(form.totalAmount)}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 하단 버튼 */}
<div className="sticky bottom-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t pt-4 pb-4 -mx-3 md:-mx-6 px-3 md:px-6 mt-6">
<FormActions
onSave={handleSave}
onCancel={handleCancel}
saveLabel="저장"
cancelLabel="취소"
saveLoading={isSaving}
saveDisabled={isSaving}
/>
</div>
</PageLayout>
);
}