feat(WEB): 수주 삭제 기능 추가 및 출하목록 데이터 수정
- 수주 상세 페이지에 삭제 기능 추가 - 수주등록/취소 상태에서 삭제 버튼 표시 - 삭제 확인 다이얼로그 구현 - 삭제 후 목록 페이지로 이동 - 출하목록 데이터 누락 수정 - 발주처/현장명 order_info 참조하도록 수정 - 배송방식 라벨 API 응답값 사용 - 공통코드 API 함수 추가 - getDeliveryMethodCodes, getDeliveryMethodOptions - getCodeLabel 유틸 함수
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||||
@@ -64,6 +65,7 @@ import {
|
|||||||
updateOrderStatus,
|
updateOrderStatus,
|
||||||
revertProductionOrder,
|
revertProductionOrder,
|
||||||
revertOrderConfirmation,
|
revertOrderConfirmation,
|
||||||
|
deleteOrder,
|
||||||
type Order,
|
type Order,
|
||||||
type OrderStatus,
|
type OrderStatus,
|
||||||
} from "@/components/orders";
|
} from "@/components/orders";
|
||||||
@@ -115,6 +117,8 @@ export default function OrderDetailPage() {
|
|||||||
const [isReverting, setIsReverting] = useState(false);
|
const [isReverting, setIsReverting] = useState(false);
|
||||||
const [isRevertConfirmDialogOpen, setIsRevertConfirmDialogOpen] = useState(false);
|
const [isRevertConfirmDialogOpen, setIsRevertConfirmDialogOpen] = useState(false);
|
||||||
const [isRevertingConfirm, setIsRevertingConfirm] = useState(false);
|
const [isRevertingConfirm, setIsRevertingConfirm] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
// 취소 폼 상태
|
// 취소 폼 상태
|
||||||
const [cancelReason, setCancelReason] = useState("");
|
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) => {
|
const openDocumentModal = (type: OrderDocumentType) => {
|
||||||
setDocumentType(type);
|
setDocumentType(type);
|
||||||
@@ -727,6 +756,8 @@ export default function OrderDetailPage() {
|
|||||||
order.status !== "shipped" &&
|
order.status !== "shipped" &&
|
||||||
order.status !== "cancelled" &&
|
order.status !== "cancelled" &&
|
||||||
order.status !== "production_ordered";
|
order.status !== "production_ordered";
|
||||||
|
// 삭제 버튼은 수주등록 또는 취소 상태에서 표시
|
||||||
|
const showDeleteButton = order.status === "order_registered" || order.status === "cancelled";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
@@ -772,9 +803,15 @@ export default function OrderDetailPage() {
|
|||||||
취소
|
취소
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel]);
|
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel, handleDelete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1116,6 +1153,80 @@ export default function OrderDetailPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 {
|
import {
|
||||||
SHIPMENT_STATUS_LABELS,
|
SHIPMENT_STATUS_LABELS,
|
||||||
SHIPMENT_STATUS_STYLES,
|
SHIPMENT_STATUS_STYLES,
|
||||||
DELIVERY_METHOD_LABELS,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
import type { ShipmentItem, ShipmentStatus, ShipmentStats, ShipmentStatusStats } from './types';
|
import type { ShipmentItem, ShipmentStatus, ShipmentStats, ShipmentStatusStats } from './types';
|
||||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
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" />
|
<X className="w-4 h-4 mx-auto text-red-600" />
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">{DELIVERY_METHOD_LABELS[item.deliveryMethod]}</TableCell>
|
<TableCell className="text-center">{item.deliveryMethodLabel}</TableCell>
|
||||||
<TableCell>{item.customerName}</TableCell>
|
<TableCell>{item.customerName}</TableCell>
|
||||||
<TableCell className="max-w-[120px] truncate">{item.siteName}</TableCell>
|
<TableCell className="max-w-[120px] truncate">{item.siteName}</TableCell>
|
||||||
<TableCell className="text-center">{item.manager || '-'}</TableCell>
|
<TableCell className="text-center">{item.manager || '-'}</TableCell>
|
||||||
@@ -331,7 +330,7 @@ export function ShipmentList() {
|
|||||||
<InfoField label="로트번호" value={item.lotNo} />
|
<InfoField label="로트번호" value={item.lotNo} />
|
||||||
<InfoField label="발주처" value={item.customerName} />
|
<InfoField label="발주처" value={item.customerName} />
|
||||||
<InfoField label="출고예정일" value={item.scheduledDate} />
|
<InfoField label="출고예정일" value={item.scheduledDate} />
|
||||||
<InfoField label="배송방식" value={DELIVERY_METHOD_LABELS[item.deliveryMethod]} />
|
<InfoField label="배송방식" value={item.deliveryMethodLabel} />
|
||||||
<InfoField label="출하가능" value={item.canShip ? '가능' : '불가'} />
|
<InfoField label="출하가능" value={item.canShip ? '가능' : '불가'} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,8 +144,10 @@ function transformApiToListItem(data: ShipmentApiData): ShipmentItem {
|
|||||||
status: data.status,
|
status: data.status,
|
||||||
priority: data.priority,
|
priority: data.priority,
|
||||||
deliveryMethod: data.delivery_method,
|
deliveryMethod: data.delivery_method,
|
||||||
customerName: data.customer_name || '',
|
deliveryMethodLabel: data.delivery_method_label || data.delivery_method,
|
||||||
siteName: data.site_name || '',
|
// 발주처/배송 정보: 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,
|
manager: data.loading_manager,
|
||||||
canShip: data.can_ship,
|
canShip: data.can_ship,
|
||||||
depositConfirmed: data.deposit_confirmed,
|
depositConfirmed: data.deposit_confirmed,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface ShipmentItem {
|
|||||||
status: ShipmentStatus; // 상태
|
status: ShipmentStatus; // 상태
|
||||||
priority: ShipmentPriority; // 우선순위
|
priority: ShipmentPriority; // 우선순위
|
||||||
deliveryMethod: DeliveryMethod; // 배송방식
|
deliveryMethod: DeliveryMethod; // 배송방식
|
||||||
|
deliveryMethodLabel: string; // 배송방식 라벨 (API에서 조회)
|
||||||
customerName: string; // 발주처
|
customerName: string; // 발주처
|
||||||
siteName: string; // 현장명
|
siteName: string; // 현장명
|
||||||
manager?: string; // 담당
|
manager?: string; // 담당
|
||||||
|
|||||||
@@ -118,4 +118,30 @@ export async function getItemTypeCodes() {
|
|||||||
*/
|
*/
|
||||||
export async function getItemTypeOptions() {
|
export async function getItemTypeOptions() {
|
||||||
return getCommonCodeOptions('item_type');
|
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