summaryrefslogtreecommitdiff
path: root/lib/b-rfq/initial/short-list-confirm-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-08 11:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-08 11:23:40 +0000
commitb84621f9b2b7161a5ad4f0b194264e9df3e65dbf (patch)
treece5ec30b3d1e5104a3a2d942c71973779436783b /lib/b-rfq/initial/short-list-confirm-dialog.tsx
parent97936ddf280c56a4f122dedcb8dc389d0d2e63a2 (diff)
(대표님) 20250708 미반영분 커밋
Diffstat (limited to 'lib/b-rfq/initial/short-list-confirm-dialog.tsx')
-rw-r--r--lib/b-rfq/initial/short-list-confirm-dialog.tsx269
1 files changed, 269 insertions, 0 deletions
diff --git a/lib/b-rfq/initial/short-list-confirm-dialog.tsx b/lib/b-rfq/initial/short-list-confirm-dialog.tsx
new file mode 100644
index 00000000..92c62dc0
--- /dev/null
+++ b/lib/b-rfq/initial/short-list-confirm-dialog.tsx
@@ -0,0 +1,269 @@
+"use client"
+
+import * as React from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+import { Loader2, Building, CheckCircle2, XCircle } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+import { shortListConfirm } from "../service"
+import { InitialRfqDetailView } from "@/db/schema"
+
+const shortListSchema = z.object({
+ selectedVendorIds: z.array(z.number()).min(1, "최소 1개 이상의 벤더를 선택해야 합니다."),
+})
+
+type ShortListFormData = z.infer<typeof shortListSchema>
+
+interface ShortListConfirmDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ rfqId: number
+ vendors: InitialRfqDetailView[]
+ onSuccess?: () => void
+}
+
+export function ShortListConfirmDialog({
+ open,
+ onOpenChange,
+ rfqId,
+ vendors,
+ onSuccess
+}: ShortListConfirmDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ const form = useForm<ShortListFormData>({
+ resolver: zodResolver(shortListSchema),
+ defaultValues: {
+ selectedVendorIds: vendors
+ .filter(vendor => vendor.shortList === true)
+ .map(vendor => vendor.vendorId)
+ .filter(Boolean) as number[]
+ },
+ })
+
+ const watchedSelectedIds = form.watch("selectedVendorIds")
+
+ // 선택된/탈락된 벤더 계산
+ const selectedVendors = vendors.filter(vendor =>
+ vendor.vendorId && watchedSelectedIds.includes(vendor.vendorId)
+ )
+ const rejectedVendors = vendors.filter(vendor =>
+ vendor.vendorId && !watchedSelectedIds.includes(vendor.vendorId)
+ )
+
+ async function onSubmit(data: ShortListFormData) {
+ if (!rfqId) return
+
+ setIsLoading(true)
+
+ try {
+ const result = await shortListConfirm({
+ rfqId,
+ selectedVendorIds: data.selectedVendorIds,
+ rejectedVendorIds: vendors
+ .filter(v => v.vendorId && !data.selectedVendorIds.includes(v.vendorId))
+ .map(v => v.vendorId!)
+ })
+
+ if (result.success) {
+ toast.success(result.message)
+ onOpenChange(false)
+ form.reset()
+ onSuccess?.()
+ } else {
+ toast.error(result.message || "Short List 확정에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("Short List confirm error:", error)
+ toast.error("Short List 확정 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleVendorToggle = (vendorId: number, checked: boolean) => {
+ const currentSelected = form.getValues("selectedVendorIds")
+
+ if (checked) {
+ form.setValue("selectedVendorIds", [...currentSelected, vendorId])
+ } else {
+ form.setValue("selectedVendorIds", currentSelected.filter(id => id !== vendorId))
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <CheckCircle2 className="h-5 w-5 text-green-600" />
+ Short List 확정
+ </DialogTitle>
+ <DialogDescription>
+ 최종 RFQ로 진행할 벤더를 선택해주세요. 선택되지 않은 벤더에게는 자동으로 Letter of Regret이 발송됩니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ <FormField
+ control={form.control}
+ name="selectedVendorIds"
+ render={() => (
+ <FormItem>
+ <FormLabel className="text-base font-semibold">
+ 벤더 선택 ({vendors.length}개 업체)
+ </FormLabel>
+ <FormControl>
+ <ScrollArea className="h-[400px] border rounded-md p-4">
+ <div className="space-y-4">
+ {vendors.map((vendor) => {
+ const isSelected = vendor.vendorId && watchedSelectedIds.includes(vendor.vendorId)
+
+ return (
+ <div
+ key={vendor.vendorId}
+ className={`flex items-start space-x-3 p-3 rounded-lg border transition-colors ${
+ isSelected
+ ? 'border-green-200 bg-green-50'
+ : 'border-red-100 bg-red-50'
+ }`}
+ >
+ <Checkbox
+ checked={isSelected}
+ onCheckedChange={(checked) =>
+ vendor.vendorId && handleVendorToggle(vendor.vendorId, !!checked)
+ }
+ className="mt-1"
+ />
+ <div className="flex-1 space-y-2">
+ <div className="flex items-center gap-2">
+ <Building className="h-4 w-4 text-muted-foreground" />
+ <span className="font-medium">{vendor.vendorName}</span>
+ {isSelected ? (
+ <Badge variant="secondary" className="bg-green-100 text-green-800">
+ 선택됨
+ </Badge>
+ ) : (
+ <Badge variant="secondary" className="bg-red-100 text-red-800">
+ 탈락
+ </Badge>
+ )}
+ </div>
+ <div className="text-sm text-muted-foreground">
+ <span className="font-mono">{vendor.vendorCode}</span>
+ {vendor.vendorCountry && (
+ <>
+ <span className="mx-2">•</span>
+ <span>{vendor.vendorCountry === "KR" ? "국내" : "해외"}</span>
+ </>
+ )}
+ {vendor.vendorCategory && (
+ <>
+ <span className="mx-2">•</span>
+ <span>{vendor.vendorCategory}</span>
+ </>
+ )}
+ {vendor.vendorBusinessSize && (
+ <>
+ <span className="mx-2">•</span>
+ <span>{vendor.vendorBusinessSize}</span>
+ </>
+ )}
+ </div>
+ <div className="text-xs text-muted-foreground">
+ RFQ 상태: <Badge variant="outline" className="text-xs">
+ {vendor.initialRfqStatus}
+ </Badge>
+ </div>
+ </div>
+ </div>
+ )
+ })}
+ </div>
+ </ScrollArea>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 요약 정보 */}
+ <div className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg">
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-green-700">
+ <CheckCircle2 className="h-4 w-4" />
+ <span className="font-medium">선택된 벤더</span>
+ </div>
+ <div className="text-2xl font-bold text-green-700">
+ {selectedVendors.length}개 업체
+ </div>
+ {selectedVendors.length > 0 && (
+ <div className="text-sm text-muted-foreground">
+ {selectedVendors.map(v => v.vendorName).join(", ")}
+ </div>
+ )}
+ </div>
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-red-700">
+ <XCircle className="h-4 w-4" />
+ <span className="font-medium">탈락 벤더</span>
+ </div>
+ <div className="text-2xl font-bold text-red-700">
+ {rejectedVendors.length}개 업체
+ </div>
+ {rejectedVendors.length > 0 && (
+ <div className="text-sm text-muted-foreground">
+ Letter of Regret 발송 예정
+ </div>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isLoading || selectedVendors.length === 0}
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ Short List 확정
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file