diff options
Diffstat (limited to 'lib/vendors/table/request-vendor-investigate-dialog.tsx')
| -rw-r--r-- | lib/vendors/table/request-vendor-investigate-dialog.tsx | 243 |
1 files changed, 218 insertions, 25 deletions
diff --git a/lib/vendors/table/request-vendor-investigate-dialog.tsx b/lib/vendors/table/request-vendor-investigate-dialog.tsx index 0309ee4a..b3deafce 100644 --- a/lib/vendors/table/request-vendor-investigate-dialog.tsx +++ b/lib/vendors/table/request-vendor-investigate-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { type Row } from "@tanstack/react-table" -import { Loader, Check, SendHorizonal } from "lucide-react" +import { Loader, Check, SendHorizonal, AlertCircle, AlertTriangle } from "lucide-react" import { toast } from "sonner" import { useMediaQuery } from "@/hooks/use-media-query" @@ -27,8 +27,29 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" + import { Vendor } from "@/db/schema/vendors" -import { requestInvestigateVendors } from "@/lib/vendor-investigation/service" +import { requestInvestigateVendors, getExistingInvestigationsForVendors } from "@/lib/vendor-investigation/service" +import { useSession } from "next-auth/react" +import { formatDate } from "@/lib/utils" interface ApprovalVendorDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { @@ -37,21 +58,98 @@ interface ApprovalVendorDialogProps onSuccess?: () => void } +// Helper function to get status badge variant and text +function getStatusBadge(status: string) { + switch (status) { + case "REQUESTED": + return { variant: "secondary", text: "Requested" } + case "SCHEDULED": + return { variant: "warning", text: "Scheduled" } + case "IN_PROGRESS": + return { variant: "default", text: "In Progress" } + case "COMPLETED": + return { variant: "success", text: "Completed" } + case "CANCELLED": + return { variant: "destructive", text: "Cancelled" } + default: + return { variant: "outline", text: status } + } +} + export function RequestVendorsInvestigateDialog({ vendors, showTrigger = true, onSuccess, ...props }: ApprovalVendorDialogProps) { - - console.log(vendors) const [isApprovePending, startApproveTransition] = React.useTransition() + const [isLoading, setIsLoading] = React.useState(true) + const [existingInvestigations, setExistingInvestigations] = React.useState<any[]>([]) const isDesktop = useMediaQuery("(min-width: 640px)") + const { data: session } = useSession() + + // Fetch existing investigations when dialog opens + React.useEffect(() => { + if (vendors.length > 0) { + setIsLoading(true) + const fetchExistingInvestigations = async () => { + try { + const vendorIds = vendors.map(vendor => vendor.id) + const result = await getExistingInvestigationsForVendors(vendorIds) + setExistingInvestigations(result) + } catch (error) { + console.error("Failed to fetch existing investigations:", error) + toast.error("Failed to fetch existing investigations") + } finally { + setIsLoading(false) + } + } + + fetchExistingInvestigations() + } + }, [vendors]) + + // Group vendors by investigation status + const vendorsWithInvestigations = React.useMemo(() => { + if (!existingInvestigations.length) return { withInvestigations: [], withoutInvestigations: vendors } + + const vendorMap = new Map(vendors.map(v => [v.id, v])) + const withInvestigations: Array<{ vendor: typeof vendors[0], investigation: any }> = [] + + // Find vendors with existing investigations + existingInvestigations.forEach(inv => { + const vendor = vendorMap.get(inv.vendorId) + if (vendor) { + withInvestigations.push({ vendor, investigation: inv }) + vendorMap.delete(inv.vendorId) + } + }) + + // Remaining vendors don't have investigations + const withoutInvestigations = Array.from(vendorMap.values()) + + return { withInvestigations, withoutInvestigations } + }, [vendors, existingInvestigations]) function onApprove() { + if (!session?.user?.id) { + toast.error("사용자 인증 정보를 찾을 수 없습니다.") + return + } + + // Only request investigations for vendors without existing ones + const vendorsToRequest = vendorsWithInvestigations.withoutInvestigations + + if (vendorsToRequest.length === 0) { + toast.info("모든 선택된 업체에 이미 실사 요청이 있습니다.") + props.onOpenChange?.(false) + return + } + startApproveTransition(async () => { const { error } = await requestInvestigateVendors({ - ids: vendors.map((vendor) => vendor.id), + ids: vendorsToRequest.map((vendor) => vendor.id), + userId: Number(session.user.id) }) if (error) { @@ -60,11 +158,102 @@ export function RequestVendorsInvestigateDialog({ } props.onOpenChange?.(false) - toast.success("Vendor Investigation successfully sent to 벤더실사담당자") + toast.success(`${vendorsToRequest.length}개 업체에 대한 실사 요청을 보냈습니다.`) onSuccess?.() }) } + const renderContent = () => { + return ( + <> + <div className="space-y-4"> + {isLoading ? ( + <div className="flex items-center justify-center py-4"> + <Loader className="size-6 animate-spin text-muted-foreground" /> + </div> + ) : ( + <> + {vendorsWithInvestigations.withInvestigations.length > 0 && ( + <Alert> + <AlertTriangle className="h-4 w-4" /> + <AlertTitle>기존 실사 요청 정보가 있습니다</AlertTitle> + <AlertDescription> + 선택한 {vendors.length}개 업체 중 {vendorsWithInvestigations.withInvestigations.length}개 업체에 대한 + 기존 실사 요청이 있습니다. 새로운 요청은 기존 데이터가 없는 업체에만 적용됩니다. + </AlertDescription> + </Alert> + )} + + {vendorsWithInvestigations.withInvestigations.length > 0 && ( + <Accordion type="single" collapsible className="w-full"> + <AccordionItem value="existing-investigations"> + <AccordionTrigger className="font-medium"> + 기존 실사 요청 ({vendorsWithInvestigations.withInvestigations.length}) + </AccordionTrigger> + <AccordionContent> + <ScrollArea className="max-h-[200px]"> + <Table> + <TableHeader> + <TableRow> + <TableHead>업체명</TableHead> + <TableHead>상태</TableHead> + <TableHead>요청일</TableHead> + <TableHead>예정 일정</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {vendorsWithInvestigations.withInvestigations.map(({ vendor, investigation }) => { + const status = getStatusBadge(investigation.investigationStatus) + return ( + <TableRow key={investigation.investigationId}> + <TableCell className="font-medium">{vendor.vendorName}</TableCell> + <TableCell> + <Badge variant={status.variant as any}>{status.text}</Badge> + </TableCell> + <TableCell>{formatDate(investigation.createdAt)}</TableCell> + <TableCell> + {investigation.scheduledStartAt + ? formatDate(investigation.scheduledStartAt) + : "미정"} + </TableCell> + </TableRow> + ) + })} + </TableBody> + </Table> + </ScrollArea> + </AccordionContent> + </AccordionItem> + </Accordion> + )} + + <div> + <h3 className="text-sm font-medium mb-2"> + 새로운 실사가 요청될 업체 ({vendorsWithInvestigations.withoutInvestigations.length}) + </h3> + {vendorsWithInvestigations.withoutInvestigations.length > 0 ? ( + <ScrollArea className="max-h-[200px]"> + <ul className="space-y-1"> + {vendorsWithInvestigations.withoutInvestigations.map((vendor) => ( + <li key={vendor.id} className="text-sm py-1 px-2 border-b"> + {vendor.vendorName} ({vendor.vendorCode || "코드 없음"}) + </li> + ))} + </ul> + </ScrollArea> + ) : ( + <p className="text-sm text-muted-foreground py-2"> + 모든 선택된 업체에 이미 실사 요청이 있습니다. + </p> + )} + </div> + </> + )} + </div> + </> + ) + } + if (isDesktop) { return ( <Dialog {...props}> @@ -72,29 +261,30 @@ export function RequestVendorsInvestigateDialog({ <DialogTrigger asChild> <Button variant="outline" size="sm" className="gap-2"> <SendHorizonal className="size-4" aria-hidden="true" /> - Vendor Investigation Request ({vendors.length}) + 실사 요청 ({vendors.length}) </Button> </DialogTrigger> ) : null} - <DialogContent> + <DialogContent className="max-w-md"> <DialogHeader> - <DialogTitle>Confirm Vendor Investigation Requst</DialogTitle> + <DialogTitle>Confirm Vendor Investigation Request</DialogTitle> <DialogDescription> - Are you sure you want to request{" "} - <span className="font-medium">{vendors.length}</span> - {vendors.length === 1 ? " vendor" : " vendors"}? - After sent, 벤더실사담당자 will be notified and can manage it. + 선택한 {vendors.length}개 업체에 대한 실사 요청을 확인합니다. + 요청 후 협력업체실사담당자에게 알림이 전송됩니다. </DialogDescription> </DialogHeader> - <DialogFooter className="gap-2 sm:space-x-0"> + + {renderContent()} + + <DialogFooter className="gap-2 sm:space-x-0 mt-4"> <DialogClose asChild> - <Button variant="outline">Cancel</Button> + <Button variant="outline">취소</Button> </DialogClose> <Button aria-label="Request selected vendors" variant="default" onClick={onApprove} - disabled={isApprovePending} + disabled={isApprovePending || isLoading || vendorsWithInvestigations.withoutInvestigations.length === 0} > {isApprovePending && ( <Loader @@ -102,7 +292,7 @@ export function RequestVendorsInvestigateDialog({ aria-hidden="true" /> )} - Request + 요청하기 </Button> </DialogFooter> </DialogContent> @@ -124,26 +314,29 @@ export function RequestVendorsInvestigateDialog({ <DrawerHeader> <DrawerTitle>Confirm Vendor Investigation</DrawerTitle> <DrawerDescription> - Are you sure you want to request{" "} - <span className="font-medium">{vendors.length}</span> - {vendors.length === 1 ? " vendor" : " vendors"}? - After sent, 벤더실사담당자 will be notified and can manage it. + 선택한 {vendors.length}개 업체에 대한 실사 요청을 확인합니다. + 요청 후 협력업체실사담당자에게 알림이 전송됩니다. </DrawerDescription> </DrawerHeader> - <DrawerFooter className="gap-2 sm:space-x-0"> + + <div className="px-4"> + {renderContent()} + </div> + + <DrawerFooter className="gap-2 sm:space-x-0 mt-4"> <DrawerClose asChild> - <Button variant="outline">Cancel</Button> + <Button variant="outline">취소</Button> </DrawerClose> <Button aria-label="Request selected vendors" variant="default" onClick={onApprove} - disabled={isApprovePending} + disabled={isApprovePending || isLoading || vendorsWithInvestigations.withoutInvestigations.length === 0} > {isApprovePending && ( <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> )} - Request + 요청하기 </Button> </DrawerFooter> </DrawerContent> |
