Files
sam-react-prod/src/components/orders/QuotationSelectDialog.tsx
권혁성 6fc9d8f6b0 fix: [order] 수주 변환 연동 + 상세/수정 UI 개선
- 견적→수주 변환 API 연동 (createOrderFromQuote)
- 수주 상세 뷰 개선 (PhoneInput, 금액 포맷)
- 수주 수정 페이지 필드명 수정 (deliveryDate→expectedShipDate)
2026-03-17 13:51:53 +09:00

114 lines
3.8 KiB
TypeScript

/**
* 견적 선택 팝업
*
* SearchableSelectionModal 공통 컴포넌트 기반
* 확정된 견적 목록에서 수주 전환할 견적을 선택
*/
'use client';
import { useCallback } from 'react';
import { FileText, Check } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { formatAmount } from '@/lib/utils/amount';
import { cn } from '@/lib/utils';
import { SearchableSelectionModal } from '@/components/organisms/SearchableSelectionModal';
import { getQuotesForSelect, type QuotationForSelect } from './actions';
interface QuotationSelectDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (quotation: QuotationForSelect) => void;
selectedId?: string;
}
// 등급 배지 컴포넌트
function GradeBadge({ grade }: { grade: string }) {
const config: Record<string, { label: string; className: string }> = {
A: { label: 'A (우량)', className: 'bg-green-100 text-green-700 border-green-200' },
B: { label: 'B (관리)', className: 'bg-yellow-100 text-yellow-700 border-yellow-200' },
C: { label: 'C (주의)', className: 'bg-red-100 text-red-700 border-red-200' },
};
const cfg = config[grade] || config.B;
return (
<Badge variant="outline" className={cn('text-xs', cfg.className)}>
{cfg.label}
</Badge>
);
}
export function QuotationSelectDialog({
open,
onOpenChange,
onSelect,
selectedId,
}: QuotationSelectDialogProps) {
const handleFetchData = useCallback(async (query: string) => {
const result = await getQuotesForSelect({ q: query || undefined, size: 50 });
if (result.success && result.data) {
return result.data.items;
}
throw new Error(result.error || '견적 목록 조회에 실패했습니다.');
}, []);
return (
<SearchableSelectionModal<QuotationForSelect>
open={open}
onOpenChange={onOpenChange}
title={
<span className="flex items-center gap-2">
<FileText className="h-5 w-5" />
</span>
}
searchPlaceholder="견적번호, 거래처, 현장명 검색..."
fetchData={handleFetchData}
keyExtractor={(q) => q.id}
loadOnOpen
dialogClassName="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col"
listContainerClassName="flex-1 overflow-y-auto space-y-3 pr-2"
infoText={(items, isLoading) =>
isLoading
? null
: `전환 가능한 견적 ${items.length}건 (최종확정 상태)`
}
mode="single"
onSelect={onSelect}
renderItem={(quotation) => (
<div
className={cn(
'p-4 border rounded-lg hover:bg-muted/50 hover:border-primary/50 transition-colors',
selectedId === quotation.id && 'border-primary bg-primary/5'
)}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<code className="text-sm font-mono bg-gray-100 px-2 py-0.5 rounded">
{quotation.quoteNumber}
</code>
<GradeBadge grade={quotation.grade} />
</div>
{selectedId === quotation.id && (
<Check className="h-5 w-5 text-primary" />
)}
</div>
<div className="font-medium text-base mb-1">
{quotation.client}
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
[{quotation.siteName}]
</span>
<span className="font-medium text-green-600">
{formatAmount(quotation.amount)}
</span>
</div>
<div className="text-xs text-muted-foreground mt-1 text-right">
{quotation.itemCount}
</div>
</div>
)}
/>
);
}