diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-28 03:12:57 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-28 03:12:57 +0000 |
| commit | 9cda8482660a87fd98c9ee43f507d75ff75b4e23 (patch) | |
| tree | 67eb1fc24eec7c4e61d3154f7b09fc5349454672 /components | |
| parent | f57898bd240d068301ce3ef477f52cff1234e4ee (diff) | |
(최겸) 구매 입찰 피드백 반영(90%)
Diffstat (limited to 'components')
6 files changed, 900 insertions, 303 deletions
diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index f298721b..bb7880f5 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react'
import { UseFormReturn } from 'react-hook-form'
-import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Plus } from 'lucide-react'
+import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Plus, Check, ChevronsUpDown } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
@@ -26,6 +26,20 @@ import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover'
+import { cn } from '@/lib/utils'
import type { CreateBiddingSchema } from '@/lib/bidding/validation'
import { contractTypeLabels, biddingTypeLabels, awardCountLabels, biddingNoticeTypeLabels } from '@/db/schema'
@@ -589,37 +603,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control}
name="biddingConditions.paymentTerms"
render={({ field }) => (
- <FormItem>
+ <FormItem className="flex flex-col">
<FormLabel>SHI 지급조건 <span className="text-red-500">*</span></FormLabel>
- <FormControl>
- <Select
- value={biddingConditions.paymentTerms}
- onValueChange={(value) => {
- setBiddingConditions(prev => ({
- ...prev,
- paymentTerms: value
- }))
- field.onChange(value)
- }}
- >
- <SelectTrigger>
- <SelectValue placeholder="지급조건 선택" />
- </SelectTrigger>
- <SelectContent>
- {paymentTermsOptions.length > 0 ? (
- paymentTermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </FormControl>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "justify-between",
+ !biddingConditions.paymentTerms && "text-muted-foreground"
+ )}
+ >
+ {biddingConditions.paymentTerms
+ ? paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)
+ ? `${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.code} ${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.description ? `(${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.description})` : ''}`
+ : "지급조건 선택"
+ : "지급조건 선택"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="지급조건 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {paymentTermsOptions.map((option) => (
+ <CommandItem
+ key={option.code}
+ value={`${option.code} ${option.description || ''}`}
+ onSelect={() => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ paymentTerms: option.code
+ }))
+ field.onChange(option.code)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ option.code === biddingConditions.paymentTerms
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {option.code} {option.description && `(${option.description})`}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
<FormMessage />
</FormItem>
)}
@@ -632,37 +671,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control}
name="biddingConditions.incoterms"
render={({ field }) => (
- <FormItem>
+ <FormItem className="flex flex-col">
<FormLabel>SHI 인도조건 <span className="text-red-500">*</span></FormLabel>
- <Select
- value={biddingConditions.incoterms}
- onValueChange={(value) => {
- setBiddingConditions(prev => ({
- ...prev,
- incoterms: value
- }))
- field.onChange(value)
- }}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="인코텀즈 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {incotermsOptions.length > 0 ? (
- incotermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "justify-between",
+ !biddingConditions.incoterms && "text-muted-foreground"
+ )}
+ >
+ {biddingConditions.incoterms
+ ? incotermsOptions.find((option) => option.code === biddingConditions.incoterms)
+ ? `${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.code} ${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.description ? `(${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.description})` : ''}`
+ : "인코텀즈 선택"
+ : "인코텀즈 선택"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="인코텀즈 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {incotermsOptions.map((option) => (
+ <CommandItem
+ key={option.code}
+ value={`${option.code} ${option.description || ''}`}
+ onSelect={() => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ incoterms: option.code
+ }))
+ field.onChange(option.code)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ option.code === biddingConditions.incoterms
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {option.code} {option.description && `(${option.description})`}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
<FormMessage />
</FormItem>
)}
@@ -699,31 +763,60 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control}
name="biddingConditions.taxConditions"
render={({ field }) => (
- <FormItem>
+ <FormItem className="flex flex-col">
<FormLabel>SHI 매입부가가치세 <span className="text-red-500">*</span></FormLabel>
- <FormControl>
- <Select
- value={biddingConditions.taxConditions}
- onValueChange={(value) => {
- setBiddingConditions(prev => ({
- ...prev,
- taxConditions: value
- }))
- field.onChange(value)
- }}
- >
- <SelectTrigger>
- <SelectValue placeholder="세금조건 선택" />
- </SelectTrigger>
- <SelectContent>
- {TAX_CONDITIONS.map((condition) => (
- <SelectItem key={condition.code} value={condition.code}>
- {condition.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "justify-between",
+ !biddingConditions.taxConditions && "text-muted-foreground"
+ )}
+ >
+ {biddingConditions.taxConditions
+ ? TAX_CONDITIONS.find((condition) => condition.code === biddingConditions.taxConditions)?.name
+ : "세금조건 선택"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="세금조건 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {TAX_CONDITIONS.map((condition) => (
+ <CommandItem
+ key={condition.code}
+ value={`${condition.code} ${condition.name}`}
+ onSelect={() => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ taxConditions: condition.code
+ }))
+ field.onChange(condition.code)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ condition.code === biddingConditions.taxConditions
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {condition.name}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
<FormMessage />
</FormItem>
)}
@@ -733,37 +826,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control}
name="biddingConditions.shippingPort"
render={({ field }) => (
- <FormItem>
+ <FormItem className="flex flex-col">
<FormLabel>SHI 선적지</FormLabel>
- <Select
- value={biddingConditions.shippingPort}
- onValueChange={(value) => {
- setBiddingConditions(prev => ({
- ...prev,
- shippingPort: value
- }))
- field.onChange(value)
- }}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="선적지 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {shippingPlaces.length > 0 ? (
- shippingPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "justify-between",
+ !biddingConditions.shippingPort && "text-muted-foreground"
+ )}
+ >
+ {biddingConditions.shippingPort
+ ? shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)
+ ? `${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.code} ${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.description ? `(${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.description})` : ''}`
+ : "선적지 선택"
+ : "선적지 선택"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="선적지 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {shippingPlaces.map((place) => (
+ <CommandItem
+ key={place.code}
+ value={`${place.code} ${place.description || ''}`}
+ onSelect={() => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ shippingPort: place.code
+ }))
+ field.onChange(place.code)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ place.code === biddingConditions.shippingPort
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {place.code} {place.description && `(${place.description})`}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
<FormMessage />
</FormItem>
)}
@@ -776,43 +894,68 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control}
name="biddingConditions.destinationPort"
render={({ field }) => (
- <FormItem>
+ <FormItem className="flex flex-col">
<FormLabel>SHI 하역지</FormLabel>
- <Select
- value={biddingConditions.destinationPort}
- onValueChange={(value) => {
- setBiddingConditions(prev => ({
- ...prev,
- destinationPort: value
- }))
- field.onChange(value)
- }}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="하역지 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {destinationPlaces.length > 0 ? (
- destinationPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "justify-between",
+ !biddingConditions.destinationPort && "text-muted-foreground"
+ )}
+ >
+ {biddingConditions.destinationPort
+ ? destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)
+ ? `${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.code} ${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.description ? `(${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.description})` : ''}`
+ : "하역지 선택"
+ : "하역지 선택"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput placeholder="하역지 검색..." />
+ <CommandList>
+ <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
+ <CommandGroup>
+ {destinationPlaces.map((place) => (
+ <CommandItem
+ key={place.code}
+ value={`${place.code} ${place.description || ''}`}
+ onSelect={() => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ destinationPort: place.code
+ }))
+ field.onChange(place.code)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ place.code === biddingConditions.destinationPort
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {place.code} {place.description && `(${place.description})`}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
<FormMessage />
</FormItem>
)}
/>
- {/* <FormField
+ {/* <FormField
control={form.control}
name="biddingConditions.contractDeliveryDate"
render={({ field }) => (
@@ -829,6 +972,8 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp }))
field.onChange(e.target.value)
}}
+ min="1900-01-01"
+ max="2100-12-31"
/>
</FormControl>
<FormMessage />
@@ -849,7 +994,12 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 계약기간 시작
</FormLabel>
<FormControl>
- <Input type="date" {...field} />
+ <Input
+ type="date"
+ {...field}
+ min="1900-01-01"
+ max="2100-12-31"
+ />
</FormControl>
<FormMessage />
</FormItem>
@@ -866,7 +1016,12 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 계약기간 종료
</FormLabel>
<FormControl>
- <Input type="date" {...field} />
+ <Input
+ type="date"
+ {...field}
+ min="1900-01-01"
+ max="2100-12-31"
+ />
</FormControl>
<FormMessage />
</FormItem>
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index 90923825..27a2c097 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useForm } from 'react-hook-form' -import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign } from 'lucide-react' +import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Check, ChevronsUpDown } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -25,6 +25,20 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { cn } from '@/lib/utils' // CreateBiddingInput 타입 정의가 없으므로 CreateBiddingSchema를 확장하여 사용합니다. import { getBiddingById, updateBiddingBasicInfo, getBiddingConditions, getBiddingNotice, updateBiddingConditions, getBiddingNoticeTemplate } from '@/lib/bidding/service' import { getPurchaseGroupCodes } from '@/components/common/selectors/purchase-group-code' @@ -270,7 +284,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB } // Procurement 데이터 로드 - const [paymentTermsData, incotermsData, shippingData, destinationData, purchaseGroupCodes, procurementManagers] = await Promise.all([ + const [paymentTermsData, incotermsData, shippingData, destinationData] = await Promise.all([ getPaymentTermsForSelection().catch(() => []), getIncotermsForSelection().catch(() => []), getPlaceOfShippingForSelection().catch(() => []), @@ -284,14 +298,20 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB DISPLAY_NAME: bidding.bidPicName || '', PURCHASE_GROUP_CODE: bidding.bidPicCode || '', user: { - id: bidding.bidPicUserId || undefined, + id: bidding.bidPicId || undefined, + name: bidding.bidPicName || '', + email: '', + employeeNumber: null, } }) setSelectedSupplyPic({ DISPLAY_NAME: bidding.supplyPicName || '', PROCUREMENT_MANAGER_CODE: bidding.supplyPicCode || '', user: { - id: bidding.supplyPicUserId || undefined, + id: bidding.supplyPicId || undefined, + name: bidding.supplyPicName || '', + email: '', + employeeNumber: null, } }) // // 입찰담당자 및 조달담당자 초기 선택값 설정 @@ -554,7 +574,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormField control={form.control} name="biddingType" render={({ field }) => ( <FormItem> <FormLabel>입찰유형</FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="입찰유형 선택" /> @@ -575,7 +595,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormField control={form.control} name="contractType" render={({ field }) => ( <FormItem> <FormLabel>계약구분</FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="계약구분 선택" /> @@ -603,7 +623,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormItem> <FormLabel>기타 입찰유형 <span className="text-red-500">*</span></FormLabel> <FormControl> - <Input placeholder="직접 입력하세요" {...field} /> + <Input placeholder="직접 입력하세요" {...field} disabled={readonly} /> </FormControl> <FormMessage /> </FormItem> @@ -656,7 +676,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormField control={form.control} name="awardCount" render={({ field }) => ( <FormItem> <FormLabel>낙찰업체 수</FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="낙찰업체 수 선택" /> @@ -691,6 +711,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB field.onChange(code.DISPLAY_NAME || '') }} placeholder="입찰담당자 선택" + disabled={readonly} /> </FormControl> <FormMessage /> @@ -711,6 +732,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB field.onChange(manager.DISPLAY_NAME || '') }} placeholder="조달담당자 선택" + disabled={readonly} /> </FormControl> <FormMessage /> @@ -723,7 +745,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <Building className="h-3 w-3" /> 구매조직 <span className="text-red-500">*</span> </FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="구매조직 선택" /> @@ -747,7 +769,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormField control={form.control} name="currency" render={({ field }) => ( <FormItem> <FormLabel>통화</FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="통화 선택" /> @@ -770,7 +792,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormField control={form.control} name="noticeType" render={({ field }) => ( <FormItem> <FormLabel>구매유형 <span className="text-red-500">*</span></FormLabel> - <Select onValueChange={field.onChange} value={field.value}> + <Select onValueChange={field.onChange} value={field.value} disabled={readonly}> <FormControl> <SelectTrigger> <SelectValue placeholder="구매유형 선택" /> @@ -801,7 +823,13 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 계약기간 시작 </FormLabel> <FormControl> - <Input type="date" {...field} /> + <Input + type="date" + {...field} + disabled={readonly} + min="1900-01-01" + max="2100-12-31" + /> </FormControl> <FormMessage /> </FormItem> @@ -814,7 +842,13 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 계약기간 종료 </FormLabel> <FormControl> - <Input type="date" {...field} /> + <Input + type="date" + {...field} + disabled={readonly} + min="1900-01-01" + max="2100-12-31" + /> </FormControl> <FormMessage /> </FormItem> @@ -853,91 +887,173 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB {/* 1행: SHI 지급조건, SHI 매입부가가치세 */} <div className="grid grid-cols-2 gap-4 mb-4"> - <div> + <div className="flex flex-col space-y-2"> <FormLabel>SHI 지급조건 <span className="text-red-500">*</span></FormLabel> - <Select - value={biddingConditions.paymentTerms} - onValueChange={(value) => { - setBiddingConditions(prev => ({ - ...prev, - paymentTerms: value - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="지급조건 선택" /> - </SelectTrigger> - <SelectContent> - {paymentTermsOptions.length > 0 ? ( - paymentTermsOptions.map((option) => ( - <SelectItem key={option.code} value={option.code}> - {option.code} {option.description && `(${option.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "justify-between", + !biddingConditions.paymentTerms && "text-muted-foreground" + )} + disabled={readonly} + > + {biddingConditions.paymentTerms + ? paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms) + ? `${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.code} ${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.description ? `(${paymentTermsOptions.find((option) => option.code === biddingConditions.paymentTerms)?.description})` : ''}` + : "지급조건 선택" + : "지급조건 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput placeholder="지급조건 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {paymentTermsOptions.map((option) => ( + <CommandItem + key={option.code} + value={`${option.code} ${option.description || ''}`} + onSelect={() => { + setBiddingConditions(prev => ({ + ...prev, + paymentTerms: option.code + })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + option.code === biddingConditions.paymentTerms + ? "opacity-100" + : "opacity-0" + )} + /> + {option.code} {option.description && `(${option.description})`} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> </div> - <div> + <div className="flex flex-col space-y-2"> <FormLabel>SHI 매입부가가치세 <span className="text-red-500">*</span></FormLabel> - <Select - value={biddingConditions.taxConditions} - onValueChange={(value) => { - setBiddingConditions(prev => ({ - ...prev, - taxConditions: value - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="세금조건 선택" /> - </SelectTrigger> - <SelectContent> - {TAX_CONDITIONS.map((condition) => ( - <SelectItem key={condition.code} value={condition.code}> - {condition.name} - </SelectItem> - ))} - </SelectContent> - </Select> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "justify-between", + !biddingConditions.taxConditions && "text-muted-foreground" + )} + disabled={readonly} + > + {biddingConditions.taxConditions + ? TAX_CONDITIONS.find((condition) => condition.code === biddingConditions.taxConditions)?.name + : "세금조건 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput placeholder="세금조건 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {TAX_CONDITIONS.map((condition) => ( + <CommandItem + key={condition.code} + value={`${condition.code} ${condition.name}`} + onSelect={() => { + setBiddingConditions(prev => ({ + ...prev, + taxConditions: condition.code + })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + condition.code === biddingConditions.taxConditions + ? "opacity-100" + : "opacity-0" + )} + /> + {condition.name} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> </div> </div> {/* 2행: SHI 인도조건, SHI 인도조건2 */} <div className="grid grid-cols-2 gap-4 mb-4"> - <div> + <div className="flex flex-col space-y-2"> <FormLabel>SHI 인도조건 <span className="text-red-500">*</span></FormLabel> - <Select - value={biddingConditions.incoterms} - onValueChange={(value) => { - setBiddingConditions(prev => ({ - ...prev, - incoterms: value - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="인코텀즈 선택" /> - </SelectTrigger> - <SelectContent> - {incotermsOptions.length > 0 ? ( - incotermsOptions.map((option) => ( - <SelectItem key={option.code} value={option.code}> - {option.code} {option.description && `(${option.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "justify-between", + !biddingConditions.incoterms && "text-muted-foreground" + )} + disabled={readonly} + > + {biddingConditions.incoterms + ? incotermsOptions.find((option) => option.code === biddingConditions.incoterms) + ? `${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.code} ${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.description ? `(${incotermsOptions.find((option) => option.code === biddingConditions.incoterms)?.description})` : ''}` + : "인코텀즈 선택" + : "인코텀즈 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput placeholder="인코텀즈 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {incotermsOptions.map((option) => ( + <CommandItem + key={option.code} + value={`${option.code} ${option.description || ''}`} + onSelect={() => { + setBiddingConditions(prev => ({ + ...prev, + incoterms: option.code + })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + option.code === biddingConditions.incoterms + ? "opacity-100" + : "opacity-0" + )} + /> + {option.code} {option.description && `(${option.description})`} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> </div> <div> @@ -951,70 +1067,123 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB incotermsOption: e.target.value })) }} + disabled={readonly} /> </div> </div> {/* 3행: SHI 선적지, SHI 하역지 */} <div className="grid grid-cols-2 gap-4 mb-4"> - <div> + <div className="flex flex-col space-y-2"> <FormLabel>SHI 선적지</FormLabel> - <Select - value={biddingConditions.shippingPort} - onValueChange={(value) => { - setBiddingConditions(prev => ({ - ...prev, - shippingPort: value - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="선적지 선택" /> - </SelectTrigger> - <SelectContent> - {shippingPlaces.length > 0 ? ( - shippingPlaces.map((place) => ( - <SelectItem key={place.code} value={place.code}> - {place.code} {place.description && `(${place.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "justify-between", + !biddingConditions.shippingPort && "text-muted-foreground" + )} + disabled={readonly} + > + {biddingConditions.shippingPort + ? shippingPlaces.find((place) => place.code === biddingConditions.shippingPort) + ? `${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.code} ${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.description ? `(${shippingPlaces.find((place) => place.code === biddingConditions.shippingPort)?.description})` : ''}` + : "선적지 선택" + : "선적지 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput placeholder="선적지 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {shippingPlaces.map((place) => ( + <CommandItem + key={place.code} + value={`${place.code} ${place.description || ''}`} + onSelect={() => { + setBiddingConditions(prev => ({ + ...prev, + shippingPort: place.code + })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + place.code === biddingConditions.shippingPort + ? "opacity-100" + : "opacity-0" + )} + /> + {place.code} {place.description && `(${place.description})`} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> </div> - <div> + <div className="flex flex-col space-y-2"> <FormLabel>SHI 하역지</FormLabel> - <Select - value={biddingConditions.destinationPort} - onValueChange={(value) => { - setBiddingConditions(prev => ({ - ...prev, - destinationPort: value - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="하역지 선택" /> - </SelectTrigger> - <SelectContent> - {destinationPlaces.length > 0 ? ( - destinationPlaces.map((place) => ( - <SelectItem key={place.code} value={place.code}> - {place.code} {place.description && `(${place.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "justify-between", + !biddingConditions.destinationPort && "text-muted-foreground" + )} + disabled={readonly} + > + {biddingConditions.destinationPort + ? destinationPlaces.find((place) => place.code === biddingConditions.destinationPort) + ? `${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.code} ${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.description ? `(${destinationPlaces.find((place) => place.code === biddingConditions.destinationPort)?.description})` : ''}` + : "하역지 선택" + : "하역지 선택"} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput placeholder="하역지 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {destinationPlaces.map((place) => ( + <CommandItem + key={place.code} + value={`${place.code} ${place.description || ''}`} + onSelect={() => { + setBiddingConditions(prev => ({ + ...prev, + destinationPort: place.code + })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + place.code === biddingConditions.destinationPort + ? "opacity-100" + : "opacity-0" + )} + /> + {place.code} {place.description && `(${place.description})`} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> </div> </div> @@ -1045,6 +1214,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB })) }} id="price-adjustment" + disabled={readonly} /> <FormLabel htmlFor="price-adjustment" className="text-sm"> {biddingConditions.isPriceAdjustmentApplicable ? "적용" : "미적용"} @@ -1067,7 +1237,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB })) }} rows={3} - readOnly={readonly} + disabled={readonly} /> </div> </div> @@ -1135,15 +1305,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }} onDropRejected={() => { toast({ - title: "File upload rejected", - description: "Please check file size and type.", + title: "파일 업로드 거부", + description: "파일 크기와 유형을 확인해주세요.", variant: "destructive", }) }} + disabled={readonly} > <DropzoneZone> <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" /> - <DropzoneTitle className="text-lg font-medium"> + <DropzoneTitle> 파일을 드래그하거나 클릭하여 업로드 </DropzoneTitle> <DropzoneDescription className="text-sm text-muted-foreground"> @@ -1194,6 +1365,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB variant="ghost" size="sm" onClick={() => handleDeleteDocument(doc.id)} + disabled={readonly} > 삭제 </Button> @@ -1227,15 +1399,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }} onDropRejected={() => { toast({ - title: "File upload rejected", - description: "Please check file size and type.", + title: "파일 업로드 거부", + description: "파일 크기와 유형을 확인해주세요.", variant: "destructive", }) }} + disabled={readonly} > <DropzoneZone> <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" /> - <DropzoneTitle className="text-lg font-medium"> + <DropzoneTitle> 파일을 드래그하거나 클릭하여 업로드 </DropzoneTitle> <DropzoneDescription className="text-sm text-muted-foreground"> @@ -1281,6 +1454,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB variant="ghost" size="sm" onClick={() => handleDeleteDocument(doc.id)} + disabled={readonly} > 삭제 </Button> diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index f6b3a3f0..6634f528 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -494,7 +494,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC </p> </div> {!readonly && ( - <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2"> + <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2" disabled={readonly}> <Plus className="h-4 w-4" /> 업체 추가 </Button> @@ -532,6 +532,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC <Checkbox checked={selectedVendor?.id === vendor.id} onCheckedChange={() => handleVendorSelect(vendor)} + disabled={readonly} /> </TableCell> <TableCell className="font-medium">{vendor.vendorName}</TableCell> @@ -565,6 +566,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC onCheckedChange={(checked) => handleTogglePriceAdjustmentQuestion(vendor.id, checked as boolean) } + disabled={readonly} /> <span className="text-sm text-muted-foreground"> {vendor.isPriceAdjustmentApplicableQuestion ? '예' : '아니오'} @@ -577,6 +579,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC size="sm" onClick={() => handleRemoveVendor(vendor.id)} className="text-red-600 hover:text-red-800" + disabled={readonly} > <Trash2 className="h-4 w-4" /> </Button> @@ -607,6 +610,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC variant="outline" onClick={handleOpenAddContactFromVendor} className="flex items-center gap-2" + disabled={readonly} > <User className="h-4 w-4" /> 업체 담당자 추가 @@ -614,6 +618,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC <Button onClick={() => setAddContactDialogOpen(true)} className="flex items-center gap-2" + disabled={readonly} > <Plus className="h-4 w-4" /> 담당자 수기 입력 @@ -652,6 +657,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC size="sm" onClick={() => handleDeleteContact(biddingCompanyContact.id)} className="text-red-600 hover:text-red-800" + disabled={readonly} > <Trash2 className="h-4 w-4" /> </Button> diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index ef0aa568..9d858f40 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -807,7 +807,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Checkbox checked={item.isRepresentative} onCheckedChange={() => setRepresentativeItem(item.id)} - disabled={items.length <= 1 && item.isRepresentative} + disabled={(items.length <= 1 && item.isRepresentative) || readonly} title="대표 아이템" /> </td> @@ -831,6 +831,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems } }} placeholder="프로젝트 선택" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> @@ -942,21 +943,25 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Input type="number" min="0" + step="0.001" placeholder="수량" value={item.quantity || ''} onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })} className="h-8 text-xs" required + disabled={readonly} /> ) : ( <Input type="number" min="0" + step="0.001" placeholder="중량" value={item.totalWeight || ''} onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })} className="h-8 text-xs" required + disabled={readonly} /> )} </td> @@ -966,6 +971,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.quantityUnit || 'EA'} onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })} required + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -984,6 +990,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.weightUnit || 'KG'} onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })} required + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -1004,6 +1011,9 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} className="h-8 text-xs" required + disabled={readonly} + min="1900-01-01" + max="2100-12-31" /> </td> <td className="border-r px-3 py-2"> @@ -1015,12 +1025,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.priceUnit || ''} onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })} className="h-8 text-xs" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> <Select value={item.purchaseUnit || 'EA'} onValueChange={(value) => updatePRItem(item.id, { purchaseUnit: value })} + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -1043,11 +1055,12 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Input type="number" min="0" - step="0.01" + step="0.001" placeholder="자재순중량" value={item.materialWeight || ''} onChange={(e) => updatePRItem(item.id, { materialWeight: e.target.value })} className="h-8 text-xs" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> @@ -1057,6 +1070,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={formatNumberWithCommas(item.targetUnitPrice)} onChange={(e) => updatePRItem(item.id, { targetUnitPrice: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> @@ -1072,6 +1086,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Select value={item.targetCurrency || 'KRW'} onValueChange={(value) => updatePRItem(item.id, { targetCurrency: value })} + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -1091,12 +1106,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={formatNumberWithCommas(item.budgetAmount)} onChange={(e) => updatePRItem(item.id, { budgetAmount: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> <Select value={item.budgetCurrency || 'KRW'} onValueChange={(value) => updatePRItem(item.id, { budgetCurrency: value })} + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -1116,12 +1133,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={formatNumberWithCommas(item.actualAmount)} onChange={(e) => updatePRItem(item.id, { actualAmount: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" + disabled={readonly} /> </td> <td className="border-r px-3 py-2"> <Select value={item.actualCurrency || 'KRW'} onValueChange={(value) => updatePRItem(item.id, { actualCurrency: value })} + disabled={readonly} > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -1148,6 +1167,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems setWbsCodeDialogOpen(true) }} className="w-full justify-start h-8 text-xs" + disabled={readonly} > {item.wbsCode ? ( <span className="truncate"> @@ -1201,6 +1221,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems setCostCenterDialogOpen(true) }} className="w-full justify-start h-8 text-xs" + disabled={readonly} > {item.costCenterCode ? ( <span className="truncate"> @@ -1254,6 +1275,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems setGlAccountDialogOpen(true) }} className="w-full justify-start h-8 text-xs" + disabled={readonly} > {item.glAccountCode ? ( <span className="truncate"> @@ -1309,7 +1331,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems variant="ghost" size="sm" onClick={() => handleRemoveItem(item.id)} - disabled={items.length <= 1} + disabled={items.length <= 1 || readonly} className="h-7 w-7 p-0" title="품목 삭제" > @@ -1343,11 +1365,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </p> </div> <div className="flex gap-2"> - <Button onClick={() => setPreQuoteDialogOpen(true)} variant="outline" className="flex items-center gap-2"> + <Button onClick={() => setPreQuoteDialogOpen(true)} variant="outline" className="flex items-center gap-2" disabled={readonly}> <FileText className="h-4 w-4" /> 사전견적 </Button> - <Button onClick={handleAddItem} className="flex items-center gap-2"> + <Button onClick={handleAddItem} className="flex items-center gap-2" disabled={readonly}> <Plus className="h-4 w-4" /> 품목 추가 </Button> @@ -1364,6 +1386,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems onChange={(e) => setTargetPriceCalculationCriteria(e.target.value)} rows={3} className="resize-none" + disabled={readonly} /> <p className="text-xs text-muted-foreground"> 내정가를 산정한 기준이나 방법을 입력하세요 @@ -1379,6 +1402,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems checked={quantityWeightMode === 'quantity'} onChange={() => handleQuantityWeightModeChange('quantity')} className="h-4 w-4" + disabled={readonly} /> <label htmlFor="quantity-mode" className="text-sm">수량 기준</label> </div> @@ -1390,6 +1414,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems checked={quantityWeightMode === 'weight'} onChange={() => handleQuantityWeightModeChange('weight')} className="h-4 w-4" + disabled={readonly} /> <label htmlFor="weight-mode" className="text-sm">중량 기준</label> </div> diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index 4ddaee08..49659ae7 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -633,6 +633,9 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={schedule.submissionStartDate} onChange={(e) => handleScheduleChange('submissionStartDate', e.target.value)} className={!schedule.submissionStartDate ? 'border-red-200' : ''} + disabled={readonly} + min="1900-01-01T00:00" + max="2100-12-31T23:59" /> {!schedule.submissionStartDate && ( <p className="text-sm text-red-500">제출 시작일시는 필수입니다</p> @@ -646,6 +649,9 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={schedule.submissionEndDate} onChange={(e) => handleScheduleChange('submissionEndDate', e.target.value)} className={!schedule.submissionEndDate ? 'border-red-200' : ''} + disabled={readonly} + min="1900-01-01T00:00" + max="2100-12-31T23:59" /> {!schedule.submissionEndDate && ( <p className="text-sm text-red-500">제출 마감일시는 필수입니다</p> @@ -665,6 +671,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc <Switch checked={schedule.isUrgent || false} onCheckedChange={(checked) => handleScheduleChange('isUrgent', checked)} + disabled={readonly} /> </div> @@ -679,6 +686,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc <Switch checked={schedule.hasSpecificationMeeting || false} onCheckedChange={(checked) => handleScheduleChange('hasSpecificationMeeting', checked)} + disabled={readonly} /> </div> @@ -693,6 +701,9 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.meetingDate} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, meetingDate: e.target.value }))} className={!specMeetingInfo.meetingDate ? 'border-red-200' : ''} + disabled={readonly} + min="1900-01-01T00:00" + max="2100-12-31T23:59" /> {!specMeetingInfo.meetingDate && ( <p className="text-sm text-red-500 mt-1">회의일시는 필수입니다</p> @@ -704,6 +715,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc placeholder="예: 14:00 ~ 16:00" value={specMeetingInfo.meetingTime} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, meetingTime: e.target.value }))} + disabled={readonly} /> </div> </div> @@ -714,6 +726,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.location} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, location: e.target.value }))} className={!specMeetingInfo.location ? 'border-red-200' : ''} + disabled={readonly} /> {!specMeetingInfo.location && ( <p className="text-sm text-red-500 mt-1">회의 장소는 필수입니다</p> @@ -725,6 +738,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc placeholder="회의 장소 주소" value={specMeetingInfo.address} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, address: e.target.value }))} + disabled={readonly} /> </div> <div className="grid grid-cols-3 gap-4"> @@ -735,6 +749,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.contactPerson} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, contactPerson: e.target.value }))} className={!specMeetingInfo.contactPerson ? 'border-red-200' : ''} + disabled={readonly} /> {!specMeetingInfo.contactPerson && ( <p className="text-sm text-red-500 mt-1">담당자는 필수입니다</p> @@ -746,6 +761,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc placeholder="전화번호" value={specMeetingInfo.contactPhone} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, contactPhone: e.target.value }))} + disabled={readonly} /> </div> <div> @@ -755,6 +771,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc placeholder="이메일" value={specMeetingInfo.contactEmail} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, contactEmail: e.target.value }))} + disabled={readonly} /> </div> </div> @@ -765,6 +782,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.agenda} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, agenda: e.target.value }))} rows={3} + disabled={readonly} /> </div> <div> @@ -774,6 +792,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.materials} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, materials: e.target.value }))} rows={3} + disabled={readonly} /> </div> <div> @@ -783,6 +802,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc value={specMeetingInfo.notes} onChange={(e) => setSpecMeetingInfo((prev) => ({ ...prev, notes: e.target.value }))} rows={3} + disabled={readonly} /> </div> </div> @@ -799,6 +819,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc onChange={(e) => handleScheduleChange('remarks', e.target.value)} placeholder="일정에 대한 추가 설명이나 참고사항을 입력하세요" rows={4} + disabled={readonly} /> </div> </div> diff --git a/components/bidding/receive/bidding-participants-dialog.tsx b/components/bidding/receive/bidding-participants-dialog.tsx new file mode 100644 index 00000000..5739a07e --- /dev/null +++ b/components/bidding/receive/bidding-participants-dialog.tsx @@ -0,0 +1,216 @@ +'use client' + +import * as React from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { DataTable } from '@/components/data-table/data-table' +import { ColumnDef } from '@tanstack/react-table' +import { Building2, User, Mail, Phone, Calendar, BadgeCheck } from 'lucide-react' +import { formatDate } from '@/lib/utils' +import { Badge } from '@/components/ui/badge' + +interface ParticipantCompany { + id: number + biddingId: number + companyId: number | null + vendorName: string + vendorCode: string + contactPerson: string | null + contactEmail: string | null + contactPhone: string | null + invitationStatus: string + updatedAt: Date | null +} + +interface BiddingParticipantsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + biddingId: number | null + participantType: 'expected' | 'participated' | 'declined' | 'pending' | null + companies: ParticipantCompany[] +} + +const invitationStatusLabels: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' }> = { + invited: { label: '초대됨', variant: 'outline' }, + accepted: { label: '참여확정', variant: 'default' }, + declined: { label: '포기', variant: 'destructive' }, + pending: { label: '미제출', variant: 'secondary' }, + submitted: { label: '제출완료', variant: 'default' }, +} + +const participantTypeLabels: Record<string, string> = { + expected: '참여예정협력사', + participated: '참여협력사', + declined: '포기협력사', + pending: '미제출협력사', +} + +export function BiddingParticipantsDialog({ + open, + onOpenChange, + biddingId, + participantType, + companies, +}: BiddingParticipantsDialogProps) { + const columns = React.useMemo<ColumnDef<ParticipantCompany>[]>( + () => [ + { + id: 'vendorCode', + accessorKey: 'vendorCode', + header: '협력사코드', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <BadgeCheck className="h-4 w-4 text-muted-foreground" /> + <span className="font-mono text-sm">{row.original.vendorCode}</span> + </div> + ), + size: 120, + }, + { + id: 'vendorName', + accessorKey: 'vendorName', + header: '협력사명', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <Building2 className="h-4 w-4 text-muted-foreground" /> + <span className="font-medium">{row.original.vendorName}</span> + </div> + ), + size: 200, + }, + { + id: 'invitationStatus', + accessorKey: 'invitationStatus', + header: '구분', + cell: ({ row }) => { + const status = row.original.invitationStatus + const statusInfo = invitationStatusLabels[status] || { label: status, variant: 'outline' as const } + return ( + <Badge variant={statusInfo.variant}> + {statusInfo.label} + </Badge> + ) + }, + size: 100, + }, + { + id: 'updatedAt', + accessorKey: 'updatedAt', + header: '응찰/포기일시', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <Calendar className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm"> + {row.original.updatedAt ? formatDate(row.original.updatedAt) : '-'} + </span> + </div> + ), + size: 150, + }, + { + id: 'contactPerson', + accessorKey: 'contactPerson', + header: '협력사 담당자', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <User className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm">{row.original.contactPerson || '-'}</span> + </div> + ), + size: 120, + }, + { + id: 'contactEmail', + accessorKey: 'contactEmail', + header: '담당자 이메일', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <Mail className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">{row.original.contactEmail || '-'}</span> + </div> + ), + size: 200, + }, + { + id: 'contactPhone', + accessorKey: 'contactPhone', + header: '담당자 전화번호', + cell: ({ row }) => ( + <div className="flex items-center gap-2"> + <Phone className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">{row.original.contactPhone || '-'}</span> + </div> + ), + size: 150, + }, + ], + [] + ) + + const table = React.useMemo( + () => ({ + data: companies, + columns, + pageCount: 1, + }), + [companies, columns] + ) + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-6xl max-h-[80vh] overflow-hidden flex flex-col"> + <DialogHeader> + <DialogTitle> + {'협력사 목록'} + {' '} + <span className="text-muted-foreground">({companies.length}개)</span> + </DialogTitle> + </DialogHeader> + <div className="flex-1 overflow-auto"> + <div className="border rounded-lg"> + <div className="overflow-x-auto"> + <table className="w-full"> + <thead className="bg-muted/50"> + <tr> + {columns.map((column) => ( + <th + key={column.id} + className="px-4 py-3 text-left text-sm font-medium" + style={{ width: column.size }} + > + {typeof column.header === 'function' + ? column.header({} as any) + : column.header} + </th> + ))} + </tr> + </thead> + <tbody> + {companies.length === 0 ? ( + <tr> + <td colSpan={columns.length} className="px-4 py-8 text-center text-muted-foreground"> + 협력사가 없습니다. + </td> + </tr> + ) : ( + companies.map((company) => ( + <tr key={company.id} className="border-t hover:bg-muted/50"> + {columns.map((column) => ( + <td key={column.id} className="px-4 py-3"> + {column.cell + ? column.cell({ row: { original: company } } as any) + : (company as any)[column.accessorKey as string]} + </td> + ))} + </tr> + )) + )} + </tbody> + </table> + </div> + </div> + </div> + </DialogContent> + </Dialog> + ) +} + |
