summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx')
-rw-r--r--lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx238
1 files changed, 238 insertions, 0 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx b/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx
new file mode 100644
index 00000000..b9dd44dd
--- /dev/null
+++ b/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx
@@ -0,0 +1,238 @@
+'use client'
+
+import * as React from 'react'
+import { Bidding } from '@/db/schema'
+import { QuotationDetails, updateTargetPrice } from '@/lib/bidding/detail/service'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
+
+interface BiddingDetailTargetPriceDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ quotationDetails: QuotationDetails | null
+ bidding: Bidding
+ onSuccess: () => void
+}
+
+export function BiddingDetailTargetPriceDialog({
+ open,
+ onOpenChange,
+ quotationDetails,
+ bidding,
+ onSuccess
+}: BiddingDetailTargetPriceDialogProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+ const [targetPrice, setTargetPrice] = React.useState(
+ bidding.targetPrice ? Number(bidding.targetPrice) : 0
+ )
+ const [calculationCriteria, setCalculationCriteria] = React.useState(
+ (bidding as any).targetPriceCalculationCriteria || ''
+ )
+
+ // Dialog가 열릴 때 상태 초기화
+ React.useEffect(() => {
+ if (open) {
+ setTargetPrice(bidding.targetPrice ? Number(bidding.targetPrice) : 0)
+ setCalculationCriteria((bidding as any).targetPriceCalculationCriteria || '')
+ }
+ }, [open, bidding])
+
+ const handleSave = () => {
+ // 필수값 검증
+ if (targetPrice <= 0) {
+ toast({
+ title: '유효성 오류',
+ description: '내정가는 0보다 큰 값을 입력해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ if (!calculationCriteria.trim()) {
+ toast({
+ title: '유효성 오류',
+ description: '내정가 산정 기준을 입력해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ startTransition(async () => {
+ const result = await updateTargetPrice(
+ bidding.id,
+ targetPrice,
+ calculationCriteria.trim(),
+ 'current-user' // TODO: 실제 사용자 ID
+ )
+
+ if (result.success) {
+ toast({
+ title: '성공',
+ description: result.message,
+ })
+ onSuccess()
+ onOpenChange(false)
+ } else {
+ toast({
+ title: '오류',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('ko-KR', {
+ style: 'currency',
+ currency: bidding.currency || 'KRW',
+ }).format(amount)
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[800px]">
+ <DialogHeader>
+ <DialogTitle>내정가 산정</DialogTitle>
+ <DialogDescription>
+ 입찰번호: {bidding.biddingNumber} - 견적 통계 및 내정가 설정
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[200px]">항목</TableHead>
+ <TableHead>값</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {/* 견적 통계 정보 */}
+ <TableRow>
+ <TableCell className="font-medium">예상액</TableCell>
+ <TableCell className="font-semibold">
+ {quotationDetails?.estimatedPrice ? formatCurrency(quotationDetails.estimatedPrice) : '-'}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell className="font-medium">최저견적가</TableCell>
+ <TableCell className="font-semibold text-green-600">
+ {quotationDetails?.lowestQuote ? formatCurrency(quotationDetails.lowestQuote) : '-'}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell className="font-medium">평균견적가</TableCell>
+ <TableCell className="font-semibold">
+ {quotationDetails?.averageQuote ? formatCurrency(quotationDetails.averageQuote) : '-'}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell className="font-medium">견적 수</TableCell>
+ <TableCell className="font-semibold">
+ {quotationDetails?.quotationCount || 0}개
+ </TableCell>
+ </TableRow>
+
+ {/* 예산 정보 */}
+ {bidding.budget && (
+ <TableRow>
+ <TableCell className="font-medium">예산</TableCell>
+ <TableCell className="font-semibold">
+ {formatCurrency(Number(bidding.budget))}
+ </TableCell>
+ </TableRow>
+ )}
+
+ {/* 최종 업데이트 시간 */}
+ {quotationDetails?.lastUpdated && (
+ <TableRow>
+ <TableCell className="font-medium">최종 업데이트</TableCell>
+ <TableCell className="text-sm text-muted-foreground">
+ {new Date(quotationDetails.lastUpdated).toLocaleString('ko-KR')}
+ </TableCell>
+ </TableRow>
+ )}
+
+ {/* 내정가 입력 */}
+ <TableRow>
+ <TableCell className="font-medium">
+ <Label htmlFor="targetPrice" className="text-sm font-medium">
+ 내정가 *
+ </Label>
+ </TableCell>
+ <TableCell>
+ <div className="space-y-2">
+ <Input
+ id="targetPrice"
+ type="number"
+ value={targetPrice}
+ onChange={(e) => setTargetPrice(Number(e.target.value))}
+ placeholder="내정가를 입력하세요"
+ className="w-full"
+ />
+ <div className="text-sm text-muted-foreground">
+ {targetPrice > 0 ? formatCurrency(targetPrice) : ''}
+ </div>
+ </div>
+ </TableCell>
+ </TableRow>
+
+ {/* 내정가 산정 기준 입력 */}
+ <TableRow>
+ <TableCell className="font-medium align-top pt-2">
+ <Label htmlFor="calculationCriteria" className="text-sm font-medium">
+ 내정가 산정 기준 *
+ </Label>
+ </TableCell>
+ <TableCell>
+ <Textarea
+ id="calculationCriteria"
+ value={calculationCriteria}
+ onChange={(e) => setCalculationCriteria(e.target.value)}
+ placeholder="내정가 산정 기준을 자세히 입력해주세요. (예: 최저견적가 대비 10% 상향 조정, 시장 평균가 고려 등)"
+ className="w-full min-h-[100px]"
+ rows={4}
+ />
+ <div className="text-xs text-muted-foreground mt-1">
+ 필수 입력 사항입니다. 내정가 산정에 대한 근거를 명확히 기재해주세요.
+ </div>
+ </TableCell>
+ </TableRow>
+ </TableBody>
+ </Table>
+ </div>
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ 취소
+ </Button>
+ <Button onClick={handleSave} disabled={isPending}>
+ 저장
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+}