summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
commita5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch)
tree667ed8c5d6ec35b109190e9f976d66ae54def4ce /components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
parentb0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff)
parentf8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff)
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'components/bidding/manage/bidding-detail-vendor-create-dialog.tsx')
-rw-r--r--components/bidding/manage/bidding-detail-vendor-create-dialog.tsx437
1 files changed, 437 insertions, 0 deletions
diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
new file mode 100644
index 00000000..ed3e2be6
--- /dev/null
+++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
@@ -0,0 +1,437 @@
+'use client'
+
+import * as React from 'react'
+import { Button } from '@/components/ui/button'
+import { Label } from '@/components/ui/label'
+import { Checkbox } from '@/components/ui/checkbox'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { ChevronsUpDown, Loader2, X, Plus } from 'lucide-react'
+import { createBiddingDetailVendor } from '@/lib/bidding/detail/service'
+import { searchVendorsForBidding } from '@/lib/bidding/service'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
+import { Badge } from '@/components/ui/badge'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+
+interface BiddingDetailVendorCreateDialogProps {
+ biddingId: number
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSuccess: () => void
+}
+
+interface Vendor {
+ id: number
+ vendorName: string
+ vendorCode: string
+ status: string
+}
+
+interface SelectedVendorWithQuestion {
+ vendor: Vendor
+ isPriceAdjustmentApplicableQuestion: boolean
+}
+
+export function BiddingDetailVendorCreateDialog({
+ biddingId,
+ open,
+ onOpenChange,
+ onSuccess
+}: BiddingDetailVendorCreateDialogProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+ const [activeTab, setActiveTab] = React.useState('select')
+
+ // Vendor 검색 상태
+ const [vendorList, setVendorList] = React.useState<Vendor[]>([])
+ const [selectedVendorsWithQuestion, setSelectedVendorsWithQuestion] = React.useState<SelectedVendorWithQuestion[]>([])
+ const [vendorOpen, setVendorOpen] = React.useState(false)
+
+ // 벤더 로드
+ const loadVendors = React.useCallback(async () => {
+ try {
+ const result = await searchVendorsForBidding('', biddingId) // 빈 검색어로 모든 벤더 로드
+ setVendorList(result || [])
+ } catch (error) {
+ console.error('Failed to load vendors:', error)
+ toast({
+ title: '오류',
+ description: '벤더 목록을 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ setVendorList([])
+ }
+ }, [biddingId, toast])
+
+ React.useEffect(() => {
+ if (open) {
+ loadVendors()
+ }
+ }, [open, loadVendors])
+
+ // 초기화
+ React.useEffect(() => {
+ if (!open) {
+ setSelectedVendorsWithQuestion([])
+ setActiveTab('select')
+ }
+ }, [open])
+
+ // 벤더 추가
+ const handleAddVendor = (vendor: Vendor) => {
+ if (!selectedVendorsWithQuestion.find(v => v.vendor.id === vendor.id)) {
+ setSelectedVendorsWithQuestion([
+ ...selectedVendorsWithQuestion,
+ {
+ vendor,
+ isPriceAdjustmentApplicableQuestion: false
+ }
+ ])
+ }
+ setVendorOpen(false)
+ }
+
+ // 벤더 제거
+ const handleRemoveVendor = (vendorId: number) => {
+ setSelectedVendorsWithQuestion(
+ selectedVendorsWithQuestion.filter(v => v.vendor.id !== vendorId)
+ )
+ }
+
+ // 이미 선택된 벤더인지 확인
+ const isVendorSelected = (vendorId: number) => {
+ return selectedVendorsWithQuestion.some(v => v.vendor.id === vendorId)
+ }
+
+ // 연동제 적용요건 문의 체크박스 토글
+ const handleTogglePriceAdjustmentQuestion = (vendorId: number, checked: boolean) => {
+ setSelectedVendorsWithQuestion(prev =>
+ prev.map(item =>
+ item.vendor.id === vendorId
+ ? { ...item, isPriceAdjustmentApplicableQuestion: checked }
+ : item
+ )
+ )
+ }
+
+ const handleCreate = () => {
+ if (selectedVendorsWithQuestion.length === 0) {
+ toast({
+ title: '오류',
+ description: '업체를 선택해주세요.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ // Tab 2로 이동하여 연동제 적용요건 문의를 확인하도록 유도
+ if (activeTab === 'select') {
+ setActiveTab('question')
+ toast({
+ title: '확인 필요',
+ description: '선택한 업체들의 연동제 적용요건 문의를 확인해주세요.',
+ })
+ return
+ }
+
+ startTransition(async () => {
+ let successCount = 0
+ const errorMessages: string[] = []
+
+ for (const item of selectedVendorsWithQuestion) {
+ try {
+ const response = await createBiddingDetailVendor(
+ biddingId,
+ item.vendor.id,
+ item.isPriceAdjustmentApplicableQuestion
+ )
+
+ if (response.success) {
+ successCount++
+ } else {
+ errorMessages.push(`${item.vendor.vendorName}: ${response.error}`)
+ }
+ } catch {
+ errorMessages.push(`${item.vendor.vendorName}: 처리 중 오류가 발생했습니다.`)
+ }
+ }
+
+ if (successCount > 0) {
+ toast({
+ title: '성공',
+ description: `${successCount}개의 업체가 성공적으로 추가되었습니다.${errorMessages.length > 0 ? ` ${errorMessages.length}개는 실패했습니다.` : ''}`,
+ })
+ onOpenChange(false)
+ resetForm()
+ onSuccess()
+ }
+
+ if (errorMessages.length > 0 && successCount === 0) {
+ toast({
+ title: '오류',
+ description: `업체 추가에 실패했습니다: ${errorMessages.join(', ')}`,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ const resetForm = () => {
+ setSelectedVendorsWithQuestion([])
+ setActiveTab('select')
+ }
+
+ const selectedVendors = selectedVendorsWithQuestion.map(item => item.vendor)
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col">
+ {/* 헤더 */}
+ <DialogHeader className="p-6 pb-0">
+ <DialogTitle>협력업체 추가</DialogTitle>
+ <DialogDescription>
+ 입찰에 참여할 업체를 선택하고 연동제 적용요건 문의를 설정하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* 탭 네비게이션 */}
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col px-6">
+ <TabsList className="grid w-full grid-cols-2">
+ <TabsTrigger value="select">
+ 1. 입찰업체 선택 ({selectedVendors.length}개)
+ </TabsTrigger>
+ <TabsTrigger
+ value="question"
+ disabled={selectedVendors.length === 0}
+ >
+ 2. 연동제 적용요건 문의
+ </TabsTrigger>
+ </TabsList>
+
+ {/* Tab 1: 입찰업체 선택 */}
+ <TabsContent value="select" className="flex-1 overflow-y-auto mt-4 pb-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">업체 선택</CardTitle>
+ <CardDescription>
+ 입찰에 참여할 협력업체를 선택하세요.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {/* 업체 추가 버튼 */}
+ <Popover open={vendorOpen} onOpenChange={setVendorOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={vendorOpen}
+ className="w-full justify-between"
+ disabled={vendorList.length === 0}
+ >
+ <span className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ 업체 선택하기
+ </span>
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[500px] p-0" align="start">
+ <Command>
+ <CommandInput placeholder="업체명 또는 코드로 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {vendorList
+ .filter(vendor => !isVendorSelected(vendor.id))
+ .map((vendor) => (
+ <CommandItem
+ key={vendor.id}
+ value={`${vendor.vendorCode} ${vendor.vendorName}`}
+ onSelect={() => handleAddVendor(vendor)}
+ >
+ <div className="flex items-center gap-2 w-full">
+ <Badge variant="outline" className="shrink-0">
+ {vendor.vendorCode}
+ </Badge>
+ <span className="truncate">{vendor.vendorName}</span>
+ </div>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+
+ {/* 선택된 업체 목록 */}
+ {selectedVendors.length > 0 && (
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <h4 className="text-sm font-medium">선택된 업체 ({selectedVendors.length}개)</h4>
+ </div>
+ <div className="space-y-2">
+ {selectedVendors.map((vendor, index) => (
+ <div
+ key={vendor.id}
+ className="flex items-center justify-between p-3 rounded-lg bg-secondary/50"
+ >
+ <div className="flex items-center gap-3">
+ <span className="text-sm text-muted-foreground">
+ {index + 1}.
+ </span>
+ <Badge variant="outline">
+ {vendor.vendorCode}
+ </Badge>
+ <span className="text-sm font-medium">
+ {vendor.vendorName}
+ </span>
+ </div>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleRemoveVendor(vendor.id)}
+ className="h-8 w-8 p-0"
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {selectedVendors.length === 0 && (
+ <div className="text-center py-8 text-muted-foreground">
+ <p className="text-sm">아직 선택된 업체가 없습니다.</p>
+ <p className="text-xs mt-1">위 버튼을 클릭하여 업체를 추가하세요.</p>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ {/* Tab 2: 연동제 적용요건 문의 체크 */}
+ <TabsContent value="question" className="flex-1 overflow-y-auto mt-4 pb-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">연동제 적용요건 문의</CardTitle>
+ <CardDescription>
+ 선택한 업체별로 연동제 적용요건 문의 여부를 체크하세요.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {selectedVendorsWithQuestion.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ <p className="text-sm">선택된 업체가 없습니다.</p>
+ <p className="text-xs mt-1">먼저 입찰업체 선택 탭에서 업체를 선택해주세요.</p>
+ </div>
+ ) : (
+ <div className="space-y-4">
+ {selectedVendorsWithQuestion.map((item, index) => (
+ <div
+ key={item.vendor.id}
+ className="flex items-center justify-between p-4 rounded-lg border"
+ >
+ <div className="flex items-center gap-4 flex-1">
+ <span className="text-sm text-muted-foreground w-8">
+ {index + 1}.
+ </span>
+ <div className="flex-1">
+ <div className="flex items-center gap-2 mb-1">
+ <Badge variant="outline">
+ {item.vendor.vendorCode}
+ </Badge>
+ <span className="font-medium">{item.vendor.vendorName}</span>
+ </div>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ <Checkbox
+ id={`question-${item.vendor.id}`}
+ checked={item.isPriceAdjustmentApplicableQuestion}
+ onCheckedChange={(checked) =>
+ handleTogglePriceAdjustmentQuestion(item.vendor.id, checked as boolean)
+ }
+ />
+ <Label
+ htmlFor={`question-${item.vendor.id}`}
+ className="text-sm cursor-pointer"
+ >
+ 연동제 적용요건 문의
+ </Label>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </TabsContent>
+ </Tabs>
+
+ {/* 푸터 */}
+ <DialogFooter className="p-6 pt-0 border-t">
+ <Button
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isPending}
+ >
+ 취소
+ </Button>
+ {activeTab === 'select' ? (
+ <Button
+ onClick={() => {
+ if (selectedVendors.length > 0) {
+ setActiveTab('question')
+ } else {
+ toast({
+ title: '오류',
+ description: '업체를 선택해주세요.',
+ variant: 'destructive',
+ })
+ }
+ }}
+ disabled={isPending || selectedVendors.length === 0}
+ >
+ 다음 단계
+ </Button>
+ ) : (
+ <Button
+ onClick={handleCreate}
+ disabled={isPending || selectedVendorsWithQuestion.length === 0}
+ >
+ {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ {selectedVendorsWithQuestion.length > 0
+ ? `${selectedVendorsWithQuestion.length}개 업체 추가`
+ : '업체 추가'
+ }
+ </Button>
+ )}
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file