feat: [shipment] 출하관리 UI 개선 - cancelled 상태 추가, 삭제 버튼 제거, 제품코드 표시, 상태 버튼 워딩 수정
- 출하 cancelled 상태 추가 및 UI 반영 - 출하 상세에서 삭제 버튼/기능 제거 - 출하 상세 제품그룹에 제품코드 표시 - 상태 변경 버튼 워딩 수정
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
ClipboardList,
|
||||
ArrowRight,
|
||||
Loader2,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -31,7 +30,6 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
@@ -56,7 +54,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { shipmentConfig } from './shipmentConfig';
|
||||
import { getShipmentById, deleteShipment, updateShipmentStatus } from './actions';
|
||||
import { getShipmentById, updateShipmentStatus } from './actions';
|
||||
import {
|
||||
SHIPMENT_STATUS_LABELS,
|
||||
SHIPMENT_STATUS_STYLES,
|
||||
@@ -93,9 +91,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
const router = useRouter();
|
||||
const [previewDocument, setPreviewDocument] = useState<'shipping' | 'delivery' | null>(null);
|
||||
const [orderDetail, setOrderDetail] = useState<OrderDocumentDetail | null>(null);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 상태 변경 관련 상태
|
||||
const [showStatusDialog, setShowStatusDialog] = useState(false);
|
||||
const [targetStatus, setTargetStatus] = useState<ShipmentStatus | null>(null);
|
||||
@@ -165,25 +160,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
router.push(`/ko/outbound/shipments/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deleteShipment(id);
|
||||
if (result.success) {
|
||||
router.push('/ko/outbound/shipments');
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[ShipmentDetail] handleDelete error:', err);
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteDialog(false);
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
const handleOpenStatusDialog = useCallback((status: ShipmentStatus) => {
|
||||
setTargetStatus(status);
|
||||
setStatusFormData({
|
||||
@@ -259,7 +235,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
);
|
||||
|
||||
const canEdit = detail ? (detail.status === 'scheduled' || detail.status === 'ready') : false;
|
||||
const canDelete = detail ? (detail.status === 'scheduled' || detail.status === 'ready') : false;
|
||||
|
||||
// 제품 부품 테이블 렌더링
|
||||
const renderPartsTable = (parts: ProductPart[]) => (
|
||||
@@ -309,16 +284,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
<ClipboardList className="w-4 h-4 md:mr-2" />
|
||||
<span className="hidden md:inline">납품확인서 보기</span>
|
||||
</Button>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 md:mr-2" />
|
||||
<span className="hidden md:inline">삭제</span>
|
||||
</Button>
|
||||
)}
|
||||
{STATUS_TRANSITIONS[detail.status] && detail.canShip && (
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -327,7 +292,7 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
onClick={() => handleOpenStatusDialog(STATUS_TRANSITIONS[detail.status]!)}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4 md:mr-2" />
|
||||
<span className="hidden md:inline">{SHIPMENT_STATUS_LABELS[STATUS_TRANSITIONS[detail.status]!]}으로 변경</span>
|
||||
<span className="hidden md:inline">{SHIPMENT_STATUS_LABELS[STATUS_TRANSITIONS[detail.status]!]}로 변경</span>
|
||||
</Button>
|
||||
)}
|
||||
{STATUS_TRANSITIONS[detail.status] && !detail.canShip && (
|
||||
@@ -343,7 +308,7 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [detail, canDelete, handleOpenStatusDialog]);
|
||||
}, [detail, handleOpenStatusDialog]);
|
||||
|
||||
// 컨텐츠 렌더링
|
||||
const renderViewContent = useCallback((_data: Record<string, unknown>) => {
|
||||
@@ -568,22 +533,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
)}
|
||||
</DocumentViewer>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
title="출고 정보 삭제"
|
||||
description={
|
||||
<>
|
||||
출고번호 {detail?.shipmentNo}을(를) 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
|
||||
{/* 상태 변경 다이얼로그 */}
|
||||
<Dialog open={showStatusDialog} onOpenChange={setShowStatusDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
|
||||
@@ -125,6 +125,7 @@ interface ShipmentItemApiData {
|
||||
unit?: string;
|
||||
lot_no?: string;
|
||||
stock_lot_id?: number;
|
||||
product_name?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
@@ -191,20 +192,20 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail {
|
||||
// items를 floor_unit 기준으로 productGroups 자동 그룹핑
|
||||
const rawItems = data.items || [];
|
||||
const items = rawItems.map(transformApiToProduct);
|
||||
const groupMap = new Map<string, { productName: string; specification: string; parts: { product: ShipmentProduct; unit: string }[] }>();
|
||||
const groupMap = new Map<string, { productName: string; productCode: string; parts: { product: ShipmentProduct; unit: string }[] }>();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const raw = rawItems[i];
|
||||
const key = item.floorUnit || `item-${item.id}`;
|
||||
if (!groupMap.has(key)) {
|
||||
groupMap.set(key, { productName: key, specification: '', parts: [] });
|
||||
groupMap.set(key, { productName: key, productCode: raw.product_name || '', parts: [] });
|
||||
}
|
||||
groupMap.get(key)!.parts.push({ product: item, unit: raw.unit || '' });
|
||||
}
|
||||
const productGroups = Array.from(groupMap.entries()).map(([key, g]) => ({
|
||||
id: key,
|
||||
productName: g.productName,
|
||||
specification: g.parts[0]?.product.specification || '',
|
||||
specification: g.productCode,
|
||||
partCount: g.parts.length,
|
||||
parts: g.parts.map((p, i) => ({
|
||||
id: p.product.id,
|
||||
@@ -465,17 +466,6 @@ export async function updateShipmentStatus(
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 출고 삭제 =====
|
||||
export async function deleteShipment(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/shipments/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '출고 삭제에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 물류사 옵션 조회 =====
|
||||
export async function getLogisticsOptions(): Promise<{ success: boolean; data: LogisticsOption[]; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<LogisticsOption[]>({
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { DetailConfig } from '@/components/templates/IntegratedDetailTempla
|
||||
*
|
||||
* 특이사항:
|
||||
* - view 모드만 지원 (수정은 별도 /edit 페이지로 이동)
|
||||
* - 삭제 기능 있음 (scheduled, ready 상태에서만)
|
||||
* - 문서 미리보기: 출고증, 거래명세서, 납품확인서
|
||||
*/
|
||||
export const shipmentConfig: DetailConfig = {
|
||||
@@ -24,15 +23,10 @@ export const shipmentConfig: DetailConfig = {
|
||||
gridColumns: 2,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: true, // 상태에 따라 동적으로 처리
|
||||
showDelete: false,
|
||||
showEdit: true,
|
||||
backLabel: '목록',
|
||||
deleteLabel: '삭제',
|
||||
editLabel: '수정',
|
||||
deleteConfirmMessage: {
|
||||
title: '출고 정보 삭제',
|
||||
description: '이 출고 정보를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
// 출고 상태
|
||||
export type ShipmentStatus =
|
||||
| 'scheduled' // 출고예정
|
||||
| 'ready' // 출고대기 (출고대기)
|
||||
| 'ready' // 출고대기
|
||||
| 'shipping' // 배송중
|
||||
| 'completed'; // 배송완료 (출고완료)
|
||||
| 'completed' // 배송완료 (출고완료)
|
||||
| 'cancelled'; // 취소
|
||||
|
||||
// 상태 라벨
|
||||
export const SHIPMENT_STATUS_LABELS: Record<ShipmentStatus, string> = {
|
||||
@@ -15,6 +16,7 @@ export const SHIPMENT_STATUS_LABELS: Record<ShipmentStatus, string> = {
|
||||
ready: '출고대기',
|
||||
shipping: '배송중',
|
||||
completed: '출고완료',
|
||||
cancelled: '취소',
|
||||
};
|
||||
|
||||
// 상태 스타일
|
||||
@@ -23,6 +25,7 @@ export const SHIPMENT_STATUS_STYLES: Record<ShipmentStatus, string> = {
|
||||
ready: 'bg-yellow-100 text-yellow-800',
|
||||
shipping: 'bg-blue-100 text-blue-800',
|
||||
completed: 'bg-green-100 text-green-800',
|
||||
cancelled: 'bg-red-100 text-red-800',
|
||||
};
|
||||
|
||||
// 출고 우선순위
|
||||
|
||||
Reference in New Issue
Block a user