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/bidding/manage | |
| parent | f57898bd240d068301ce3ef477f52cff1234e4ee (diff) | |
(최겸) 구매 입찰 피드백 반영(90%)
Diffstat (limited to 'components/bidding/manage')
4 files changed, 381 insertions, 155 deletions
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> |
