Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -12,8 +12,8 @@ import { cn } from "@/lib/utils";
|
||||
* // 수주전환 상태
|
||||
* <BadgeSm variant="converted">수주전환</BadgeSm>
|
||||
*
|
||||
* // 최종확정 상태
|
||||
* <BadgeSm variant="finalized">최종확정</BadgeSm>
|
||||
* // 견적완료 상태
|
||||
* <BadgeSm variant="finalized">견적완료</BadgeSm>
|
||||
*
|
||||
* // 수정중 상태
|
||||
* <BadgeSm variant="revising">2차 수정</BadgeSm>
|
||||
@@ -24,7 +24,7 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export type BadgeSmVariant =
|
||||
| "converted" // 수주전환 - 보라색
|
||||
| "finalized" // 최종확정 - 초록색
|
||||
| "finalized" // 견적완료 - 초록색
|
||||
| "revising" // 수정중 - 주황색
|
||||
| "initial" // 최초작성 - 회색
|
||||
| "current" // 현재버전 - 파란색
|
||||
@@ -90,11 +90,11 @@ export function getQuoteStatusBadge(quote: {
|
||||
);
|
||||
}
|
||||
|
||||
// 최종확정
|
||||
// 견적완료
|
||||
if (quote.isFinal) {
|
||||
return (
|
||||
<BadgeSm variant="finalized">
|
||||
최종확정
|
||||
견적완료
|
||||
</BadgeSm>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ export function OrderRegistration({
|
||||
// 견적 정보로 폼 자동 채우기 (견적에서 가져온 품목은 삭제 불가)
|
||||
const items: OrderItem[] = (quotation.items || []).map((qi: QuotationItem) => ({
|
||||
id: qi.id,
|
||||
itemId: qi.itemId ? Number(qi.itemId) : undefined, // Items Master 참조 ID
|
||||
itemCode: qi.itemCode,
|
||||
itemName: qi.itemName,
|
||||
type: qi.type,
|
||||
|
||||
@@ -136,6 +136,7 @@ interface ApiQuoteForSelect {
|
||||
|
||||
interface ApiQuoteItem {
|
||||
id: number;
|
||||
item_id?: number | null; // Items Master 참조 ID
|
||||
item_code?: string;
|
||||
item_name: string;
|
||||
type_code?: string;
|
||||
@@ -431,6 +432,7 @@ export interface QuotationForSelect {
|
||||
|
||||
export interface QuotationItem {
|
||||
id: string;
|
||||
itemId?: string | null; // Items Master 참조 ID
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
type: string; // 종
|
||||
@@ -705,6 +707,7 @@ function transformQuoteItemForSelect(apiItem: ApiQuoteItem): QuotationItem {
|
||||
|
||||
return {
|
||||
id: String(apiItem.id),
|
||||
itemId: apiItem.item_id ? String(apiItem.item_id) : null,
|
||||
itemCode: apiItem.item_code || '',
|
||||
itemName: apiItem.item_name,
|
||||
type: typeFromNote,
|
||||
|
||||
@@ -552,6 +552,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
onOpenChange={handleModalClose}
|
||||
onAdd={handleSaveRule}
|
||||
editRule={editingRule}
|
||||
processId={initialData?.id}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -48,9 +48,11 @@ interface RuleModalProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (rule: Omit<ClassificationRule, 'id' | 'createdAt'>) => void;
|
||||
editRule?: ClassificationRule;
|
||||
/** 현재 공정 ID (다른 공정에 이미 배정된 품목 제외용) */
|
||||
processId?: string;
|
||||
}
|
||||
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProps) {
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: RuleModalProps) {
|
||||
// 공통 상태
|
||||
const [registrationType, setRegistrationType] = useState<RuleRegistrationType>(
|
||||
editRule?.registrationType || 'pattern'
|
||||
@@ -85,10 +87,11 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp
|
||||
q: q || undefined,
|
||||
itemType: itemType === 'all' ? undefined : itemType,
|
||||
size: 1000, // 전체 품목 조회
|
||||
excludeProcessId: processId, // 다른 공정에 이미 배정된 품목 제외
|
||||
});
|
||||
setItemList(items);
|
||||
setIsItemsLoading(false);
|
||||
}, []);
|
||||
}, [processId]);
|
||||
|
||||
// 검색어 유효성 검사 함수
|
||||
const isValidSearchKeyword = (keyword: string): boolean => {
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
* - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료)
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Edit } from 'lucide-react';
|
||||
import { ArrowLeft, Edit, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -18,6 +19,18 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { deleteProcessStep } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { ProcessStep } from '@/types/process';
|
||||
|
||||
interface StepDetailProps {
|
||||
@@ -28,7 +41,9 @@ interface StepDetailProps {
|
||||
export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
const router = useRouter();
|
||||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||
const { canUpdate } = usePermission();
|
||||
const { canUpdate, canDelete } = usePermission();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(
|
||||
@@ -40,6 +55,24 @@ export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
router.push(`/ko/master-data/process-management/${processId}`);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deleteProcessStep(processId, step.id);
|
||||
if (result.success) {
|
||||
toast.success('단계가 삭제되었습니다.');
|
||||
router.push(`/ko/master-data/process-management/${processId}`);
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader title="단계 상세" />
|
||||
@@ -131,13 +164,48 @@ export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
<ArrowLeft className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">공정으로 돌아가기</span>
|
||||
</Button>
|
||||
{canUpdate && (
|
||||
<Button onClick={handleEdit} size="sm" className="md:size-default">
|
||||
<Edit className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수정</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
size="sm"
|
||||
className="md:size-default"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">삭제</span>
|
||||
</Button>
|
||||
)}
|
||||
{canUpdate && (
|
||||
<Button onClick={handleEdit} size="sm" className="md:size-default">
|
||||
<Edit className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수정</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>단계 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
'{step.stepName}' 단계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -620,10 +620,13 @@ interface GetItemListParams {
|
||||
q?: string;
|
||||
itemType?: string;
|
||||
size?: number;
|
||||
/** 해당 공정 외 다른 공정에 이미 배정된 품목 제외 (공정 ID) */
|
||||
excludeProcessId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 목록 조회 (분류 규칙용)
|
||||
* - excludeProcessId: 다른 공정에 이미 배정된 품목 제외 (중복 방지)
|
||||
*/
|
||||
export async function getItemList(params?: GetItemListParams): Promise<ItemOption[]> {
|
||||
try {
|
||||
@@ -631,6 +634,7 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
searchParams.set('size', String(params?.size || 1000));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.itemType) searchParams.set('item_type', params.itemType);
|
||||
if (params?.excludeProcessId) searchParams.set('exclude_process_id', params.excludeProcessId);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
|
||||
@@ -179,9 +179,14 @@ export function ItemSearchModal({
|
||||
className="p-3 hover:bg-blue-50 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-gray-900">{item.itemCode}</span>
|
||||
<span className="ml-2 text-sm text-gray-600">{item.itemName}</span>
|
||||
<span className="text-sm text-gray-600">{item.itemName}</span>
|
||||
{item.hasInspectionTemplate && (
|
||||
<span className="text-xs text-white bg-green-500 px-1.5 py-0.5 rounded">
|
||||
수입검사
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.unit && (
|
||||
<span className="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Button } from "../ui/button";
|
||||
interface QuoteFooterBarProps {
|
||||
totalLocations: number;
|
||||
totalAmount: number;
|
||||
status: "draft" | "temporary" | "final";
|
||||
status: "draft" | "temporary" | "final" | "converted";
|
||||
/** 견적서 보기 */
|
||||
onQuoteView: () => void;
|
||||
/** 거래명세서 보기 */
|
||||
@@ -34,6 +34,10 @@ interface QuoteFooterBarProps {
|
||||
onEdit?: () => void;
|
||||
/** 수주등록 */
|
||||
onOrderRegister?: () => void;
|
||||
/** 수주 보기 (이미 수주가 있는 경우) */
|
||||
onOrderView?: () => void;
|
||||
/** 연결된 수주 ID (있으면 수주 보기, 없으면 수주등록) */
|
||||
orderId?: number | null;
|
||||
/** 할인하기 */
|
||||
onDiscount?: () => void;
|
||||
/** 수식보기 */
|
||||
@@ -61,6 +65,8 @@ export function QuoteFooterBar({
|
||||
onBack,
|
||||
onEdit,
|
||||
onOrderRegister,
|
||||
onOrderView,
|
||||
orderId,
|
||||
onDiscount,
|
||||
onFormulaView,
|
||||
hasBomResult = false,
|
||||
@@ -132,8 +138,8 @@ export function QuoteFooterBar({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 수정 - view 모드에서만 표시 */}
|
||||
{isViewMode && onEdit && (
|
||||
{/* 수정 - view 모드에서만 표시, 수주 등록된 경우 숨김 */}
|
||||
{isViewMode && onEdit && !orderId && (
|
||||
<Button
|
||||
onClick={onEdit}
|
||||
variant="outline"
|
||||
@@ -145,11 +151,11 @@ export function QuoteFooterBar({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 할인하기 - view 모드에서는 비활성 */}
|
||||
{/* 할인하기 - view 모드 또는 수주 등록된 경우 비활성 */}
|
||||
{onDiscount && (
|
||||
<Button
|
||||
onClick={onDiscount}
|
||||
disabled={isViewMode}
|
||||
disabled={isViewMode || !!orderId}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-orange-300 text-orange-600 hover:bg-orange-50 md:size-default md:px-6"
|
||||
@@ -159,25 +165,31 @@ export function QuoteFooterBar({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 저장 - edit 모드에서만 표시 */}
|
||||
{/* 저장 버튼 - edit 모드에서만 표시 */}
|
||||
{/* final/converted 상태면 "저장", 그 외는 "임시저장" */}
|
||||
{!isViewMode && (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="bg-slate-500 hover:bg-slate-600 text-white md:size-default md:px-6"
|
||||
className={status === "final" || status === "converted"
|
||||
? "bg-blue-600 hover:bg-blue-700 text-white md:size-default md:px-6"
|
||||
: "bg-slate-500 hover:bg-slate-600 text-white md:size-default md:px-6"
|
||||
}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 md:mr-2" />
|
||||
)}
|
||||
<span className="hidden md:inline">저장</span>
|
||||
<span className="hidden md:inline">
|
||||
{status === "final" || status === "converted" ? "저장" : "임시저장"}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 최종확정 - 양쪽 모드에서 표시 (final 상태가 아닐 때만) */}
|
||||
{status !== "final" && (
|
||||
{/* 견적완료 - edit 모드에서만 표시 (final 상태가 아닐 때만) */}
|
||||
{!isViewMode && status !== "final" && (
|
||||
<Button
|
||||
onClick={onFinalize}
|
||||
disabled={isSaving || totalAmount === 0}
|
||||
@@ -189,21 +201,34 @@ export function QuoteFooterBar({
|
||||
) : (
|
||||
<Check className="h-4 w-4 md:mr-2" />
|
||||
)}
|
||||
<span className="hidden md:inline">최종확정</span>
|
||||
<span className="hidden md:inline">견적완료</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 수주등록 - final 상태일 때 표시 */}
|
||||
{status === "final" && onOrderRegister && (
|
||||
<Button
|
||||
onClick={onOrderRegister}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 text-white md:size-default md:px-6"
|
||||
>
|
||||
<ClipboardList className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수주등록</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* 수주등록/수주보기 - view 모드에서 final 또는 converted 상태일 때 표시 */}
|
||||
{isViewMode && (status === "final" || status === "converted") && (orderId ? (
|
||||
onOrderView && (
|
||||
<Button
|
||||
onClick={onOrderView}
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white md:size-default md:px-6"
|
||||
>
|
||||
<ClipboardList className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수주 보기</span>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
onOrderRegister && (
|
||||
<Button
|
||||
onClick={onOrderRegister}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 text-white md:size-default md:px-6"
|
||||
>
|
||||
<ClipboardList className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수주등록</span>
|
||||
</Button>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -382,7 +382,7 @@ export function QuoteManagementClient({
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="initial">최초작성</SelectItem>
|
||||
<SelectItem value="revising">N차수정</SelectItem>
|
||||
<SelectItem value="final">최종확정</SelectItem>
|
||||
<SelectItem value="final">견적완료</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -95,10 +95,11 @@ export interface QuoteFormDataV2 {
|
||||
contact: string; // 연락처
|
||||
vatType: "included" | "excluded"; // 부가세 (포함/별도)
|
||||
remarks: string; // 비고
|
||||
status: "draft" | "temporary" | "final"; // 작성중, 임시저장, 최종저장
|
||||
status: "draft" | "temporary" | "final" | "converted"; // 작성중, 임시저장, 최종저장, 수주전환
|
||||
discountRate: number; // 할인율 (%)
|
||||
discountAmount: number; // 할인 금액
|
||||
locations: LocationItem[];
|
||||
orderId?: number | null; // 연결된 수주 ID (수주전환 시 설정)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -151,6 +152,8 @@ interface QuoteRegistrationV2Props {
|
||||
onCalculate?: () => void;
|
||||
onEdit?: () => void;
|
||||
onOrderRegister?: () => void;
|
||||
/** 수주 보기 (이미 수주가 있는 경우) */
|
||||
onOrderView?: () => void;
|
||||
initialData?: QuoteFormDataV2 | null;
|
||||
isLoading?: boolean;
|
||||
/** IntegratedDetailTemplate 사용 시 타이틀 영역 숨김 */
|
||||
@@ -168,6 +171,7 @@ export function QuoteRegistrationV2({
|
||||
onCalculate,
|
||||
onEdit,
|
||||
onOrderRegister,
|
||||
onOrderView,
|
||||
initialData,
|
||||
isLoading = false,
|
||||
hideHeader = false,
|
||||
@@ -844,7 +848,7 @@ export function QuoteRegistrationV2({
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">상태</label>
|
||||
<Input
|
||||
value={formData.status === "final" ? "최종확정" : formData.status === "temporary" ? "임시저장" : "최초작성"}
|
||||
value={formData.status === "final" ? "견적완료" : formData.status === "temporary" ? "임시저장" : "최초작성"}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
@@ -968,6 +972,8 @@ export function QuoteRegistrationV2({
|
||||
onBack={onBack}
|
||||
onEdit={onEdit}
|
||||
onOrderRegister={onOrderRegister}
|
||||
onOrderView={onOrderView}
|
||||
orderId={formData.orderId}
|
||||
onDiscount={() => setDiscountModalOpen(true)}
|
||||
onFormulaView={() => setFormulaViewOpen(true)}
|
||||
hasBomResult={hasBomResult}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const QUOTE_STATUS_LABELS: Record<QuoteStatus, string> = {
|
||||
sent: '발송완료',
|
||||
approved: '승인',
|
||||
rejected: '거절',
|
||||
finalized: '최종확정',
|
||||
finalized: '견적완료',
|
||||
converted: '수주전환',
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface Quote {
|
||||
updatedBy?: string;
|
||||
finalizedAt?: string;
|
||||
finalizedBy?: string;
|
||||
orderId?: number | null; // 연결된 수주 ID (수주전환 시 설정)
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
@@ -182,6 +183,8 @@ export interface QuoteApiData {
|
||||
updated_by: number | null;
|
||||
finalized_at: string | null;
|
||||
finalized_by: number | null;
|
||||
// 연결된 수주 ID (수주전환 시 설정)
|
||||
order_id?: number | null;
|
||||
// 관계 데이터 (with 로드 시)
|
||||
creator?: {
|
||||
id: number;
|
||||
@@ -269,6 +272,7 @@ export function transformApiToFrontend(apiData: QuoteApiData): Quote {
|
||||
updatedBy: apiData.updater?.name || undefined,
|
||||
finalizedAt: apiData.finalized_at || undefined,
|
||||
finalizedBy: apiData.finalizer?.name || undefined,
|
||||
orderId: apiData.order_id ?? undefined, // 연결된 수주 ID
|
||||
};
|
||||
}
|
||||
|
||||
@@ -348,7 +352,7 @@ export const QUOTE_FILTER_OPTIONS: { value: QuoteFilterType; label: string }[] =
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'initial', label: '최초작성' },
|
||||
{ value: 'revising', label: '수정중' },
|
||||
{ value: 'final', label: '최종확정' },
|
||||
{ value: 'final', label: '견적완료' },
|
||||
{ value: 'converted', label: '수주전환' },
|
||||
];
|
||||
|
||||
@@ -694,10 +698,11 @@ export interface QuoteFormDataV2 {
|
||||
contact: string;
|
||||
dueDate: string;
|
||||
remarks: string;
|
||||
status: 'draft' | 'temporary' | 'final'; // 작성중, 임시저장, 최종저장
|
||||
status: 'draft' | 'temporary' | 'final' | 'converted'; // 작성중, 임시저장, 최종저장, 수주전환
|
||||
discountRate: number; // 할인율 (%)
|
||||
discountAmount: number; // 할인 금액
|
||||
locations: LocationItem[];
|
||||
orderId?: number | null; // 연결된 수주 ID (수주전환 시 설정)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -709,7 +714,7 @@ export interface QuoteFormDataV2 {
|
||||
*
|
||||
* 핵심 차이점:
|
||||
* - V2는 locations[] 배열, API는 items[] + calculation_inputs.items[] 구조
|
||||
* - V2 status는 3가지 (draft/temporary/final), API status는 6가지
|
||||
* - V2 status는 4가지 (draft/temporary/final/converted), API status는 6가지
|
||||
* - BOM 산출 결과가 있으면 items에 자재 상세 포함
|
||||
*/
|
||||
export function transformV2ToApi(
|
||||
@@ -964,6 +969,7 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
|
||||
description?: string;
|
||||
discountRate?: number;
|
||||
discountAmount?: number;
|
||||
orderId?: number | null; // 연결된 수주 ID (camelCase 버전)
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -991,6 +997,8 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
|
||||
discountRate: Number(apiData.discount_rate) || transformed.discountRate || 0,
|
||||
discountAmount: Number(apiData.discount_amount) || transformed.discountAmount || 0,
|
||||
locations: locations,
|
||||
// 연결된 수주 ID (raw API: order_id, transformed: orderId)
|
||||
orderId: apiData.order_id ?? transformed.orderId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user