feat(WEB): 자재/영업/품질 모듈 기능 개선 및 문서 컴포넌트 추가
- 입고관리: 상세/목록 UI 개선, actions 로직 강화 - 재고현황: 상세/목록 개선, StockAuditModal 신규 추가 - 영업주문관리: 페이지 구조 개선, OrderSalesDetailEdit 기능 강화 - 주문: OrderRegistration 개선, SalesOrderDocument 신규 추가 - 견적: QuoteTransactionModal 기능 개선 - 품질: InspectionModalV2, ImportInspectionDocument 대폭 개선 - UniversalListPage: 템플릿 기능 확장 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ReceivingDetail } from '@/components/material/ReceivingManagement';
|
||||
|
||||
interface Props {
|
||||
@@ -9,5 +10,8 @@ interface Props {
|
||||
|
||||
export default function ReceivingDetailPage({ params }: Props) {
|
||||
const { id } = use(params);
|
||||
return <ReceivingDetail id={id} />;
|
||||
const searchParams = useSearchParams();
|
||||
const mode = (searchParams.get('mode') as 'view' | 'edit' | 'new') || 'view';
|
||||
|
||||
return <ReceivingDetail id={id} mode={mode} />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { Document, DocumentItem } from '../types';
|
||||
import { MOCK_ORDER_DATA, MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||||
@@ -20,12 +20,20 @@ import {
|
||||
JointbarInspectionDocument,
|
||||
QualityDocumentUploader,
|
||||
} from './documents';
|
||||
import type { ImportInspectionTemplate } from './documents/ImportInspectionDocument';
|
||||
|
||||
// 검사 템플릿 API
|
||||
import { getInspectionTemplate } from '@/components/material/ReceivingManagement/actions';
|
||||
|
||||
interface InspectionModalV2Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
document: Document | null;
|
||||
documentItem: DocumentItem | null;
|
||||
// 수입검사 템플릿 로드용 추가 props
|
||||
itemName?: string;
|
||||
specification?: string;
|
||||
supplier?: string;
|
||||
}
|
||||
|
||||
// 문서 타입별 정보
|
||||
@@ -318,17 +326,90 @@ const WorkLogDocument = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 로딩 컴포넌트
|
||||
const LoadingDocument = () => (
|
||||
<div className="bg-white shadow-sm p-16 w-full h-full rounded flex flex-col items-center justify-center text-center">
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
|
||||
<p className="text-gray-600 text-sm">검사 템플릿을 불러오는 중...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 에러 컴포넌트
|
||||
const ErrorDocument = ({ message, onRetry }: { message: string; onRetry?: () => void }) => (
|
||||
<div className="bg-white shadow-sm p-16 w-full h-full rounded flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<p className="text-gray-800 font-medium mb-2">템플릿 로드 실패</p>
|
||||
<p className="text-gray-500 text-sm mb-4">{message}</p>
|
||||
{onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* InspectionModal V2
|
||||
* - DocumentViewer 시스템 사용
|
||||
* - 기존 문서 렌더링 로직 유지
|
||||
* - 수입검사: 모달 열릴 때 API로 템플릿 로드 (Lazy Loading)
|
||||
*/
|
||||
export const InspectionModalV2 = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
document: doc,
|
||||
documentItem,
|
||||
itemName,
|
||||
specification,
|
||||
supplier,
|
||||
}: InspectionModalV2Props) => {
|
||||
// 수입검사 템플릿 상태
|
||||
const [importTemplate, setImportTemplate] = useState<ImportInspectionTemplate | null>(null);
|
||||
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
|
||||
const [templateError, setTemplateError] = useState<string | null>(null);
|
||||
|
||||
// 수입검사 템플릿 로드 (모달 열릴 때)
|
||||
useEffect(() => {
|
||||
if (isOpen && doc?.type === 'import' && itemName && specification) {
|
||||
loadInspectionTemplate();
|
||||
}
|
||||
// 모달 닫힐 때 상태 초기화
|
||||
if (!isOpen) {
|
||||
setImportTemplate(null);
|
||||
setTemplateError(null);
|
||||
}
|
||||
}, [isOpen, doc?.type, itemName, specification]);
|
||||
|
||||
const loadInspectionTemplate = async () => {
|
||||
if (!itemName || !specification) return;
|
||||
|
||||
setIsLoadingTemplate(true);
|
||||
setTemplateError(null);
|
||||
|
||||
try {
|
||||
const result = await getInspectionTemplate({
|
||||
itemName,
|
||||
specification,
|
||||
lotNo: documentItem?.code,
|
||||
supplier,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
// API 응답을 ImportInspectionTemplate 형식으로 변환
|
||||
setImportTemplate(result.data as ImportInspectionTemplate);
|
||||
} else {
|
||||
setTemplateError(result.error || '템플릿을 불러올 수 없습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[InspectionModalV2] loadInspectionTemplate error:', error);
|
||||
setTemplateError('템플릿 로드 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoadingTemplate(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!doc) return null;
|
||||
|
||||
const docInfo = DOCUMENT_INFO[doc.type] || { label: doc.title, hasTemplate: false, color: 'text-gray-600' };
|
||||
@@ -362,6 +443,20 @@ export const InspectionModalV2 = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 수입검사 문서 렌더링 (Lazy Loading)
|
||||
const renderImportInspectionDocument = () => {
|
||||
if (isLoadingTemplate) {
|
||||
return <LoadingDocument />;
|
||||
}
|
||||
|
||||
if (templateError) {
|
||||
return <ErrorDocument message={templateError} onRetry={loadInspectionTemplate} />;
|
||||
}
|
||||
|
||||
// 템플릿이 로드되면 전달, 아니면 기본 템플릿 사용
|
||||
return <ImportInspectionDocument template={importTemplate || undefined} />;
|
||||
};
|
||||
|
||||
// 문서 타입에 따른 컨텐츠 렌더링
|
||||
const renderDocumentContent = () => {
|
||||
switch (doc.type) {
|
||||
@@ -374,7 +469,7 @@ export const InspectionModalV2 = ({
|
||||
case 'shipping':
|
||||
return <ShippingSlip data={MOCK_SHIPMENT_DETAIL} />;
|
||||
case 'import':
|
||||
return <ImportInspectionDocument />;
|
||||
return renderImportInspectionDocument();
|
||||
case 'product':
|
||||
return <ProductInspectionDocument />;
|
||||
case 'report':
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ import {
|
||||
revertProductionOrder,
|
||||
revertOrderConfirmation,
|
||||
deleteOrder,
|
||||
createProductionOrder,
|
||||
type Order,
|
||||
type OrderStatus,
|
||||
} from "@/components/orders";
|
||||
@@ -151,6 +152,14 @@ export default function OrderDetailPage() {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 생산지시 생성 모달 상태
|
||||
const [isProductionDialogOpen, setIsProductionDialogOpen] = useState(false);
|
||||
const [isCreatingProduction, setIsCreatingProduction] = useState(false);
|
||||
const [productionPriority, setProductionPriority] = useState<"normal" | "high" | "urgent">("normal");
|
||||
const [productionMemo, setProductionMemo] = useState("");
|
||||
// 생산지시 완료 알림 모달 상태
|
||||
const [isProductionSuccessDialogOpen, setIsProductionSuccessDialogOpen] = useState(false);
|
||||
|
||||
// 취소 폼 상태
|
||||
const [cancelReason, setCancelReason] = useState("");
|
||||
const [cancelDetail, setCancelDetail] = useState("");
|
||||
@@ -195,8 +204,42 @@ export default function OrderDetailPage() {
|
||||
};
|
||||
|
||||
const handleProductionOrder = () => {
|
||||
// 생산지시 생성 페이지로 이동
|
||||
router.push(`/sales/order-management-sales/${orderId}/production-order`);
|
||||
// 생산지시 생성 모달 열기
|
||||
setProductionPriority("normal");
|
||||
setProductionMemo("");
|
||||
setIsProductionDialogOpen(true);
|
||||
};
|
||||
|
||||
// 생산지시 확정 처리
|
||||
const handleProductionOrderSubmit = async () => {
|
||||
if (order) {
|
||||
setIsCreatingProduction(true);
|
||||
try {
|
||||
const result = await createProductionOrder(order.id, {
|
||||
priority: productionPriority,
|
||||
memo: productionMemo || undefined,
|
||||
});
|
||||
if (result.success && result.data) {
|
||||
// 주문 상태 업데이트
|
||||
setOrder({ ...order, status: "production_ordered" as OrderStatus });
|
||||
setIsProductionDialogOpen(false);
|
||||
// 성공 알림 모달 표시
|
||||
setIsProductionSuccessDialogOpen(true);
|
||||
} else {
|
||||
toast.error(result.error || "생산지시 생성에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating production order:", error);
|
||||
toast.error("생산지시 생성 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsCreatingProduction(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 생산지시 완료 알림 확인
|
||||
const handleProductionSuccessConfirm = () => {
|
||||
setIsProductionSuccessDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleViewProductionOrder = () => {
|
||||
@@ -419,53 +462,6 @@ export default function OrderDetailPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
@@ -473,10 +469,16 @@ export default function OrderDetailPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<InfoItem label="발주처" value={order.client} />
|
||||
<InfoItem label="로트번호" value={order.lotNumber} />
|
||||
<InfoItem label="접수일" value={order.orderDate} />
|
||||
<InfoItem label="수주처" value={order.client} />
|
||||
<InfoItem label="현장명" value={order.siteName} />
|
||||
<InfoItem label="담당자" value={order.manager} />
|
||||
<InfoItem label="연락처" value={order.contact} />
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">상태</p>
|
||||
<div className="mt-1">{getOrderStatusBadge(order.status)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -488,14 +490,16 @@ export default function OrderDetailPage() {
|
||||
</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.expectedShipDate || "미정"} />
|
||||
<InfoItem label="배송방식" value={order.deliveryMethod} />
|
||||
<InfoItem label="운임비용" value={order.shippingCost} />
|
||||
<InfoItem label="수신(반장/업체)" value={order.receiver} />
|
||||
<InfoItem label="수신자" value={order.receiver} />
|
||||
<InfoItem label="수신처 연락처" value={order.receiverContact} />
|
||||
<InfoItem label="수신처 주소" value={order.address} />
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-sm text-muted-foreground">주소</p>
|
||||
<p className="font-medium">{order.address || "-"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -512,11 +516,11 @@ export default function OrderDetailPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 제품 내역 (트리 구조) */}
|
||||
{/* 제품내용 (아코디언) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">제품 내역</CardTitle>
|
||||
<CardTitle className="text-lg">제품내용</CardTitle>
|
||||
{order.products && order.products.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -570,26 +574,14 @@ export default function OrderDetailPage() {
|
||||
<span className="font-medium">{product.productName}</span>
|
||||
{product.openWidth && product.openHeight && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
({product.openWidth} × {product.openHeight} mm)
|
||||
({product.openWidth} × {product.openHeight})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{product.floor && (
|
||||
<span className="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded">
|
||||
{product.floor}
|
||||
</span>
|
||||
)}
|
||||
{product.code && (
|
||||
<span className="text-xs font-medium text-gray-600 bg-gray-100 px-2 py-0.5 rounded">
|
||||
{product.code}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{productItems.length}개 부품
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
부품 {productItems.length}개
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 부품 목록 (확장 시 표시) */}
|
||||
@@ -600,34 +592,20 @@ export default function OrderDetailPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">순번</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>
|
||||
{productItems.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.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
||||
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice || 0)}원
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount || 0)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -642,114 +620,76 @@ export default function OrderDetailPage() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 매칭되지 않은 부품 (기타 부품) */}
|
||||
{(() => {
|
||||
const unmatchedItems = getUnmatchedItems();
|
||||
if (unmatchedItems.length === 0) return null;
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className="p-4 bg-gray-50 flex items-center gap-3">
|
||||
<Package className="h-5 w-5 text-gray-400" />
|
||||
<span className="font-medium text-gray-600">기타 부품</span>
|
||||
<span className="text-xs text-gray-500 ml-auto">
|
||||
{unmatchedItems.length}개 부품
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">순번</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>
|
||||
{unmatchedItems.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.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
||||
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice || 0)}원
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount || 0)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{/* 기타부품 (아코디언) */}
|
||||
{(() => {
|
||||
const unmatchedItems = getUnmatchedItems();
|
||||
if (unmatchedItems.length === 0) return null;
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">기타부품</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleProduct("other-parts")}
|
||||
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedProducts.has("other-parts") ? (
|
||||
<ChevronDown className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
<Package className="h-5 w-5 text-gray-400" />
|
||||
<span className="font-medium text-gray-600">기타부품</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
/* products가 없는 경우: 기존 부품 테이블 표시 */
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">순번</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 && order.items.length > 0 ? (
|
||||
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.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
||||
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice || 0)}원
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount || 0)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-gray-400 py-8">
|
||||
등록된 부품이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm text-gray-500">
|
||||
{unmatchedItems.length}개
|
||||
</span>
|
||||
</button>
|
||||
{expandedProducts.has("other-parts") && (
|
||||
<div className="border-t">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">순번</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead>규격</TableHead>
|
||||
<TableHead className="text-center">수량</TableHead>
|
||||
<TableHead className="text-center">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{unmatchedItems.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">{index + 1}</TableCell>
|
||||
<TableCell>{item.itemName}</TableCell>
|
||||
<TableCell>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
||||
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 합계 */}
|
||||
<div className="flex flex-col items-end gap-2 pt-4 mt-4 border-t">
|
||||
{/* 합계 */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">소계:</span>
|
||||
<span className="w-32 text-right">
|
||||
@@ -760,7 +700,7 @@ export default function OrderDetailPage() {
|
||||
<span className="text-muted-foreground">할인율:</span>
|
||||
<span className="w-32 text-right">{Number.isInteger(order.discountRate || 0) ? (order.discountRate || 0) : Math.round(order.discountRate || 0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-lg font-semibold">
|
||||
<div className="flex items-center gap-4 text-lg font-semibold border-t pt-2 mt-2">
|
||||
<span>총금액:</span>
|
||||
<span className="w-32 text-right text-green-600">
|
||||
{formatAmount(order.totalAmount || 0)}원
|
||||
@@ -771,79 +711,84 @@ export default function OrderDetailPage() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}, [order, expandedProducts, expandAllProducts, collapseAllProducts, toggleProduct, getItemsForProduct, getUnmatchedItems, openDocumentModal]);
|
||||
}, [order, expandedProducts, expandAllProducts, collapseAllProducts, toggleProduct, getItemsForProduct, getUnmatchedItems]);
|
||||
|
||||
// 견적 수정 핸들러
|
||||
const handleEditQuote = () => {
|
||||
if (order?.quoteId) {
|
||||
router.push(`/sales/quotes/${order.quoteId}?mode=edit`);
|
||||
}
|
||||
};
|
||||
|
||||
// 수주서 보기 핸들러
|
||||
const handleViewOrderDocument = () => {
|
||||
openDocumentModal("salesOrder");
|
||||
};
|
||||
|
||||
// 커스텀 헤더 액션 (상태별 버튼)
|
||||
const renderHeaderActions = useCallback(() => {
|
||||
if (!order) return null;
|
||||
|
||||
// 상태별 버튼 표시 여부
|
||||
const showEditQuoteButton = !!order.quoteId; // 연결된 견적이 있을 때
|
||||
const showEditButton = order.status !== "shipped" && order.status !== "cancelled";
|
||||
const showConfirmButton = order.status === "order_registered";
|
||||
const showProductionCreateButton = order.status === "order_confirmed";
|
||||
const showProductionViewButton = false;
|
||||
const showRevertButton = order.status === "production_ordered";
|
||||
const showRevertConfirmButton = order.status === "order_confirmed";
|
||||
const showCancelButton =
|
||||
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">
|
||||
{showEditButton && (
|
||||
<Button variant="outline" onClick={handleEdit}>
|
||||
{/* 견적 수정 */}
|
||||
{showEditQuoteButton && (
|
||||
<Button variant="outline" onClick={handleEditQuote}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
견적 수정
|
||||
</Button>
|
||||
)}
|
||||
{/* 수주서 보기 */}
|
||||
<Button variant="outline" onClick={handleViewOrderDocument}>
|
||||
<Eye 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>
|
||||
)}
|
||||
{showRevertButton && (
|
||||
<Button variant="outline" onClick={handleRevertProduction} className="border-amber-200 text-amber-600 hover:border-amber-300 hover:bg-amber-50">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
생산지시 되돌리기
|
||||
</Button>
|
||||
)}
|
||||
{/* 수주확정 되돌리기 */}
|
||||
{showRevertConfirmButton && (
|
||||
<Button variant="outline" onClick={handleRevertConfirmation} className="border-slate-300 text-slate-600 hover:border-slate-400 hover:bg-slate-50">
|
||||
<RotateCcw 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" />
|
||||
취소
|
||||
{/* 생산지시 생성 */}
|
||||
{showProductionCreateButton && (
|
||||
<Button onClick={handleProductionOrder}>
|
||||
<Factory className="h-4 w-4 mr-2" />
|
||||
생산지시 생성
|
||||
</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" />
|
||||
삭제
|
||||
{/* 생산지시 되돌리기 */}
|
||||
{showRevertButton && (
|
||||
<Button variant="outline" onClick={handleRevertProduction} className="border-amber-200 text-amber-600 hover:border-amber-300 hover:bg-amber-50">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
생산지시 되돌리기
|
||||
</Button>
|
||||
)}
|
||||
{/* 수정 */}
|
||||
{showEditButton && (
|
||||
<Button variant="outline" onClick={handleEdit}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel, handleDelete]);
|
||||
}, [order, handleEditQuote, handleViewOrderDocument, handleConfirmOrder, handleRevertConfirmation, handleProductionOrder, handleRevertProduction, handleEdit]);
|
||||
|
||||
// V2 패턴: ?mode=edit일 때 수정 컴포넌트 렌더링
|
||||
if (isEditMode) {
|
||||
@@ -886,6 +831,12 @@ export default function OrderDetailPage() {
|
||||
discountRate: order.discountRate,
|
||||
totalAmount: order.totalAmount,
|
||||
remarks: order.remarks,
|
||||
// 수주서 전용 필드
|
||||
documentNumber: order.lotNumber,
|
||||
certificationNumber: order.lotNumber,
|
||||
recipientName: order.receiver,
|
||||
recipientContact: order.receiverContact,
|
||||
shutterCount: order.products?.length || 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -1264,6 +1215,102 @@ export default function OrderDetailPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 생산지시 생성 모달 */}
|
||||
<Dialog open={isProductionDialogOpen} onOpenChange={setIsProductionDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>생산지시 생성</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* 우선순위 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-medium">우선순위</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={productionPriority === "urgent" ? "default" : "outline"}
|
||||
className={productionPriority === "urgent" ? "bg-gray-900 hover:bg-gray-800" : ""}
|
||||
onClick={() => setProductionPriority("urgent")}
|
||||
>
|
||||
긴급
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={productionPriority === "high" ? "default" : "outline"}
|
||||
className={productionPriority === "high" ? "bg-gray-900 hover:bg-gray-800" : ""}
|
||||
onClick={() => setProductionPriority("high")}
|
||||
>
|
||||
우선
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={productionPriority === "normal" ? "default" : "outline"}
|
||||
className={productionPriority === "normal" ? "bg-orange-500 hover:bg-orange-600" : ""}
|
||||
onClick={() => setProductionPriority("normal")}
|
||||
>
|
||||
일반
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="productionMemo">비고</Label>
|
||||
<Textarea
|
||||
id="productionMemo"
|
||||
placeholder="비고 사항을 입력하세요"
|
||||
value={productionMemo}
|
||||
onChange={(e) => setProductionMemo(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsProductionDialogOpen(false)}
|
||||
className="bg-gray-900 text-white hover:bg-gray-800"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleProductionOrderSubmit}
|
||||
className="bg-gray-900 hover:bg-gray-800"
|
||||
disabled={isCreatingProduction}
|
||||
>
|
||||
{isCreatingProduction ? "처리 중..." : "생산지시 확정"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 생산지시 완료 알림 모달 */}
|
||||
<Dialog open={isProductionSuccessDialogOpen} onOpenChange={setIsProductionSuccessDialogOpen}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">알림</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6 text-center space-y-2">
|
||||
<p className="font-medium text-lg">작업지시가 생성되었습니다.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
생산관리 > 작업지시 관리에서 확인하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="justify-center">
|
||||
<Button
|
||||
onClick={handleProductionSuccessConfirm}
|
||||
className="bg-gray-900 hover:bg-gray-800 min-w-[120px]"
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*
|
||||
* 수주 관리 페이지
|
||||
* - 상단 통계 카드: 이번 달 수주, 분할 대기, 생산지시 대기, 출하 대기
|
||||
* - 필터 탭: 전체, 수주등록, 수주확정, 생산지시완료, 미수
|
||||
* - 상태 필터: 셀렉트박스 (전체, 수주등록, N자수정, 수주확정, 생산지시완료)
|
||||
* - 날짜 범위 필터: 달력
|
||||
* - 완전한 반응형 지원
|
||||
* - API 연동 완료 (2025-01-08)
|
||||
*/
|
||||
@@ -31,8 +32,8 @@ import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||
import {
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type TabOption,
|
||||
type TableColumn,
|
||||
type FilterFieldConfig,
|
||||
} from "@/components/templates/UniversalListPage";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -126,11 +127,34 @@ function OrderListContent() {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filterType, setFilterType] = useState("all");
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 필터 상태
|
||||
const [startDate, setStartDate] = useState<string>("");
|
||||
const [endDate, setEndDate] = useState<string>("");
|
||||
|
||||
// 필터 상태
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
|
||||
status: 'all',
|
||||
});
|
||||
|
||||
// 상태 필터 설정 (filterConfig)
|
||||
const filterConfig: FilterFieldConfig[] = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'order_registered', label: '수주등록' },
|
||||
{ value: 'order_confirmed', label: '수주확정' },
|
||||
{ value: 'production_ordered', label: '생산지시완료' },
|
||||
],
|
||||
allOptionLabel: '전체',
|
||||
},
|
||||
];
|
||||
|
||||
// 취소 확인 다이얼로그 state
|
||||
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
||||
const [cancelTargetId, setCancelTargetId] = useState<string | null>(null);
|
||||
@@ -187,19 +211,14 @@ function OrderListContent() {
|
||||
const matchesSearch =
|
||||
!searchTerm ||
|
||||
order.lotNumber.toLowerCase().includes(searchLower) ||
|
||||
order.quoteNumber.toLowerCase().includes(searchLower) ||
|
||||
order.client.toLowerCase().includes(searchLower) ||
|
||||
order.siteName.toLowerCase().includes(searchLower);
|
||||
|
||||
// 상태 필터 (filterValues 사용)
|
||||
const statusFilter = filterValues.status as string;
|
||||
let matchesFilter = true;
|
||||
if (filterType === "registered") {
|
||||
matchesFilter = order.status === "order_registered";
|
||||
} else if (filterType === "confirmed") {
|
||||
matchesFilter = order.status === "order_confirmed";
|
||||
} else if (filterType === "production_ordered") {
|
||||
matchesFilter = order.status === "production_ordered";
|
||||
} else if (filterType === "receivable") {
|
||||
matchesFilter = order.hasReceivable === true;
|
||||
if (statusFilter && statusFilter !== "all") {
|
||||
matchesFilter = order.status === statusFilter;
|
||||
}
|
||||
|
||||
return matchesSearch && matchesFilter;
|
||||
@@ -249,10 +268,10 @@ function OrderListContent() {
|
||||
};
|
||||
}, [mobileDisplayCount, filteredOrders.length]);
|
||||
|
||||
// 탭이나 검색어 변경 시 모바일 표시 개수 초기화
|
||||
// 필터나 검색어 변경 시 모바일 표시 개수 초기화
|
||||
useEffect(() => {
|
||||
setMobileDisplayCount(20);
|
||||
}, [searchTerm, filterType]);
|
||||
}, [searchTerm, filterValues]);
|
||||
|
||||
// 통계 계산 (API stats 우선 사용, 없으면 로컬 계산)
|
||||
const now = new Date();
|
||||
@@ -453,54 +472,26 @@ function OrderListContent() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 탭 구성
|
||||
const tabs: TabOption[] = [
|
||||
{
|
||||
value: "all",
|
||||
label: "전체",
|
||||
count: orders.length,
|
||||
color: "blue",
|
||||
},
|
||||
{
|
||||
value: "registered",
|
||||
label: "수주등록",
|
||||
count: orders.filter((o) => o.status === "order_registered").length,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
value: "confirmed",
|
||||
label: "수주확정",
|
||||
count: orders.filter((o) => o.status === "order_confirmed").length,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
value: "production_ordered",
|
||||
label: "생산지시완료",
|
||||
count: orders.filter((o) => o.status === "production_ordered").length,
|
||||
color: "blue",
|
||||
},
|
||||
{
|
||||
value: "receivable",
|
||||
label: "미수",
|
||||
count: orders.filter((o) => o.hasReceivable).length,
|
||||
color: "red",
|
||||
},
|
||||
];
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
// 테이블 컬럼 정의 (16개: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)
|
||||
const tableColumns: TableColumn[] = [
|
||||
{ key: "rowNumber", label: "번호", className: "px-4" },
|
||||
{ key: "lotNumber", label: "로트번호", className: "px-4" },
|
||||
{ key: "quoteNumber", label: "견적번호", className: "px-4" },
|
||||
{ key: "client", label: "발주처", className: "px-4" },
|
||||
{ key: "siteName", label: "현장명", className: "px-4" },
|
||||
{ key: "status", label: "상태", className: "px-4" },
|
||||
{ key: "expectedShipDate", label: "출고예정일", className: "px-4" },
|
||||
{ key: "deliveryMethod", label: "배송방식", className: "px-4" },
|
||||
{ key: "actions", label: "작업", className: "px-4" },
|
||||
{ key: "rowNumber", label: "번호", className: "px-2 text-center" },
|
||||
{ key: "lotNumber", label: "로트번호", className: "px-2" },
|
||||
{ key: "siteName", label: "현장명", className: "px-2" },
|
||||
{ key: "expectedShipDate", label: "출고예정일", className: "px-2" },
|
||||
{ key: "orderDate", label: "접수일", className: "px-2" },
|
||||
{ key: "client", label: "수주처", className: "px-2" },
|
||||
{ key: "productName", label: "제품명", className: "px-2" },
|
||||
{ key: "receiver", label: "수신자", className: "px-2" },
|
||||
{ key: "receiverAddress", label: "수신주소", className: "px-2" },
|
||||
{ key: "receiverPlace", label: "수신처", className: "px-2" },
|
||||
{ key: "deliveryMethod", label: "배송", className: "px-2" },
|
||||
{ key: "manager", label: "담당자", className: "px-2" },
|
||||
{ key: "frameCount", label: "틀수", className: "px-2 text-center" },
|
||||
{ key: "status", label: "상태", className: "px-2" },
|
||||
{ key: "remarks", label: "비고", className: "px-2" },
|
||||
];
|
||||
|
||||
// 테이블 행 렌더링
|
||||
// 테이블 행 렌더링 (16개 컬럼: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)
|
||||
const renderTableRow = (
|
||||
order: Order,
|
||||
index: number,
|
||||
@@ -508,7 +499,6 @@ function OrderListContent() {
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
const itemId = order.id;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
@@ -522,52 +512,25 @@ function OrderListContent() {
|
||||
onCheckedChange={onToggle}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{globalIndex}</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded font-mono">
|
||||
{order.lotNumber}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded font-mono">
|
||||
{order.quoteNumber}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{order.client}</TableCell>
|
||||
<TableCell>{order.siteName}</TableCell>
|
||||
<TableCell>{getOrderStatusBadge(order.status)}</TableCell>
|
||||
<TableCell>{order.siteName || "-"}</TableCell>
|
||||
<TableCell>{order.expectedShipDate || "-"}</TableCell>
|
||||
<TableCell>{order.orderDate || "-"}</TableCell>
|
||||
<TableCell>{order.client || "-"}</TableCell>
|
||||
<TableCell>{(order as any).productName || "-"}</TableCell>
|
||||
<TableCell>{(order as any).receiver || "-"}</TableCell>
|
||||
<TableCell className="max-w-[150px] truncate">{(order as any).receiverAddress || "-"}</TableCell>
|
||||
<TableCell>{(order as any).receiverPlace || "-"}</TableCell>
|
||||
<TableCell>{order.deliveryMethodLabel || "-"}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleView(order)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(order)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 삭제 버튼 - shipped, cancelled 제외 모든 상태에서 표시 */}
|
||||
{order.status !== "shipped" && order.status !== "cancelled" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(order.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{(order as any).manager || "-"}</TableCell>
|
||||
<TableCell className="text-center">{(order as any).frameCount || "-"}</TableCell>
|
||||
<TableCell>{getOrderStatusBadge(order.status)}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate">{(order as any).remarks || "-"}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -697,12 +660,25 @@ function OrderListContent() {
|
||||
|
||||
columns: tableColumns,
|
||||
|
||||
tabs: tabs,
|
||||
defaultTab: filterType,
|
||||
// 탭 제거 - 상태 필터는 headerActions의 셀렉트박스로 대체
|
||||
// tabs: undefined,
|
||||
// defaultTab: undefined,
|
||||
|
||||
computeStats: () => stats,
|
||||
|
||||
searchPlaceholder: "로트번호, 견적번호, 발주처, 현장명 검색...",
|
||||
searchPlaceholder: "로트번호, 현장명, 수주처 검색...",
|
||||
|
||||
// 날짜 범위 필터
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
presetsPosition: 'inline',
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
onEndDateChange: setEndDate,
|
||||
dateField: "orderDate", // 접수일 기준 필터링
|
||||
},
|
||||
|
||||
itemsPerPage: itemsPerPage,
|
||||
|
||||
@@ -718,17 +694,21 @@ function OrderListContent() {
|
||||
);
|
||||
},
|
||||
|
||||
tabFilter: (order, activeTab) => {
|
||||
if (activeTab === "all") return true;
|
||||
if (activeTab === "registered") return order.status === "order_registered";
|
||||
if (activeTab === "confirmed") return order.status === "order_confirmed";
|
||||
if (activeTab === "production_ordered") return order.status === "production_ordered";
|
||||
if (activeTab === "receivable") return order.hasReceivable === true;
|
||||
return true;
|
||||
// 커스텀 필터 함수 (filterConfig 사용)
|
||||
customFilterFn: (items, fv) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
const statusVal = fv.status as string;
|
||||
// 상태 필터
|
||||
if (statusVal && statusVal !== 'all' && item.status !== statusVal) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
headerActions: () => (
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button variant="outline" onClick={handleSendNotification} disabled={isPending}>
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
수주완료
|
||||
@@ -740,6 +720,11 @@ function OrderListContent() {
|
||||
</div>
|
||||
),
|
||||
|
||||
// 필터 설정 (filterConfig 사용 - PC 인라인, 모바일 바텀시트)
|
||||
filterConfig,
|
||||
initialFilters: filterValues,
|
||||
filterTitle: '수주 필터',
|
||||
|
||||
renderTableRow: renderTableRow,
|
||||
|
||||
renderMobileCard: renderMobileCard,
|
||||
@@ -807,8 +792,8 @@ function OrderListContent() {
|
||||
selectedItems,
|
||||
onSelectionChange: setSelectedItems,
|
||||
}}
|
||||
onTabChange={(value) => {
|
||||
setFilterType(value);
|
||||
onFilterChange={(newFilters) => {
|
||||
setFilterValues(newFilters);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
onSearchChange={setSearchTerm}
|
||||
|
||||
Reference in New Issue
Block a user