fix: [order] 수주 변환 연동 + 상세/수정 UI 개선

- 견적→수주 변환 API 연동 (createOrderFromQuote)
- 수주 상세 뷰 개선 (PhoneInput, 금액 포맷)
- 수주 수정 페이지 필드명 수정 (deliveryDate→expectedShipDate)
This commit is contained in:
2026-03-17 13:51:34 +09:00
parent 704ea3c02d
commit 6fc9d8f6b0
8 changed files with 70 additions and 50 deletions

View File

@@ -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>

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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}`),

View File

@@ -7,6 +7,7 @@ export {
getOrders,
getOrderById,
createOrder,
createOrderFromQuote,
updateOrder,
deleteOrder,
deleteOrders,