fix: [order] 수주 변환 연동 + 상세/수정 UI 개선
- 견적→수주 변환 API 연동 (createOrderFromQuote) - 수주 상세 뷰 개선 (PhoneInput, 금액 포맷) - 수주 수정 페이지 필드명 수정 (deliveryDate→expectedShipDate)
This commit is contained in:
@@ -520,10 +520,10 @@ export default function OrderEditPage() {
|
||||
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
||||
<TableCell className="text-center">{item.unit}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice)}원
|
||||
{formatAmount(item.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount ?? 0)}원
|
||||
{formatAmount(item.amount ?? 0)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -536,7 +536,7 @@ export default function OrderEditPage() {
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">소계:</span>
|
||||
<span className="w-32 text-right">
|
||||
{formatAmount(form.subtotal)}원
|
||||
{formatAmount(form.subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
@@ -546,7 +546,7 @@ export default function OrderEditPage() {
|
||||
<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)}원
|
||||
{formatAmount(form.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -620,7 +620,13 @@ export default function OrderDetailPage() {
|
||||
const product = order.products?.[nodeIndex];
|
||||
const nodeWidth = (node.options?.width as number) || product?.openWidth;
|
||||
const nodeHeight = (node.options?.height as number) || product?.openHeight;
|
||||
const productName = product?.productName || node.name || `개소 ${nodeIndex + 1}`;
|
||||
const nodeProductName = (node.options as Record<string, unknown>)?.product_name as string || product?.productName || '';
|
||||
const nodeFloor = (node.options as Record<string, unknown>)?.floor as string || '';
|
||||
const nodeSymbol = (node.options as Record<string, unknown>)?.symbol as string || '';
|
||||
const locLabel = [nodeFloor, nodeSymbol].filter(Boolean).join(' / ');
|
||||
const productName = nodeProductName
|
||||
? `${nodeProductName}${locLabel ? ` (${locLabel})` : ''}`
|
||||
: node.name || `개소 ${nodeIndex + 1}`;
|
||||
const nodeItems = node.items || [];
|
||||
|
||||
return (
|
||||
@@ -831,29 +837,7 @@ export default function OrderDetailPage() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 합계 */}
|
||||
<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">
|
||||
{formatAmount(order.subtotal || 0)}원
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<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 border-t pt-2 mt-2">
|
||||
<span>총금액:</span>
|
||||
<span className="w-32 text-right text-green-600">
|
||||
{formatAmount(order.totalAmount || 0)}원
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 합계 - 제거됨 (소계/할인율/총금액은 수주 상세에서 불필요) */}
|
||||
{/* 절곡 재고 현황 */}
|
||||
{bendingStock.length > 0 && (
|
||||
<Card>
|
||||
@@ -1192,12 +1176,13 @@ export default function OrderDetailPage() {
|
||||
<TableBody>
|
||||
{order.nodes && order.nodes.length > 0 ? (
|
||||
order.nodes.map((node, idx) => {
|
||||
const opts = node.options as Record<string, unknown> || {};
|
||||
const product = order.products?.[idx];
|
||||
const width = (node.options?.width as number) || product?.openWidth;
|
||||
const height = (node.options?.height as number) || product?.openHeight;
|
||||
const productName = product?.productName || node.name || '-';
|
||||
const floor = product?.floor || '-';
|
||||
const code = product?.code || '-';
|
||||
const width = (opts.width as number) || product?.openWidth;
|
||||
const height = (opts.height as number) || product?.openHeight;
|
||||
const productName = (opts.product_name as string) || product?.productName || node.name || '-';
|
||||
const floor = (opts.floor as string) || product?.floor || '-';
|
||||
const code = (opts.symbol as string) || product?.code || '-';
|
||||
return (
|
||||
<TableRow key={node.id}>
|
||||
<TableCell className="text-center text-xs">{idx + 1}</TableCell>
|
||||
@@ -1450,7 +1435,7 @@ export default function OrderDetailPage() {
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">총금액</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{formatAmount(order.totalAmount || 0)}원
|
||||
{formatAmount(order.totalAmount || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { OrderRegistration, OrderFormData, createOrder, getQuoteByIdForSelect } from "@/components/orders";
|
||||
import { OrderRegistration, OrderFormData, createOrder, createOrderFromQuote, getQuoteByIdForSelect } from "@/components/orders";
|
||||
import type { QuotationForSelect, QuotationItem } from "@/components/orders/actions";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2 } from "lucide-react";
|
||||
@@ -56,6 +56,30 @@ export default function OrderNewPage() {
|
||||
|
||||
const handleSave = async (formData: OrderFormData) => {
|
||||
try {
|
||||
// quoteId가 있으면 견적→수주 변환 API 호출 (개소별 분리 포함)
|
||||
if (quoteId) {
|
||||
const result = await createOrderFromQuote(Number(quoteId), {
|
||||
deliveryDate: formData.expectedShipDate,
|
||||
memo: formData.remarks,
|
||||
deliveryMethodCode: formData.deliveryMethod,
|
||||
options: {
|
||||
receiver: formData.receiver,
|
||||
receiver_contact: formData.receiverContact,
|
||||
shipping_address: formData.address,
|
||||
shipping_address_detail: formData.addressDetail,
|
||||
shipping_cost_code: formData.shippingCost,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success("견적에서 수주가 생성되었습니다.");
|
||||
router.push("/sales/order-management-sales");
|
||||
} else {
|
||||
toast.error(result.error || "수주 생성에 실패했습니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await createOrder(formData);
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -833,17 +833,17 @@ export function OrderRegistration({
|
||||
<Label>
|
||||
수신처 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
<PhoneInput
|
||||
placeholder="수신처 입력"
|
||||
value={form.receiverContact}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
receiverContact: e.target.value,
|
||||
receiverContact: value,
|
||||
}));
|
||||
clearFieldError("receiverContact");
|
||||
}}
|
||||
className={cn(fieldErrors.receiverContact && "border-red-500")}
|
||||
error={!!fieldErrors.receiverContact}
|
||||
/>
|
||||
{fieldErrors.receiverContact && (
|
||||
<p className="text-sm text-red-500">{fieldErrors.receiverContact}</p>
|
||||
|
||||
@@ -149,7 +149,7 @@ function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number })
|
||||
{statusConfig.label}
|
||||
</BadgeSm>
|
||||
<span className="text-sm font-medium">
|
||||
{formatAmount(node.totalPrice)}원
|
||||
{formatAmount(node.totalPrice)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -187,10 +187,10 @@ function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number })
|
||||
<TableCell className="text-center">{item.quantity}</TableCell>
|
||||
<TableCell className="text-center">{item.unit}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice)}원
|
||||
{formatAmount(item.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount ?? 0)}원
|
||||
{formatAmount(item.amount ?? 0)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -515,7 +515,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">소계:</span>
|
||||
<span className="w-32 text-right">
|
||||
{formatAmount(order.subtotal ?? 0)}원
|
||||
{formatAmount(order.subtotal ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
@@ -525,7 +525,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<div className="flex items-center gap-4 text-lg font-semibold">
|
||||
<span>총금액:</span>
|
||||
<span className="w-32 text-right text-green-600">
|
||||
{formatAmount(order.totalAmount ?? 0)}원
|
||||
{formatAmount(order.totalAmount ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -570,10 +570,10 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<TableCell className="text-center">{item.quantity}</TableCell>
|
||||
<TableCell className="text-center">{item.unit}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.unitPrice)}원
|
||||
{formatAmount(item.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatAmount(item.amount ?? 0)}원
|
||||
{formatAmount(item.amount ?? 0)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -586,7 +586,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">소계:</span>
|
||||
<span className="w-32 text-right">
|
||||
{formatAmount(order.subtotal ?? 0)}원
|
||||
{formatAmount(order.subtotal ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
@@ -596,7 +596,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<div className="flex items-center gap-4 text-lg font-semibold">
|
||||
<span>총금액:</span>
|
||||
<span className="w-32 text-right text-green-600">
|
||||
{formatAmount(order.totalAmount ?? 0)}원
|
||||
{formatAmount(order.totalAmount ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -783,7 +783,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">총금액</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{formatAmount(order.totalAmount ?? 0)}원
|
||||
{formatAmount(order.totalAmount ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
|
||||
@@ -100,7 +100,7 @@ export function QuotationSelectDialog({
|
||||
[{quotation.siteName}]
|
||||
</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{formatAmount(quotation.amount)}원
|
||||
{formatAmount(quotation.amount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 text-right">
|
||||
|
||||
@@ -394,6 +394,14 @@ export interface OrderStats {
|
||||
export interface CreateFromQuoteData {
|
||||
deliveryDate?: string;
|
||||
memo?: string;
|
||||
deliveryMethodCode?: string;
|
||||
options?: {
|
||||
receiver?: string;
|
||||
receiver_contact?: string;
|
||||
shipping_address?: string;
|
||||
shipping_address_detail?: string;
|
||||
shipping_cost_code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 생산지시 생성용
|
||||
@@ -1010,6 +1018,8 @@ export async function createOrderFromQuote(
|
||||
const apiData: Record<string, unknown> = {};
|
||||
if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate;
|
||||
if (data?.memo) apiData.memo = data.memo;
|
||||
if (data?.deliveryMethodCode) apiData.delivery_method_code = data.deliveryMethodCode;
|
||||
if (data?.options) apiData.options = data.options;
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/orders/from-quote/${quoteId}`),
|
||||
|
||||
@@ -7,6 +7,7 @@ export {
|
||||
getOrders,
|
||||
getOrderById,
|
||||
createOrder,
|
||||
createOrderFromQuote,
|
||||
updateOrder,
|
||||
deleteOrder,
|
||||
deleteOrders,
|
||||
|
||||
Reference in New Issue
Block a user