- 견적→수주 변환 API 연동 (createOrderFromQuote) - 수주 상세 뷰 개선 (PhoneInput, 금액 포맷) - 수주 수정 페이지 필드명 수정 (deliveryDate→expectedShipDate)
114 lines
3.8 KiB
TypeScript
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>
|
|
)}
|
|
/>
|
|
);
|
|
}
|