664 lines
24 KiB
TypeScript
664 lines
24 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 수주 상세 보기 컴포넌트 (View Mode)
|
||
|
|
*
|
||
|
|
* - 문서 모달: 계약서, 거래명세서, 발주서
|
||
|
|
* - 기본 정보, 수주/배송 정보, 비고
|
||
|
|
* - 제품 내역 테이블
|
||
|
|
* - 상태별 버튼 차이
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { useRouter } from "next/navigation";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from "@/components/ui/table";
|
||
|
|
import {
|
||
|
|
FileText,
|
||
|
|
ArrowLeft,
|
||
|
|
Edit,
|
||
|
|
Factory,
|
||
|
|
XCircle,
|
||
|
|
FileSpreadsheet,
|
||
|
|
FileCheck,
|
||
|
|
ClipboardList,
|
||
|
|
Eye,
|
||
|
|
CheckCircle2,
|
||
|
|
} from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
import { PageLayout } from "@/components/organisms/PageLayout";
|
||
|
|
import { PageHeader } from "@/components/organisms/PageHeader";
|
||
|
|
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||
|
|
import { formatAmount } from "@/utils/formatAmount";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
DialogFooter,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import {
|
||
|
|
OrderDocumentModal,
|
||
|
|
type OrderDocumentType,
|
||
|
|
getOrderById,
|
||
|
|
updateOrderStatus,
|
||
|
|
type Order,
|
||
|
|
type OrderStatus,
|
||
|
|
} from "@/components/orders";
|
||
|
|
import { ServerErrorPage } from "@/components/common/ServerErrorPage";
|
||
|
|
|
||
|
|
|
||
|
|
// 상태 뱃지 헬퍼
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 정보 표시 컴포넌트
|
||
|
|
function InfoItem({ label, value }: { label: string; value: string }) {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">{label}</p>
|
||
|
|
<p className="font-medium">{value || "-"}</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface OrderSalesDetailViewProps {
|
||
|
|
orderId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||
|
|
const router = useRouter();
|
||
|
|
|
||
|
|
const [order, setOrder] = useState<Order | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
||
|
|
const [isCancelling, setIsCancelling] = useState(false);
|
||
|
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||
|
|
const [isConfirming, setIsConfirming] = useState(false);
|
||
|
|
|
||
|
|
// 취소 폼 상태
|
||
|
|
const [cancelReason, setCancelReason] = useState("");
|
||
|
|
const [cancelDetail, setCancelDetail] = useState("");
|
||
|
|
|
||
|
|
// 문서 모달 상태
|
||
|
|
const [documentModalOpen, setDocumentModalOpen] = useState(false);
|
||
|
|
const [documentType, setDocumentType] = useState<OrderDocumentType>("contract");
|
||
|
|
|
||
|
|
// 데이터 로드 (API 호출)
|
||
|
|
useEffect(() => {
|
||
|
|
async function loadOrder() {
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
const result = await getOrderById(orderId);
|
||
|
|
if (result.success && result.data) {
|
||
|
|
setOrder(result.data);
|
||
|
|
} else {
|
||
|
|
toast.error(result.error || "수주 정보를 불러오는데 실패했습니다.");
|
||
|
|
setOrder(null);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error loading order:", error);
|
||
|
|
toast.error("수주 정보를 불러오는 중 오류가 발생했습니다.");
|
||
|
|
setOrder(null);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
loadOrder();
|
||
|
|
}, [orderId]);
|
||
|
|
|
||
|
|
const handleBack = () => {
|
||
|
|
router.push("/sales/order-management-sales");
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleEdit = () => {
|
||
|
|
// V2 패턴: ?mode=edit로 이동
|
||
|
|
router.push(`/sales/order-management-sales/${orderId}?mode=edit`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleProductionOrder = () => {
|
||
|
|
// 생산지시 생성 페이지로 이동
|
||
|
|
router.push(`/sales/order-management-sales/${orderId}/production-order`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleViewProductionOrder = () => {
|
||
|
|
// 생산지시 목록 페이지로 이동 (수주관리 내부)
|
||
|
|
router.push(`/sales/order-management-sales/production-orders`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCancel = () => {
|
||
|
|
setCancelReason("");
|
||
|
|
setCancelDetail("");
|
||
|
|
setIsCancelDialogOpen(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirmCancel = async () => {
|
||
|
|
if (!cancelReason) {
|
||
|
|
toast.error("취소 사유를 선택해주세요.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (order) {
|
||
|
|
setIsCancelling(true);
|
||
|
|
try {
|
||
|
|
const result = await updateOrderStatus(order.id, "cancelled");
|
||
|
|
if (result.success) {
|
||
|
|
setOrder({ ...order, status: "cancelled" });
|
||
|
|
toast.success("수주가 취소되었습니다.");
|
||
|
|
setIsCancelDialogOpen(false);
|
||
|
|
setCancelReason("");
|
||
|
|
setCancelDetail("");
|
||
|
|
} else {
|
||
|
|
toast.error(result.error || "수주 취소에 실패했습니다.");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error cancelling order:", error);
|
||
|
|
toast.error("수주 취소 중 오류가 발생했습니다.");
|
||
|
|
} finally {
|
||
|
|
setIsCancelling(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 수주 확정 처리
|
||
|
|
const handleConfirmOrder = () => {
|
||
|
|
setIsConfirmDialogOpen(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirmOrderSubmit = async () => {
|
||
|
|
if (order) {
|
||
|
|
setIsConfirming(true);
|
||
|
|
try {
|
||
|
|
const result = await updateOrderStatus(order.id, "order_confirmed");
|
||
|
|
if (result.success && result.data) {
|
||
|
|
setOrder(result.data);
|
||
|
|
toast.success("수주가 확정되었습니다.");
|
||
|
|
setIsConfirmDialogOpen(false);
|
||
|
|
} else {
|
||
|
|
toast.error(result.error || "수주 확정에 실패했습니다.");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error confirming order:", error);
|
||
|
|
toast.error("수주 확정 중 오류가 발생했습니다.");
|
||
|
|
} finally {
|
||
|
|
setIsConfirming(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 문서 모달 열기
|
||
|
|
const openDocumentModal = (type: OrderDocumentType) => {
|
||
|
|
setDocumentType(type);
|
||
|
|
setDocumentModalOpen(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!order) {
|
||
|
|
return (
|
||
|
|
<ServerErrorPage
|
||
|
|
title="수주 정보를 불러올 수 없습니다"
|
||
|
|
message="수주 정보를 찾을 수 없습니다."
|
||
|
|
showBackButton={true}
|
||
|
|
showHomeButton={true}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 상태별 버튼 표시 여부
|
||
|
|
const showEditButton = order.status !== "shipped" && order.status !== "cancelled";
|
||
|
|
// 수주 확정 버튼: 수주등록 상태에서만 표시
|
||
|
|
const showConfirmButton = order.status === "order_registered";
|
||
|
|
// 생산지시 생성 버튼: 출하완료, 취소, 생산지시완료 제외하고 표시
|
||
|
|
// (수주등록, 수주확정, 생산중, 재작업중, 작업완료에서 표시)
|
||
|
|
const showProductionCreateButton =
|
||
|
|
order.status !== "shipped" &&
|
||
|
|
order.status !== "cancelled" &&
|
||
|
|
order.status !== "production_ordered";
|
||
|
|
// 생산지시 보기 버튼: 생산지시완료 상태에서 숨김 (기획서 오류로 제거)
|
||
|
|
const showProductionViewButton = false;
|
||
|
|
const showCancelButton =
|
||
|
|
order.status !== "shipped" &&
|
||
|
|
order.status !== "cancelled" &&
|
||
|
|
order.status !== "production_ordered";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PageLayout>
|
||
|
|
{/* 헤더 */}
|
||
|
|
<PageHeader
|
||
|
|
title="수주 상세"
|
||
|
|
icon={FileText}
|
||
|
|
actions={
|
||
|
|
<div className="flex items-center gap-2 flex-wrap">
|
||
|
|
<Button variant="outline" onClick={handleBack}>
|
||
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||
|
|
목록
|
||
|
|
</Button>
|
||
|
|
{showEditButton && (
|
||
|
|
<Button variant="outline" onClick={handleEdit}>
|
||
|
|
<Edit className="h-4 w-4 mr-2" />
|
||
|
|
수정
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{showConfirmButton && (
|
||
|
|
<Button onClick={handleConfirmOrder} className="bg-green-600 hover:bg-green-700">
|
||
|
|
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||
|
|
수주 확정
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{showProductionCreateButton && (
|
||
|
|
<Button onClick={handleProductionOrder}>
|
||
|
|
<Factory className="h-4 w-4 mr-2" />
|
||
|
|
생산지시 생성
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{showProductionViewButton && (
|
||
|
|
<Button onClick={handleViewProductionOrder}>
|
||
|
|
<Eye className="h-4 w-4 mr-2" />
|
||
|
|
생산지시 보기
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{showCancelButton && (
|
||
|
|
<Button variant="outline" onClick={handleCancel} className="border-orange-200 text-orange-600 hover:border-orange-300">
|
||
|
|
<XCircle className="h-4 w-4 mr-2" />
|
||
|
|
취소
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* 수주 정보 헤더 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="pb-4">
|
||
|
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||
|
|
{order.lotNumber}
|
||
|
|
</code>
|
||
|
|
{getOrderStatusBadge(order.status)}
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-muted-foreground">
|
||
|
|
수주일: {order.orderDate}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="pt-0">
|
||
|
|
{/* 문서 버튼들 */}
|
||
|
|
<div className="flex items-center gap-2 pt-4 border-t">
|
||
|
|
<span className="text-sm text-muted-foreground mr-2">문서:</span>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => openDocumentModal("contract")}
|
||
|
|
>
|
||
|
|
<FileCheck className="h-4 w-4 mr-1" />
|
||
|
|
계약서
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => openDocumentModal("transaction")}
|
||
|
|
>
|
||
|
|
<FileSpreadsheet className="h-4 w-4 mr-1" />
|
||
|
|
거래명세서
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => openDocumentModal("purchaseOrder")}
|
||
|
|
>
|
||
|
|
<ClipboardList className="h-4 w-4 mr-1" />
|
||
|
|
발주서
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 기본 정보 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">기본 정보</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||
|
|
<InfoItem label="발주처" value={order.client} />
|
||
|
|
<InfoItem label="현장명" value={order.siteName} />
|
||
|
|
<InfoItem label="담당자" value={order.manager} />
|
||
|
|
<InfoItem label="연락처" value={order.contact} />
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 수주/배송 정보 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">수주/배송 정보</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||
|
|
<InfoItem label="수주일자" value={order.orderDate} />
|
||
|
|
<InfoItem label="출고예정일" value={order.expectedShipDate || "미정"} />
|
||
|
|
<InfoItem label="납품요청일" value={order.deliveryRequestDate} />
|
||
|
|
<InfoItem label="배송방식" value={order.deliveryMethod} />
|
||
|
|
<InfoItem label="운임비용" value={order.shippingCost} />
|
||
|
|
<InfoItem label="수신(반장/업체)" value={order.receiver} />
|
||
|
|
<InfoItem label="수신처 연락처" value={order.receiverContact} />
|
||
|
|
<InfoItem label="수신처 주소" value={order.address} />
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 비고 */}
|
||
|
|
{order.remarks && (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">비고</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<p className="whitespace-pre-wrap">{order.remarks}</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 제품 내역 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">제품 내역</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="border rounded-lg overflow-hidden">
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead className="w-[60px] text-center">순번</TableHead>
|
||
|
|
<TableHead>품목코드</TableHead>
|
||
|
|
<TableHead>품명</TableHead>
|
||
|
|
<TableHead>층</TableHead>
|
||
|
|
<TableHead>부호</TableHead>
|
||
|
|
<TableHead>규격</TableHead>
|
||
|
|
<TableHead className="text-center">수량</TableHead>
|
||
|
|
<TableHead className="text-center">단위</TableHead>
|
||
|
|
<TableHead className="text-right">단가</TableHead>
|
||
|
|
<TableHead className="text-right">금액</TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{order.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(order.subtotal)}원
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-4 text-sm">
|
||
|
|
<span className="text-muted-foreground">할인율:</span>
|
||
|
|
<span className="w-32 text-right">{order.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(order.totalAmount)}원
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 문서 모달 */}
|
||
|
|
<OrderDocumentModal
|
||
|
|
open={documentModalOpen}
|
||
|
|
onOpenChange={setDocumentModalOpen}
|
||
|
|
documentType={documentType}
|
||
|
|
data={{
|
||
|
|
lotNumber: order.lotNumber,
|
||
|
|
orderDate: order.orderDate,
|
||
|
|
client: order.client,
|
||
|
|
siteName: order.siteName,
|
||
|
|
manager: order.manager,
|
||
|
|
managerContact: order.contact,
|
||
|
|
deliveryRequestDate: order.deliveryRequestDate,
|
||
|
|
expectedShipDate: order.expectedShipDate,
|
||
|
|
deliveryMethod: order.deliveryMethod,
|
||
|
|
address: order.address,
|
||
|
|
items: order.items,
|
||
|
|
subtotal: order.subtotal,
|
||
|
|
discountRate: order.discountRate,
|
||
|
|
totalAmount: order.totalAmount,
|
||
|
|
remarks: order.remarks,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 취소 확인 다이얼로그 */}
|
||
|
|
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
||
|
|
<DialogContent className="max-w-md">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<XCircle className="h-5 w-5" />
|
||
|
|
수주 취소
|
||
|
|
</DialogTitle>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 수주 정보 박스 */}
|
||
|
|
<div className="border rounded-lg p-4 space-y-2 text-sm">
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">수주번호</span>
|
||
|
|
<span className="font-medium">{order.lotNumber}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">발주처</span>
|
||
|
|
<span>{order.client}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">현장명</span>
|
||
|
|
<span>{order.siteName}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-muted-foreground">현재 상태</span>
|
||
|
|
{getOrderStatusBadge(order.status)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 취소 사유 선택 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="cancelReason">
|
||
|
|
취소 사유 <span className="text-red-500">*</span>
|
||
|
|
</Label>
|
||
|
|
<Select value={cancelReason} onValueChange={setCancelReason}>
|
||
|
|
<SelectTrigger id="cancelReason">
|
||
|
|
<SelectValue placeholder="취소 사유를 선택하세요" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="customer_request">고객 요청</SelectItem>
|
||
|
|
<SelectItem value="spec_change">사양 변경</SelectItem>
|
||
|
|
<SelectItem value="price_issue">가격 문제</SelectItem>
|
||
|
|
<SelectItem value="delivery_issue">납기 문제</SelectItem>
|
||
|
|
<SelectItem value="duplicate_order">중복 수주</SelectItem>
|
||
|
|
<SelectItem value="other">기타</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 상세 사유 입력 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="cancelDetail">상세 사유</Label>
|
||
|
|
<Textarea
|
||
|
|
id="cancelDetail"
|
||
|
|
placeholder="취소 사유에 대한 상세 내용을 입력하세요"
|
||
|
|
value={cancelDetail}
|
||
|
|
onChange={(e) => setCancelDetail(e.target.value)}
|
||
|
|
rows={3}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 취소 시 유의사항 */}
|
||
|
|
<div className="bg-gray-50 border rounded-lg p-4 text-sm space-y-1">
|
||
|
|
<p className="font-medium mb-2">취소 시 유의사항</p>
|
||
|
|
<ul className="space-y-1 text-muted-foreground">
|
||
|
|
<li>• 취소된 수주는 목록에서 '취소' 상태로 표시됩니다</li>
|
||
|
|
<li>• 취소 후에는 수정이 불가능합니다</li>
|
||
|
|
<li>• 관련된 생산지시가 있는 경우 먼저 생산지시를 취소해야 합니다</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => setIsCancelDialogOpen(false)}
|
||
|
|
>
|
||
|
|
닫기
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={handleConfirmCancel}
|
||
|
|
className="border-gray-300"
|
||
|
|
disabled={isCancelling}
|
||
|
|
>
|
||
|
|
<XCircle className="h-4 w-4 mr-1" />
|
||
|
|
{isCancelling ? "취소 중..." : "취소 확정"}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
{/* 수주 확정 다이얼로그 */}
|
||
|
|
<Dialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}>
|
||
|
|
<DialogContent className="max-w-md">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||
|
|
수주 확정
|
||
|
|
</DialogTitle>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 수주 정보 박스 */}
|
||
|
|
<div className="border rounded-lg p-4 space-y-2 text-sm">
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">수주번호</span>
|
||
|
|
<span className="font-medium">{order.lotNumber}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">발주처</span>
|
||
|
|
<span>{order.client}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">현장명</span>
|
||
|
|
<span>{order.siteName}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span className="text-muted-foreground">총금액</span>
|
||
|
|
<span className="font-medium text-green-600">
|
||
|
|
{formatAmount(order.totalAmount)}원
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-muted-foreground">현재 상태</span>
|
||
|
|
{getOrderStatusBadge(order.status)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 확정 안내 */}
|
||
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-sm space-y-1">
|
||
|
|
<p className="font-medium mb-2 text-green-700">확정 후 변경사항</p>
|
||
|
|
<ul className="space-y-1 text-green-600">
|
||
|
|
<li>• 수주 상태가 '수주확정'으로 변경됩니다</li>
|
||
|
|
<li>• 생산지시를 생성할 수 있습니다</li>
|
||
|
|
<li>• 확정 후에도 수정이 가능합니다</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => setIsConfirmDialogOpen(false)}
|
||
|
|
>
|
||
|
|
닫기
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
onClick={handleConfirmOrderSubmit}
|
||
|
|
className="bg-green-600 hover:bg-green-700"
|
||
|
|
disabled={isConfirming}
|
||
|
|
>
|
||
|
|
<CheckCircle2 className="h-4 w-4 mr-1" />
|
||
|
|
{isConfirming ? "확정 중..." : "확정"}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
</PageLayout>
|
||
|
|
);
|
||
|
|
}
|