fix(WEB): 수주 페이지 필드 매핑 및 제품-부품 트리 구조 개선
- ApiClient 인터페이스: representative → manager_name, contact_person 변경 - transformApiToFrontend: client.representative → client.manager_name 수정 - ApiOrderItem에 floor_code, symbol_code 필드 추가 (제품-부품 매핑) - ApiOrder에 options 타입 정의 추가 - ApiQuote에 calculation_inputs 타입 정의 추가 - 수주 상세 페이지 제품-부품 트리 구조 UI 개선
This commit is contained in:
@@ -1,26 +1,562 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트
|
||||
* 수주 수정 페이지
|
||||
*
|
||||
* - 기본 정보 (읽기전용)
|
||||
* - 수주/배송 정보 (편집 가능)
|
||||
* - 비고 (편집 가능)
|
||||
* - 품목 내역 (생산 시작 후 수정 불가)
|
||||
*/
|
||||
export default function OrderEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace(`/sales/order-management-sales/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { FileText, AlertTriangle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { PageLayout } from "@/components/organisms/PageLayout";
|
||||
import { PageHeader } from "@/components/organisms/PageHeader";
|
||||
import { FormActions } from "@/components/organisms/FormActions";
|
||||
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||
import { formatAmount } from "@/utils/formatAmount";
|
||||
import {
|
||||
OrderItem,
|
||||
getOrderById,
|
||||
updateOrder,
|
||||
type OrderStatus,
|
||||
} from "@/components/orders";
|
||||
|
||||
// 수정 폼 데이터
|
||||
interface EditFormData {
|
||||
// 읽기전용 정보
|
||||
lotNumber: string;
|
||||
quoteNumber: string;
|
||||
client: string;
|
||||
siteName: string;
|
||||
manager: string;
|
||||
contact: string;
|
||||
status: OrderStatus;
|
||||
|
||||
// 수정 가능 정보
|
||||
expectedShipDate: string;
|
||||
expectedShipDateUndecided: boolean;
|
||||
deliveryRequestDate: string;
|
||||
deliveryMethod: string;
|
||||
shippingCost: string;
|
||||
receiver: string;
|
||||
receiverContact: string;
|
||||
address: string;
|
||||
addressDetail: string;
|
||||
remarks: string;
|
||||
|
||||
// 품목 (수정 제한)
|
||||
items: OrderItem[];
|
||||
canEditItems: boolean;
|
||||
subtotal: number;
|
||||
discountRate: number;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
// 배송방식 옵션
|
||||
const DELIVERY_METHODS = [
|
||||
{ value: "direct", label: "직접배차" },
|
||||
{ value: "pickup", label: "상차" },
|
||||
{ value: "courier", label: "택배" },
|
||||
];
|
||||
|
||||
// 운임비용 옵션
|
||||
const SHIPPING_COSTS = [
|
||||
{ value: "free", label: "무료" },
|
||||
{ value: "prepaid", label: "선불" },
|
||||
{ value: "collect", label: "착불" },
|
||||
{ value: "negotiable", label: "협의" },
|
||||
];
|
||||
|
||||
|
||||
// 상태 뱃지 헬퍼
|
||||
function getOrderStatusBadge(status: OrderStatus) {
|
||||
const statusConfig: Record<OrderStatus, { label: string; className: string }> = {
|
||||
order_registered: { label: "수주등록", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
||||
order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
||||
production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
||||
in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" },
|
||||
rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" },
|
||||
work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" },
|
||||
shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" },
|
||||
cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" },
|
||||
};
|
||||
const config = statusConfig[status];
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
<BadgeSm className={config.className}>
|
||||
{config.label}
|
||||
</BadgeSm>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrderEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const orderId = params.id as string;
|
||||
|
||||
const [form, setForm] = useState<EditFormData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 데이터 로드 (API)
|
||||
useEffect(() => {
|
||||
async function loadOrder() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await getOrderById(orderId);
|
||||
if (result.success && result.data) {
|
||||
const order = result.data;
|
||||
// 상태에 따라 품목 수정 가능 여부 결정
|
||||
const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes(
|
||||
order.status
|
||||
);
|
||||
// Order 데이터를 EditFormData로 변환
|
||||
setForm({
|
||||
lotNumber: order.lotNumber,
|
||||
quoteNumber: order.quoteNumber || "",
|
||||
client: order.client,
|
||||
siteName: order.siteName,
|
||||
manager: order.manager || "",
|
||||
contact: order.contact || "",
|
||||
status: order.status,
|
||||
expectedShipDate: order.expectedShipDate || "",
|
||||
expectedShipDateUndecided: !order.expectedShipDate,
|
||||
deliveryRequestDate: order.deliveryRequestDate || "",
|
||||
deliveryMethod: order.deliveryMethod || "",
|
||||
shippingCost: order.shippingCost || "",
|
||||
receiver: order.receiver || "",
|
||||
receiverContact: order.receiverContact || "",
|
||||
address: order.address || "",
|
||||
addressDetail: order.addressDetail || "",
|
||||
remarks: order.remarks || "",
|
||||
items: order.items || [],
|
||||
canEditItems,
|
||||
subtotal: order.subtotal || order.amount,
|
||||
discountRate: order.discountRate || 0,
|
||||
totalAmount: order.amount,
|
||||
});
|
||||
} else {
|
||||
toast.error(result.error || "수주 정보를 불러오는데 실패했습니다.");
|
||||
router.push("/sales/order-management-sales");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading order:", error);
|
||||
toast.error("수주 정보를 불러오는 중 오류가 발생했습니다.");
|
||||
router.push("/sales/order-management-sales");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadOrder();
|
||||
}, [orderId, router]);
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push(`/sales/order-management-sales/${orderId}`);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form) return;
|
||||
|
||||
// 유효성 검사
|
||||
if (!form.deliveryRequestDate) {
|
||||
toast.error("납품요청일을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!form.receiver.trim()) {
|
||||
toast.error("수신(반장/업체)을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!form.receiverContact.trim()) {
|
||||
toast.error("수신처 연락처를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// API 연동
|
||||
// 주의: clientId를 보내지 않으면 기존 값 유지됨
|
||||
// clientName과 clientContact는 반드시 보내야 기존 값이 유지됨
|
||||
const result = await updateOrder(orderId, {
|
||||
clientName: form.client,
|
||||
clientContact: form.contact,
|
||||
siteName: form.siteName,
|
||||
expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate,
|
||||
deliveryRequestDate: form.deliveryRequestDate,
|
||||
deliveryMethod: form.deliveryMethod,
|
||||
shippingCost: form.shippingCost,
|
||||
receiver: form.receiver,
|
||||
receiverContact: form.receiverContact,
|
||||
address: form.address,
|
||||
addressDetail: form.addressDetail,
|
||||
remarks: form.remarks,
|
||||
items: form.items.map((item) => ({
|
||||
itemId: item.id ? parseInt(item.id, 10) : undefined,
|
||||
itemCode: item.itemCode,
|
||||
itemName: item.itemName,
|
||||
specification: item.spec,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
unitPrice: item.unitPrice,
|
||||
})),
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success("수주가 수정되었습니다.");
|
||||
router.push(`/sales/order-management-sales/${orderId}`);
|
||||
} else {
|
||||
toast.error(result.error || "수주 수정에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating order:", error);
|
||||
toast.error("수주 수정 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || !form) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 */}
|
||||
<PageHeader
|
||||
title="수주 수정"
|
||||
icon={FileText}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||
{form.lotNumber}
|
||||
</code>
|
||||
{getOrderStatusBadge(form.status)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 (읽기전용) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
기본 정보
|
||||
<span className="text-sm font-normal text-muted-foreground">(읽기전용)</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">로트번호</Label>
|
||||
<p className="font-medium">{form.lotNumber}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">견적번호</Label>
|
||||
<p className="font-medium">{form.quoteNumber}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">담당자</Label>
|
||||
<p className="font-medium">{form.manager}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">발주처</Label>
|
||||
<p className="font-medium">{form.client}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">현장명</Label>
|
||||
<p className="font-medium">{form.siteName}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">연락처</Label>
|
||||
<p className="font-medium">{form.contact}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 수주/배송 정보 (편집 가능) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">수주/배송 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 출고예정일 */}
|
||||
<div className="space-y-2">
|
||||
<Label>출고예정일</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
type="date"
|
||||
value={form.expectedShipDate}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, expectedShipDate: e.target.value })
|
||||
}
|
||||
disabled={form.expectedShipDateUndecided}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="expectedShipDateUndecided"
|
||||
checked={form.expectedShipDateUndecided}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm({
|
||||
...form,
|
||||
expectedShipDateUndecided: checked as boolean,
|
||||
expectedShipDate: checked ? "" : form.expectedShipDate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="expectedShipDateUndecided"
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
미정
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 납품요청일 */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
납품요청일 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={form.deliveryRequestDate}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, deliveryRequestDate: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 배송방식 */}
|
||||
<div className="space-y-2">
|
||||
<Label>배송방식</Label>
|
||||
<Select
|
||||
value={form.deliveryMethod}
|
||||
onValueChange={(value) =>
|
||||
setForm({ ...form, deliveryMethod: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="배송방식 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DELIVERY_METHODS.map((method) => (
|
||||
<SelectItem key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 운임비용 */}
|
||||
<div className="space-y-2">
|
||||
<Label>운임비용</Label>
|
||||
<Select
|
||||
value={form.shippingCost}
|
||||
onValueChange={(value) =>
|
||||
setForm({ ...form, shippingCost: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="운임비용 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SHIPPING_COSTS.map((cost) => (
|
||||
<SelectItem key={cost.value} value={cost.value}>
|
||||
{cost.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 수신(반장/업체) */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
수신(반장/업체) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={form.receiver}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, receiver: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수신처 연락처 */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
수신처 연락처 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={form.receiverContact}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, receiverContact: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수신처 주소 */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>수신처 주소</Label>
|
||||
<Input
|
||||
value={form.address}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, address: e.target.value })
|
||||
}
|
||||
placeholder="주소"
|
||||
className="mb-2"
|
||||
/>
|
||||
<Input
|
||||
value={form.addressDetail}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, addressDetail: e.target.value })
|
||||
}
|
||||
placeholder="상세주소"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 비고 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">비고</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={form.remarks}
|
||||
onChange={(e) => setForm({ ...form, remarks: e.target.value })}
|
||||
placeholder="특이사항을 입력하세요"
|
||||
rows={4}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 품목 내역 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
품목 내역
|
||||
{!form.canEditItems && (
|
||||
<span className="flex items-center gap-1 text-sm font-normal text-orange-600">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
생산 시작 후 수정 불가
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">No</TableHead>
|
||||
<TableHead>품목코드</TableHead>
|
||||
<TableHead>품명</TableHead>
|
||||
<TableHead>종</TableHead>
|
||||
<TableHead>부호</TableHead>
|
||||
<TableHead>규격(mm)</TableHead>
|
||||
<TableHead className="text-center">수량</TableHead>
|
||||
<TableHead className="text-center">단위</TableHead>
|
||||
<TableHead className="text-right">단가</TableHead>
|
||||
<TableHead className="text-right">금액</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{form.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.type || "-"}</TableCell>
|
||||
<TableCell>{item.symbol || "-"}</TableCell>
|
||||
<TableCell>{item.spec}</TableCell>
|
||||
<TableCell className="text-center">{item.quantity}</TableCell>
|
||||
<TableCell className="text-center">{item.unit}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice)}원
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 합계 */}
|
||||
<div className="flex flex-col items-end gap-2 pt-4 mt-4 border-t">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">소계:</span>
|
||||
<span className="w-32 text-right">
|
||||
{formatAmount(form.subtotal)}원
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">할인율:</span>
|
||||
<span className="w-32 text-right">{form.discountRate}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-lg font-semibold">
|
||||
<span>총금액:</span>
|
||||
<span className="w-32 text-right text-green-600">
|
||||
{formatAmount(form.totalAmount)}원
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="sticky bottom-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t pt-4 pb-4 -mx-3 md:-mx-6 px-3 md:px-6 mt-6">
|
||||
<FormActions
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
saveLabel="저장"
|
||||
cancelLabel="취소"
|
||||
saveLoading={isSaving}
|
||||
saveDisabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,10 +100,11 @@ export default function EstimateDetailForm({
|
||||
console.log('🔍 [handleConfirmSave] formData.priceAdjustmentData:', formData.priceAdjustmentData);
|
||||
console.log('🔍 [handleConfirmSave] formData 전체:', formData);
|
||||
|
||||
// 현재 사용자 이름을 견적자로 설정하여 저장
|
||||
// 현재 사용자 이름을 견적자로 설정하고, 상태를 견적완료로 변경하여 저장
|
||||
const result = await updateEstimate(estimateId, {
|
||||
...formData,
|
||||
estimatorName: currentUser!.name,
|
||||
status: 'completed', // 저장 시 견적완료 상태로 변경 (입찰에서 조회 가능)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -30,6 +30,13 @@ interface ApiOrder {
|
||||
memo: string | null;
|
||||
remarks: string | null;
|
||||
note: string | null;
|
||||
options: {
|
||||
shipping_cost_code?: string;
|
||||
receiver?: string;
|
||||
receiver_contact?: string;
|
||||
shipping_address?: string;
|
||||
shipping_address_detail?: string;
|
||||
} | null;
|
||||
created_by: number | null;
|
||||
updated_by: number | null;
|
||||
created_at: string;
|
||||
@@ -45,6 +52,9 @@ interface ApiOrderItem {
|
||||
item_id: number | null;
|
||||
item_name: string;
|
||||
specification: string | null;
|
||||
// 제품-부품 매핑용 코드
|
||||
floor_code: string | null;
|
||||
symbol_code: string | null;
|
||||
quantity: number;
|
||||
unit: string | null;
|
||||
unit_price: number;
|
||||
@@ -58,7 +68,8 @@ interface ApiClient {
|
||||
id: number;
|
||||
name: string;
|
||||
business_no?: string;
|
||||
representative?: string;
|
||||
contact_person?: string;
|
||||
manager_name?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
}
|
||||
@@ -68,6 +79,17 @@ interface ApiQuote {
|
||||
quote_no: string;
|
||||
quote_number?: string;
|
||||
site_name: string | null;
|
||||
calculation_inputs?: {
|
||||
items?: Array<{
|
||||
productCategory?: string;
|
||||
productName?: string;
|
||||
openWidth?: string;
|
||||
openHeight?: string;
|
||||
quantity?: number;
|
||||
floor?: string;
|
||||
code?: string;
|
||||
}>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// 견적 목록 조회용 상세 타입
|
||||
@@ -215,6 +237,16 @@ export interface Order {
|
||||
subtotal?: number; // 소계 (supply_amount와 동일)
|
||||
discountRate?: number; // 할인율
|
||||
totalAmount?: number; // 총금액 (amount와 동일하지만 명시적)
|
||||
// 제품 정보 (견적의 calculation_inputs에서 가져옴)
|
||||
products?: Array<{
|
||||
productName: string;
|
||||
productCategory?: string;
|
||||
openWidth?: string;
|
||||
openHeight?: string;
|
||||
quantity: number;
|
||||
floor?: string;
|
||||
code?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
@@ -411,17 +443,28 @@ function transformApiToFrontend(apiData: ApiOrder): Order {
|
||||
remarks: apiData.remarks ?? undefined,
|
||||
note: apiData.note ?? undefined,
|
||||
items: apiData.items?.map(transformItemApiToFrontend) || [], // 상세 페이지용 추가 필드 (API에서 매핑)
|
||||
manager: apiData.client?.representative ?? undefined,
|
||||
manager: apiData.client?.manager_name ?? undefined,
|
||||
contact: apiData.client_contact ?? apiData.client?.phone ?? undefined,
|
||||
deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유
|
||||
shippingCost: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||
receiver: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||
receiverContact: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||
address: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||
addressDetail: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||
// options JSON에서 추출
|
||||
shippingCost: apiData.options?.shipping_cost_code ?? undefined,
|
||||
receiver: apiData.options?.receiver ?? undefined,
|
||||
receiverContact: apiData.options?.receiver_contact ?? undefined,
|
||||
address: apiData.options?.shipping_address ?? undefined,
|
||||
addressDetail: apiData.options?.shipping_address_detail ?? undefined,
|
||||
subtotal: apiData.supply_amount,
|
||||
discountRate: apiData.discount_rate,
|
||||
totalAmount: apiData.total_amount,
|
||||
// 제품 정보 (견적의 calculation_inputs에서 추출)
|
||||
products: apiData.quote?.calculation_inputs?.items?.map(item => ({
|
||||
productName: item.productName || '',
|
||||
productCategory: item.productCategory,
|
||||
openWidth: item.openWidth,
|
||||
openHeight: item.openHeight,
|
||||
quantity: item.quantity || 1,
|
||||
floor: item.floor,
|
||||
code: item.code,
|
||||
})) || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,8 +476,8 @@ function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem {
|
||||
itemName: apiItem.item_name,
|
||||
specification: apiItem.specification ?? undefined,
|
||||
spec: apiItem.specification ?? undefined, // specification alias
|
||||
type: undefined, // 층 - API에 해당 필드 없음
|
||||
symbol: undefined, // 부호 - API에 해당 필드 없음
|
||||
type: apiItem.floor_code ?? undefined, // 층 코드 (제품-부품 매핑용)
|
||||
symbol: apiItem.symbol_code ?? undefined, // 부호 코드 (제품-부품 매핑용)
|
||||
quantity: apiItem.quantity,
|
||||
unit: apiItem.unit ?? undefined,
|
||||
unitPrice: apiItem.unit_price,
|
||||
@@ -461,11 +504,13 @@ function transformFrontendToApi(data: OrderFormData | Record<string, unknown>):
|
||||
const selectedQuotation = formData.selectedQuotation as { id?: string } | undefined;
|
||||
const quoteIdValue = selectedQuotation?.id ? parseInt(selectedQuotation.id, 10) || null : null;
|
||||
|
||||
return {
|
||||
// Build result object - only include client_id if explicitly provided
|
||||
// to avoid overwriting existing value with null on update
|
||||
const result: Record<string, unknown> = {
|
||||
quote_id: quoteIdValue,
|
||||
order_type_code: formData.orderTypeCode || 'ORDER',
|
||||
category_code: formData.categoryCode || null,
|
||||
client_id: clientIdValue,
|
||||
// client_id is conditionally added below
|
||||
client_name: formData.clientName || null,
|
||||
client_contact: formData.clientContact || formData.contact || null,
|
||||
site_name: formData.siteName || null,
|
||||
@@ -480,6 +525,14 @@ function transformFrontendToApi(data: OrderFormData | Record<string, unknown>):
|
||||
memo: formData.memo || null,
|
||||
remarks: formData.remarks || null,
|
||||
note: formData.note || null,
|
||||
// options JSON으로 묶어서 저장 (운임비용, 수신자, 수신처 연락처, 주소)
|
||||
options: {
|
||||
shipping_cost_code: formData.shippingCost || null,
|
||||
receiver: formData.receiver || null,
|
||||
receiver_contact: formData.receiverContact || null,
|
||||
shipping_address: formData.address || null,
|
||||
shipping_address_detail: formData.addressDetail || null,
|
||||
},
|
||||
items: items.map((item) => {
|
||||
// Handle both form's OrderItem (id, spec) and API's OrderItemFormData (itemId, specification)
|
||||
// 중요: 문자열로 전달될 수 있으므로 반드시 Number()로 변환
|
||||
@@ -502,6 +555,14 @@ function transformFrontendToApi(data: OrderFormData | Record<string, unknown>):
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
// Only include client_id if explicitly provided (not undefined)
|
||||
// This prevents overwriting existing client_id with null on update
|
||||
if (clientId !== undefined) {
|
||||
result.client_id = clientIdValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function transformWorkOrderApiToFrontend(apiData: ApiWorkOrder): WorkOrder {
|
||||
@@ -1022,6 +1083,117 @@ export async function createProductionOrder(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 되돌리기 (작업지시 및 관련 데이터 삭제)
|
||||
*/
|
||||
export async function revertProductionOrder(orderId: string): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
order: Order;
|
||||
deletedCounts: {
|
||||
workResults: number;
|
||||
workOrderItems: number;
|
||||
workOrders: number;
|
||||
};
|
||||
previousStatus: string;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/revert-production`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '생산지시 되돌리기에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{
|
||||
order: ApiOrder;
|
||||
deleted_counts: {
|
||||
work_results: number;
|
||||
work_order_items: number;
|
||||
work_orders: number;
|
||||
};
|
||||
previous_status: string;
|
||||
}> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '생산지시 되돌리기에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
order: transformApiToFrontend(result.data.order),
|
||||
deletedCounts: {
|
||||
workResults: result.data.deleted_counts.work_results,
|
||||
workOrderItems: result.data.deleted_counts.work_order_items,
|
||||
workOrders: result.data.deleted_counts.work_orders,
|
||||
},
|
||||
previousStatus: result.data.previous_status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[revertProductionOrder] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주확정 되돌리기 (수주등록 상태로 변경)
|
||||
*/
|
||||
export async function revertOrderConfirmation(orderId: string): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
order: Order;
|
||||
previousStatus: string;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/revert-confirmation`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '수주확정 되돌리기에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{
|
||||
order: ApiOrder;
|
||||
previous_status: string;
|
||||
}> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '수주확정 되돌리기에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
order: transformApiToFrontend(result.data.order),
|
||||
previousStatus: result.data.previous_status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[revertOrderConfirmation] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 변환용 확정 견적 목록 조회
|
||||
* QuotationSelectDialog에서 사용
|
||||
|
||||
@@ -3,28 +3,40 @@
|
||||
/**
|
||||
* 계약서 문서 컴포넌트
|
||||
* - 스크린샷 형식 + 지출결의서 디자인 스타일
|
||||
* - 제품 정보는 견적의 calculation_inputs에서 추출한 products로 표시
|
||||
*/
|
||||
|
||||
import { formatAmount } from "@/utils/formatAmount";
|
||||
import { OrderItem } from "@/components/orders";
|
||||
import { OrderItem } from "../actions";
|
||||
|
||||
// 제품 정보 타입
|
||||
interface ProductInfo {
|
||||
productName: string;
|
||||
productCategory?: string;
|
||||
openWidth?: string;
|
||||
openHeight?: string;
|
||||
quantity: number;
|
||||
floor?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
interface ContractDocumentProps {
|
||||
orderNumber: string;
|
||||
orderDate: string;
|
||||
client: string;
|
||||
clientBusinessNumber?: string;
|
||||
clientCeo?: string;
|
||||
siteName?: string;
|
||||
clientManager?: string;
|
||||
clientContact?: string;
|
||||
clientAddress?: string;
|
||||
companyName?: string;
|
||||
companyCeo?: string;
|
||||
companyBusinessNumber?: string;
|
||||
companyContact?: string;
|
||||
companyAddress?: string;
|
||||
items: OrderItem[];
|
||||
subtotal: number;
|
||||
discountRate: number;
|
||||
totalAmount: number;
|
||||
items?: OrderItem[];
|
||||
products?: ProductInfo[];
|
||||
subtotal?: number;
|
||||
discountRate?: number;
|
||||
totalAmount?: number;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
@@ -32,19 +44,19 @@ export function ContractDocument({
|
||||
orderNumber,
|
||||
orderDate,
|
||||
client,
|
||||
clientBusinessNumber = "123-45-67890",
|
||||
clientCeo = "대표자",
|
||||
clientContact = "02-1234-5678",
|
||||
clientAddress = "서울시 강남구",
|
||||
companyName = "(주)케이디산업",
|
||||
companyCeo = "김대표",
|
||||
companyBusinessNumber = "111-22-33333",
|
||||
companyContact = "02-9999-8888",
|
||||
companyAddress = "경기도 화성시 케이디로 123",
|
||||
items,
|
||||
subtotal,
|
||||
discountRate,
|
||||
totalAmount,
|
||||
siteName,
|
||||
clientManager,
|
||||
clientContact,
|
||||
companyName,
|
||||
companyCeo,
|
||||
companyBusinessNumber,
|
||||
companyContact,
|
||||
companyAddress,
|
||||
items = [],
|
||||
products,
|
||||
subtotal = 0,
|
||||
discountRate = 0,
|
||||
totalAmount = 0,
|
||||
remarks,
|
||||
}: ContractDocumentProps) {
|
||||
const discountAmount = Math.round(subtotal * (discountRate / 100));
|
||||
@@ -62,51 +74,63 @@ export function ContractDocument({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 제품명 */}
|
||||
{/* 제품 정보 (개소별) */}
|
||||
<div className="border border-gray-300 mb-4">
|
||||
<div className="bg-gray-800 text-white p-2 text-sm font-medium text-center">
|
||||
제품명
|
||||
수주 제품 (개소별 사이즈)
|
||||
</div>
|
||||
<div className="p-3 text-center text-sm">
|
||||
스크린 세터 (표준형)
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
{products && products.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{products.map((product, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded p-3 bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-gray-500 bg-white px-2 py-0.5 rounded border">
|
||||
항목 {index + 1}
|
||||
</span>
|
||||
{product.floor && (
|
||||
<span className="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded">
|
||||
{product.floor}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 수주물목 테이블 */}
|
||||
<div className="border border-gray-300 mb-4">
|
||||
<div className="bg-gray-800 text-white p-2 text-sm font-medium text-center">
|
||||
수주물목 (개소별 사이즈)
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 text-xs">제품명</span>
|
||||
<p className="font-medium">{product.productName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 text-xs">오픈사이즈</span>
|
||||
<p className="font-medium">
|
||||
{product.openWidth && product.openHeight
|
||||
? `${product.openWidth} × ${product.openHeight} mm`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 text-xs">수량</span>
|
||||
<p className="font-medium">{product.quantity} SET</p>
|
||||
</div>
|
||||
{product.code && (
|
||||
<div>
|
||||
<span className="text-gray-500 text-xs">부호</span>
|
||||
<p className="font-medium">{product.code}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-gray-400 py-4">
|
||||
등록된 제품이 없습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 border-b border-gray-300">
|
||||
<th className="p-2 text-center font-medium border-r border-gray-300 w-24">품목코드</th>
|
||||
<th className="p-2 text-left font-medium border-r border-gray-300">품명</th>
|
||||
<th className="p-2 text-center font-medium border-r border-gray-300 w-28">규격</th>
|
||||
<th className="p-2 text-center font-medium border-r border-gray-300 w-16">수량</th>
|
||||
<th className="p-2 text-center font-medium w-16">단위</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<tr key={item.id} className="border-b border-gray-300">
|
||||
<td className="p-2 text-center border-r border-gray-300">{item.itemCode}</td>
|
||||
<td className="p-2 border-r border-gray-300">{item.itemName}</td>
|
||||
<td className="p-2 text-center border-r border-gray-300">{item.spec}</td>
|
||||
<td className="p-2 text-center border-r border-gray-300">{item.quantity}</td>
|
||||
<td className="p-2 text-center">{item.unit}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr className="border-b border-gray-300">
|
||||
<td colSpan={5} className="p-4 text-center text-gray-400">
|
||||
등록된 품목이 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 발주처/당사 정보 */}
|
||||
@@ -118,19 +142,19 @@ export function ContractDocument({
|
||||
<div className="p-3 space-y-1 text-sm">
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">업체명</span>
|
||||
<span>{client}</span>
|
||||
<span>{client || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">현장명</span>
|
||||
<span>-</span>
|
||||
<span>{siteName || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">담당자</span>
|
||||
<span>{clientCeo}</span>
|
||||
<span>{clientManager || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">연락처</span>
|
||||
<span>{clientContact}</span>
|
||||
<span>{clientContact || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,23 +165,23 @@ export function ContractDocument({
|
||||
<div className="p-3 space-y-1 text-sm">
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">업체명</span>
|
||||
<span>{companyName}</span>
|
||||
<span>{companyName || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">대표자</span>
|
||||
<span>{companyCeo}</span>
|
||||
<span>{companyCeo || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">사업자번호</span>
|
||||
<span>{companyBusinessNumber}</span>
|
||||
<span>{companyBusinessNumber || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">연락처</span>
|
||||
<span>{companyContact}</span>
|
||||
<span>{companyContact || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="w-20 text-gray-600">주소</span>
|
||||
<span>{companyAddress}</span>
|
||||
<span>{companyAddress || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,21 +202,21 @@ export function ContractDocument({
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300 w-32">공급가액</td>
|
||||
<td className="p-2 text-right border-r border-gray-300">{formatAmount(subtotal)}원</td>
|
||||
<td className="p-2 text-right border-r border-gray-300">{formatAmount(subtotal)}</td>
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300 w-32">할인율</td>
|
||||
<td className="p-2 text-right">{discountRate}%</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300">할인액</td>
|
||||
<td className="p-2 text-right border-r border-gray-300 text-red-600">-{formatAmount(discountAmount)}원</td>
|
||||
<td className="p-2 text-right border-r border-gray-300 text-red-600">-{formatAmount(discountAmount)}</td>
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300">할인 후 공급가액</td>
|
||||
<td className="p-2 text-right">{formatAmount(afterDiscount)}원</td>
|
||||
<td className="p-2 text-right">{formatAmount(afterDiscount)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300">부가세(10%)</td>
|
||||
<td className="p-2 text-right border-r border-gray-300">{formatAmount(vat)}원</td>
|
||||
<td className="p-2 text-right border-r border-gray-300">{formatAmount(vat)}</td>
|
||||
<td className="p-2 bg-gray-100 border-r border-gray-300 font-medium">합계</td>
|
||||
<td className="p-2 text-right font-semibold">{formatAmount(finalTotal)}원</td>
|
||||
<td className="p-2 text-right font-semibold">{formatAmount(finalTotal)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* - 계약서, 거래명세서, 발주서를 모달 형태로 표시
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -24,27 +25,40 @@ import { ContractDocument } from "./ContractDocument";
|
||||
import { TransactionDocument } from "./TransactionDocument";
|
||||
import { PurchaseOrderDocument } from "./PurchaseOrderDocument";
|
||||
import { printArea } from "@/lib/print-utils";
|
||||
import { OrderItem } from "../ItemAddDialog";
|
||||
import { OrderItem } from "../actions";
|
||||
import { getCompanyInfo } from "@/components/settings/CompanyInfoManagement/actions";
|
||||
|
||||
// 문서 타입
|
||||
export type OrderDocumentType = "contract" | "transaction" | "purchaseOrder";
|
||||
|
||||
// 제품 정보 타입 (견적의 calculation_inputs에서 추출)
|
||||
export interface ProductInfo {
|
||||
productName: string;
|
||||
productCategory?: string;
|
||||
openWidth?: string;
|
||||
openHeight?: string;
|
||||
quantity: number;
|
||||
floor?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
// 문서 데이터 인터페이스
|
||||
export interface OrderDocumentData {
|
||||
lotNumber: string;
|
||||
orderDate: string;
|
||||
client: string;
|
||||
siteName: string;
|
||||
manager: string;
|
||||
managerContact: string;
|
||||
deliveryRequestDate: string;
|
||||
expectedShipDate: string;
|
||||
deliveryMethod: string;
|
||||
address: string;
|
||||
items: OrderItem[];
|
||||
subtotal: number;
|
||||
discountRate: number;
|
||||
totalAmount: number;
|
||||
siteName?: string;
|
||||
manager?: string;
|
||||
managerContact?: string;
|
||||
deliveryRequestDate?: string;
|
||||
expectedShipDate?: string;
|
||||
deliveryMethod?: string;
|
||||
address?: string;
|
||||
items?: OrderItem[];
|
||||
products?: ProductInfo[];
|
||||
subtotal?: number;
|
||||
discountRate?: number;
|
||||
totalAmount?: number;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
@@ -55,12 +69,40 @@ interface OrderDocumentModalProps {
|
||||
data: OrderDocumentData;
|
||||
}
|
||||
|
||||
// 회사 정보 타입
|
||||
interface CompanyInfo {
|
||||
companyName: string;
|
||||
representativeName: string;
|
||||
businessNumber: string;
|
||||
managerPhone: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export function OrderDocumentModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
documentType,
|
||||
data,
|
||||
}: OrderDocumentModalProps) {
|
||||
const [companyInfo, setCompanyInfo] = useState<CompanyInfo | null>(null);
|
||||
|
||||
// 모달이 열릴 때 회사 정보 조회
|
||||
useEffect(() => {
|
||||
if (open && !companyInfo) {
|
||||
getCompanyInfo().then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setCompanyInfo({
|
||||
companyName: result.data.companyName,
|
||||
representativeName: result.data.representativeName,
|
||||
businessNumber: result.data.businessNumber,
|
||||
managerPhone: result.data.managerPhone,
|
||||
address: result.data.address,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [open, companyInfo]);
|
||||
|
||||
const getDocumentTitle = () => {
|
||||
switch (documentType) {
|
||||
case "contract":
|
||||
@@ -102,10 +144,19 @@ export function OrderDocumentModal({
|
||||
orderNumber={data.lotNumber}
|
||||
orderDate={data.orderDate}
|
||||
client={data.client}
|
||||
items={data.items}
|
||||
subtotal={data.subtotal}
|
||||
discountRate={data.discountRate}
|
||||
totalAmount={data.totalAmount}
|
||||
siteName={data.siteName}
|
||||
clientManager={data.manager}
|
||||
clientContact={data.managerContact}
|
||||
companyName={companyInfo?.companyName}
|
||||
companyCeo={companyInfo?.representativeName}
|
||||
companyBusinessNumber={companyInfo?.businessNumber}
|
||||
companyContact={companyInfo?.managerPhone}
|
||||
companyAddress={companyInfo?.address}
|
||||
items={data.items || []}
|
||||
products={data.products}
|
||||
subtotal={data.subtotal || 0}
|
||||
discountRate={data.discountRate || 0}
|
||||
totalAmount={data.totalAmount || 0}
|
||||
remarks={data.remarks}
|
||||
/>
|
||||
);
|
||||
@@ -115,10 +166,10 @@ export function OrderDocumentModal({
|
||||
orderNumber={data.lotNumber}
|
||||
orderDate={data.orderDate}
|
||||
client={data.client}
|
||||
items={data.items}
|
||||
subtotal={data.subtotal}
|
||||
discountRate={data.discountRate}
|
||||
totalAmount={data.totalAmount}
|
||||
items={data.items || []}
|
||||
subtotal={data.subtotal || 0}
|
||||
discountRate={data.discountRate || 0}
|
||||
totalAmount={data.totalAmount || 0}
|
||||
/>
|
||||
);
|
||||
case "purchaseOrder":
|
||||
@@ -126,14 +177,14 @@ export function OrderDocumentModal({
|
||||
<PurchaseOrderDocument
|
||||
orderNumber={data.lotNumber}
|
||||
client={data.client}
|
||||
siteName={data.siteName}
|
||||
siteName={data.siteName || "-"}
|
||||
manager={data.manager}
|
||||
managerContact={data.managerContact}
|
||||
deliveryRequestDate={data.deliveryRequestDate}
|
||||
deliveryRequestDate={data.deliveryRequestDate || "-"}
|
||||
expectedShipDate={data.expectedShipDate}
|
||||
deliveryMethod={data.deliveryMethod}
|
||||
address={data.address}
|
||||
items={data.items}
|
||||
address={data.address || "-"}
|
||||
items={data.items || []}
|
||||
remarks={data.remarks}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,37 +5,37 @@
|
||||
* - 스크린샷 형식 + 지출결의서 디자인 스타일
|
||||
*/
|
||||
|
||||
import { OrderItem } from "@/components/orders";
|
||||
import { OrderItem } from "../actions";
|
||||
|
||||
interface PurchaseOrderDocumentProps {
|
||||
orderNumber: string;
|
||||
client: string;
|
||||
siteName: string;
|
||||
siteName?: string;
|
||||
manager?: string;
|
||||
managerContact?: string;
|
||||
deliveryRequestDate: string;
|
||||
deliveryRequestDate?: string;
|
||||
expectedShipDate?: string;
|
||||
deliveryMethod?: string;
|
||||
address: string;
|
||||
address?: string;
|
||||
orderDate?: string;
|
||||
installationCount?: number;
|
||||
items: OrderItem[];
|
||||
items?: OrderItem[];
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
export function PurchaseOrderDocument({
|
||||
orderNumber,
|
||||
client,
|
||||
siteName,
|
||||
siteName = "-",
|
||||
manager = "-",
|
||||
managerContact = "010-0123-4567",
|
||||
deliveryRequestDate,
|
||||
deliveryRequestDate = "-",
|
||||
expectedShipDate = "-",
|
||||
deliveryMethod = "상차",
|
||||
address,
|
||||
address = "-",
|
||||
orderDate = new Date().toISOString().split("T")[0],
|
||||
installationCount = 3,
|
||||
items,
|
||||
items = [],
|
||||
remarks,
|
||||
}: PurchaseOrderDocumentProps) {
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { formatAmount } from "@/utils/formatAmount";
|
||||
import { OrderItem } from "@/components/orders";
|
||||
import { OrderItem } from "../actions";
|
||||
|
||||
interface TransactionDocumentProps {
|
||||
orderNumber: string;
|
||||
@@ -22,10 +22,10 @@ interface TransactionDocumentProps {
|
||||
companyBusinessNumber?: string;
|
||||
companyContact?: string;
|
||||
companyAddress?: string;
|
||||
items: OrderItem[];
|
||||
subtotal: number;
|
||||
discountRate: number;
|
||||
totalAmount: number;
|
||||
items?: OrderItem[];
|
||||
subtotal?: number;
|
||||
discountRate?: number;
|
||||
totalAmount?: number;
|
||||
}
|
||||
|
||||
export function TransactionDocument({
|
||||
@@ -42,10 +42,10 @@ export function TransactionDocument({
|
||||
companyBusinessNumber = "123-45-67890",
|
||||
companyContact = "02-1234-5678",
|
||||
companyAddress = "서울 강남구 테헤란로 123",
|
||||
items,
|
||||
subtotal,
|
||||
discountRate,
|
||||
totalAmount,
|
||||
items = [],
|
||||
subtotal = 0,
|
||||
discountRate = 0,
|
||||
totalAmount = 0,
|
||||
}: TransactionDocumentProps) {
|
||||
const discountAmount = Math.round(subtotal * (discountRate / 100));
|
||||
const afterDiscount = subtotal - discountAmount;
|
||||
|
||||
@@ -12,6 +12,8 @@ export {
|
||||
deleteOrders,
|
||||
updateOrderStatus,
|
||||
getOrderStats,
|
||||
revertProductionOrder,
|
||||
revertOrderConfirmation,
|
||||
type Order,
|
||||
type OrderItem as OrderItemApi,
|
||||
type OrderFormData as OrderApiFormData,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user