fix(WEB): 영업 페이지 및 견적 타입 수정
- 생산지시 페이지 에러 처리 개선 - 견적관리 상세 페이지 개선 - quotes types 수정
This commit is contained in:
@@ -340,8 +340,7 @@ export default function ProductionOrderCreatePage() {
|
||||
|
||||
// 성공 다이얼로그 상태
|
||||
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
|
||||
const [generatedOrderNumber, setGeneratedOrderNumber] = useState("");
|
||||
const [generatedWorkOrderId, setGeneratedWorkOrderId] = useState<string | null>(null);
|
||||
const [generatedWorkOrders, setGeneratedWorkOrders] = useState<Array<{ workOrderNo: string; processName?: string }>>([]);
|
||||
|
||||
// 수주 데이터 및 공정 목록 로드
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -388,16 +387,39 @@ export default function ProductionOrderCreatePage() {
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
// 공정별 품목 그룹핑 (processIds 추출을 위해)
|
||||
const groups = groupItemsByProcess(
|
||||
(order.items || []).map((item) => ({
|
||||
itemName: item.itemName,
|
||||
itemCode: item.itemCode,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
processes
|
||||
);
|
||||
|
||||
// 모든 매칭된 공정 ID 추출 (공정별로 각각 작업지시 생성)
|
||||
const matchedGroups = groups.filter((g) => g.process.id !== "unmatched");
|
||||
const allProcessIds = matchedGroups
|
||||
.map((g) => parseInt(g.process.id, 10))
|
||||
.filter((id) => !isNaN(id));
|
||||
|
||||
const productionData: CreateProductionOrderData = {
|
||||
priority: selectedPriority,
|
||||
memo: memo || undefined,
|
||||
processIds: allProcessIds.length > 0 ? allProcessIds : undefined,
|
||||
};
|
||||
|
||||
const result = await createProductionOrder(orderId, productionData);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setGeneratedOrderNumber(result.data.workOrder.workOrderNo);
|
||||
setGeneratedWorkOrderId(result.data.workOrder.id);
|
||||
// 다중 작업지시 응답 처리
|
||||
const workOrders = result.data.workOrders || (result.data.workOrder ? [result.data.workOrder] : []);
|
||||
setGeneratedWorkOrders(
|
||||
workOrders.map((wo: { workOrderNo: string; process?: { processName: string } }) => ({
|
||||
workOrderNo: wo.workOrderNo,
|
||||
processName: wo.process?.processName,
|
||||
}))
|
||||
);
|
||||
setShowSuccessDialog(true);
|
||||
} else {
|
||||
setError(result.error || "생산지시 생성에 실패했습니다.");
|
||||
@@ -911,16 +933,27 @@ export default function ProductionOrderCreatePage() {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
생산지시가 생성되었습니다.
|
||||
작업지시가 {generatedWorkOrders.length}건 생성되었습니다.
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">생산지시번호:</p>
|
||||
<p className="font-mono font-semibold text-foreground">{generatedOrderNumber}</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">생성된 작업지시:</p>
|
||||
<ul className="space-y-1">
|
||||
{generatedWorkOrders.map((wo, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2">
|
||||
<span className="font-mono font-semibold text-foreground">{wo.workOrderNo}</span>
|
||||
{wo.processName && (
|
||||
<BadgeSm className="bg-blue-100 text-blue-700 border-blue-200">
|
||||
{wo.processName}
|
||||
</BadgeSm>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
생산관리 > 생산지시 관리에서 작업지시서를 생성하세요.
|
||||
생산관리 > 작업지시 관리에서 확인하세요.
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
|
||||
@@ -47,6 +47,9 @@ import {
|
||||
MessageCircle,
|
||||
X,
|
||||
FileCheck,
|
||||
Package,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
|
||||
@@ -69,6 +72,9 @@ export default function QuoteDetailPage() {
|
||||
const [showDetailedBreakdown, setShowDetailedBreakdown] = useState(true);
|
||||
const [showMaterialList, setShowMaterialList] = useState(true);
|
||||
|
||||
// BOM 자재 상세 펼침/접힘 상태
|
||||
const [isBomExpanded, setIsBomExpanded] = useState(true);
|
||||
|
||||
// 견적 데이터 조회
|
||||
const fetchQuote = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -464,6 +470,84 @@ export default function QuoteDetailPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* BOM 자재 상세 */}
|
||||
{quote.bomMaterials && quote.bomMaterials.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
BOM 자재 상세
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{quote.bomMaterials.length}개 품목
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsBomExpanded(!isBomExpanded)}
|
||||
>
|
||||
{isBomExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{isBomExpanded && (
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="text-left p-2 font-medium">No</th>
|
||||
<th className="text-left p-2 font-medium">품목코드</th>
|
||||
<th className="text-left p-2 font-medium">품목명</th>
|
||||
<th className="text-left p-2 font-medium">유형</th>
|
||||
<th className="text-left p-2 font-medium">규격</th>
|
||||
<th className="text-center p-2 font-medium">단위</th>
|
||||
<th className="text-right p-2 font-medium">수량</th>
|
||||
<th className="text-right p-2 font-medium">단가</th>
|
||||
<th className="text-right p-2 font-medium">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{quote.bomMaterials.map((material, index) => (
|
||||
<tr key={index} className="border-b hover:bg-muted/30">
|
||||
<td className="p-2 text-muted-foreground">{index + 1}</td>
|
||||
<td className="p-2 font-mono text-xs">{material.itemCode}</td>
|
||||
<td className="p-2">{material.itemName}</td>
|
||||
<td className="p-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{material.itemType === 'RM' ? '원자재' :
|
||||
material.itemType === 'SM' ? '부자재' :
|
||||
material.itemType === 'CS' ? '소모품' : material.itemType}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-2 text-muted-foreground">{material.specification || '-'}</td>
|
||||
<td className="p-2 text-center">{material.unit}</td>
|
||||
<td className="p-2 text-right">{material.quantity.toLocaleString()}</td>
|
||||
<td className="p-2 text-right">₩{material.unitPrice.toLocaleString()}</td>
|
||||
<td className="p-2 text-right font-medium">₩{material.totalPrice.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 bg-muted/30">
|
||||
<td colSpan={8} className="p-2 text-right font-medium">합계</td>
|
||||
<td className="p-2 text-right font-bold text-blue-600">
|
||||
₩{quote.bomMaterials.reduce((sum, m) => sum + m.totalPrice, 0).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 견적서 다이얼로그 */}
|
||||
<Dialog open={isQuoteDocumentOpen} onOpenChange={setIsQuoteDocumentOpen}>
|
||||
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] flex flex-col p-0">
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface QuoteItem {
|
||||
id: string;
|
||||
quoteId: string;
|
||||
productId?: string;
|
||||
itemCode?: string; // 품목코드 (item_code)
|
||||
productName: string;
|
||||
specification?: string;
|
||||
unit?: string;
|
||||
@@ -298,6 +299,7 @@ export function transformItemApiToFrontend(apiData: QuoteItemApiData): QuoteItem
|
||||
id: String(apiData.id),
|
||||
quoteId: String(apiData.quote_id),
|
||||
productId: apiData.item_id ? String(apiData.item_id) : (apiData.product_id ? String(apiData.product_id) : undefined),
|
||||
itemCode: apiData.item_code || undefined, // 품목코드
|
||||
productName,
|
||||
specification: apiData.specification || undefined,
|
||||
unit: apiData.unit || undefined,
|
||||
@@ -545,7 +547,7 @@ export function transformQuoteToFormData(quote: Quote): QuoteFormData {
|
||||
? quote.items.map((item, index) => ({
|
||||
itemIndex: index,
|
||||
finishedGoodsCode: '',
|
||||
itemCode: item.productId || item.id || '',
|
||||
itemCode: item.itemCode || '', // 품목코드 사용
|
||||
itemName: item.productName,
|
||||
itemType: '',
|
||||
itemCategory: '',
|
||||
|
||||
Reference in New Issue
Block a user