feat(WEB): 수주 삭제 기능 추가 및 출하목록 데이터 수정
- 수주 상세 페이지에 삭제 기능 추가 - 수주등록/취소 상태에서 삭제 버튼 표시 - 삭제 확인 다이얼로그 구현 - 삭제 후 목록 페이지로 이동 - 출하목록 데이터 누락 수정 - 발주처/현장명 order_info 참조하도록 수정 - 배송방식 라벨 API 응답값 사용 - 공통코드 API 함수 추가 - getDeliveryMethodCodes, getDeliveryMethodOptions - getCodeLabel 유틸 함수
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||
@@ -64,6 +65,7 @@ import {
|
||||
updateOrderStatus,
|
||||
revertProductionOrder,
|
||||
revertOrderConfirmation,
|
||||
deleteOrder,
|
||||
type Order,
|
||||
type OrderStatus,
|
||||
} from "@/components/orders";
|
||||
@@ -115,6 +117,8 @@ export default function OrderDetailPage() {
|
||||
const [isReverting, setIsReverting] = useState(false);
|
||||
const [isRevertConfirmDialogOpen, setIsRevertConfirmDialogOpen] = useState(false);
|
||||
const [isRevertingConfirm, setIsRevertingConfirm] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 취소 폼 상태
|
||||
const [cancelReason, setCancelReason] = useState("");
|
||||
@@ -296,6 +300,31 @@ export default function OrderDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 수주 삭제
|
||||
const handleDelete = () => {
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteSubmit = async () => {
|
||||
if (order) {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deleteOrder(order.id);
|
||||
if (result.success) {
|
||||
toast.success("수주가 삭제되었습니다.");
|
||||
router.push("/sales/order-management-sales");
|
||||
} else {
|
||||
toast.error(result.error || "수주 삭제에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting order:", error);
|
||||
toast.error("수주 삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 문서 모달 열기
|
||||
const openDocumentModal = (type: OrderDocumentType) => {
|
||||
setDocumentType(type);
|
||||
@@ -727,6 +756,8 @@ export default function OrderDetailPage() {
|
||||
order.status !== "shipped" &&
|
||||
order.status !== "cancelled" &&
|
||||
order.status !== "production_ordered";
|
||||
// 삭제 버튼은 수주등록 또는 취소 상태에서 표시
|
||||
const showDeleteButton = order.status === "order_registered" || order.status === "cancelled";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -772,9 +803,15 @@ export default function OrderDetailPage() {
|
||||
취소
|
||||
</Button>
|
||||
)}
|
||||
{showDeleteButton && (
|
||||
<Button variant="outline" onClick={handleDelete} className="border-red-200 text-red-600 hover:border-red-300 hover:bg-red-50">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel]);
|
||||
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel, handleDelete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1116,6 +1153,80 @@ export default function OrderDetailPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 수주 삭제 다이얼로그 */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Trash2 className="h-5 w-5 text-red-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 || 0)}원
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">현재 상태</span>
|
||||
{getOrderStatusBadge(order.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 경고 메시지 */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm space-y-1">
|
||||
<p className="font-medium mb-2 text-red-700">⚠️ 삭제 시 주의사항</p>
|
||||
<ul className="space-y-1 text-red-600">
|
||||
<li>• 삭제된 수주는 복구할 수 없습니다</li>
|
||||
<li>• 관련된 모든 품목 정보가 함께 삭제됩니다</li>
|
||||
<li>• 이 작업은 되돌릴 수 없습니다</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 확인 안내 */}
|
||||
<div className="bg-gray-100 border border-gray-200 rounded-lg p-4 text-sm">
|
||||
<p className="text-gray-700 font-medium">
|
||||
정말로 이 수주를 삭제하시겠습니까?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteSubmit}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
{isDeleting ? "삭제 중..." : "삭제 확정"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -41,7 +41,6 @@ import { getShipments, getShipmentStats, getShipmentStatsByStatus } from './acti
|
||||
import {
|
||||
SHIPMENT_STATUS_LABELS,
|
||||
SHIPMENT_STATUS_STYLES,
|
||||
DELIVERY_METHOD_LABELS,
|
||||
} from './types';
|
||||
import type { ShipmentItem, ShipmentStatus, ShipmentStats, ShipmentStatusStats } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
@@ -287,7 +286,7 @@ export function ShipmentList() {
|
||||
<X className="w-4 h-4 mx-auto text-red-600" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{DELIVERY_METHOD_LABELS[item.deliveryMethod]}</TableCell>
|
||||
<TableCell className="text-center">{item.deliveryMethodLabel}</TableCell>
|
||||
<TableCell>{item.customerName}</TableCell>
|
||||
<TableCell className="max-w-[120px] truncate">{item.siteName}</TableCell>
|
||||
<TableCell className="text-center">{item.manager || '-'}</TableCell>
|
||||
@@ -331,7 +330,7 @@ export function ShipmentList() {
|
||||
<InfoField label="로트번호" value={item.lotNo} />
|
||||
<InfoField label="발주처" value={item.customerName} />
|
||||
<InfoField label="출고예정일" value={item.scheduledDate} />
|
||||
<InfoField label="배송방식" value={DELIVERY_METHOD_LABELS[item.deliveryMethod]} />
|
||||
<InfoField label="배송방식" value={item.deliveryMethodLabel} />
|
||||
<InfoField label="출하가능" value={item.canShip ? '가능' : '불가'} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -144,8 +144,10 @@ function transformApiToListItem(data: ShipmentApiData): ShipmentItem {
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
deliveryMethod: data.delivery_method,
|
||||
customerName: data.customer_name || '',
|
||||
siteName: data.site_name || '',
|
||||
deliveryMethodLabel: data.delivery_method_label || data.delivery_method,
|
||||
// 발주처/배송 정보: order_info 우선 참조 (Order가 Single Source of Truth)
|
||||
customerName: data.order_info?.customer_name || data.customer_name || '',
|
||||
siteName: data.order_info?.site_name || data.site_name || '',
|
||||
manager: data.loading_manager,
|
||||
canShip: data.can_ship,
|
||||
depositConfirmed: data.deposit_confirmed,
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface ShipmentItem {
|
||||
status: ShipmentStatus; // 상태
|
||||
priority: ShipmentPriority; // 우선순위
|
||||
deliveryMethod: DeliveryMethod; // 배송방식
|
||||
deliveryMethodLabel: string; // 배송방식 라벨 (API에서 조회)
|
||||
customerName: string; // 발주처
|
||||
siteName: string; // 현장명
|
||||
manager?: string; // 담당
|
||||
|
||||
@@ -118,4 +118,30 @@ export async function getItemTypeCodes() {
|
||||
*/
|
||||
export async function getItemTypeOptions() {
|
||||
return getCommonCodeOptions('item_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 코드 조회
|
||||
*/
|
||||
export async function getDeliveryMethodCodes() {
|
||||
return getCommonCodes('delivery_method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 옵션 조회
|
||||
*/
|
||||
export async function getDeliveryMethodOptions() {
|
||||
return getCommonCodeOptions('delivery_method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드값으로 라벨 조회 (code → name 매핑)
|
||||
*/
|
||||
export async function getCodeLabel(group: string, code: string): Promise<string> {
|
||||
const result = await getCommonCodes(group);
|
||||
if (result.success && result.data) {
|
||||
const found = result.data.find((item) => item.code === code);
|
||||
return found?.name || code;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
Reference in New Issue
Block a user