diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-02 09:54:08 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-02 09:54:08 +0000 |
| commit | dfdfae3018f8499240f48d28ce634f4a5c56e006 (patch) | |
| tree | 4493b172c061fa5bf4e94c083788110eb1507f6d /lib/vendors/table | |
| parent | 21a72eeddc74cf775e2a76e2c569de970bd62a7f (diff) | |
벤더 코멘트 처리
Diffstat (limited to 'lib/vendors/table')
| -rw-r--r-- | lib/vendors/table/attachmentButton.tsx | 45 | ||||
| -rw-r--r-- | lib/vendors/table/request-additional-Info-dialog.tsx | 152 | ||||
| -rw-r--r-- | lib/vendors/table/request-project-pq-dialog.tsx | 242 | ||||
| -rw-r--r-- | lib/vendors/table/request-vendor-investigate-dialog.tsx | 152 | ||||
| -rw-r--r-- | lib/vendors/table/send-vendor-dialog.tsx | 4 | ||||
| -rw-r--r-- | lib/vendors/table/vendors-table-columns.tsx | 158 | ||||
| -rw-r--r-- | lib/vendors/table/vendors-table-toolbar-actions.tsx | 86 | ||||
| -rw-r--r-- | lib/vendors/table/vendors-table.tsx | 5 |
8 files changed, 733 insertions, 111 deletions
diff --git a/lib/vendors/table/attachmentButton.tsx b/lib/vendors/table/attachmentButton.tsx index a82f59e1..3ffa9c5f 100644 --- a/lib/vendors/table/attachmentButton.tsx +++ b/lib/vendors/table/attachmentButton.tsx @@ -16,25 +16,25 @@ interface AttachmentsButtonProps { export function AttachmentsButton({ vendorId, hasAttachments, attachmentsList = [] }: AttachmentsButtonProps) { if (!hasAttachments) return null; - + const handleDownload = async () => { try { toast.loading('첨부파일을 준비하는 중...'); - + // 서버 액션 호출 const result = await downloadVendorAttachments(vendorId); - + // 로딩 토스트 닫기 toast.dismiss(); - + if (!result || !result.url) { toast.error('다운로드 준비 중 오류가 발생했습니다.'); return; } - + // 파일 다운로드 트리거 toast.success('첨부파일 다운로드가 시작되었습니다.'); - + // 다운로드 링크 열기 const a = document.createElement('a'); a.href = result.url; @@ -43,27 +43,34 @@ export function AttachmentsButton({ vendorId, hasAttachments, attachmentsList = document.body.appendChild(a); a.click(); document.body.removeChild(a); - + } catch (error) { toast.dismiss(); toast.error('첨부파일 다운로드에 실패했습니다.'); console.error('첨부파일 다운로드 오류:', error); } }; - + return ( - <Button - variant="ghost" - size="icon" - onClick={handleDownload} - title={`${attachmentsList.length}개 파일 다운로드`} - > - <PaperclipIcon className="h-4 w-4" /> - {attachmentsList.length > 1 && ( - <Badge variant="outline" className="ml-1 h-5 min-w-5 px-1"> + <> + {attachmentsList && attachmentsList.length > 0 && + <Button + variant="ghost" + size="icon" + onClick={handleDownload} + title={`${attachmentsList.length}개 파일 다운로드`} + > + <PaperclipIcon className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> + {/* {attachmentsList.length > 1 && ( + <Badge + variant="secondary" + className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.425rem] leading-none flex items-center justify-center" + > {attachmentsList.length} </Badge> - )} - </Button> + )} */} + </Button> + } + </> ); } diff --git a/lib/vendors/table/request-additional-Info-dialog.tsx b/lib/vendors/table/request-additional-Info-dialog.tsx new file mode 100644 index 00000000..872162dd --- /dev/null +++ b/lib/vendors/table/request-additional-Info-dialog.tsx @@ -0,0 +1,152 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Send } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Vendor } from "@/db/schema/vendors" +import { requestInfo } from "../service" + +interface RequestInfoDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + vendors: Row<Vendor>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function RequestInfoDialog({ + vendors, + showTrigger = true, + onSuccess, + ...props +}: RequestInfoDialogProps) { + const [isRequestPending, startRequestTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onApprove() { + startRequestTransition(async () => { + const { error, success } = await requestInfo({ + ids: vendors.map((vendor) => vendor.id), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success("추가 정보 요청이 성공적으로 벤더에게 발송되었습니다.") + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Send className="size-4" aria-hidden="true" /> + 추가 정보 요청 ({vendors.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>벤더 추가 정보 요청 확인</DialogTitle> + <DialogDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개의 벤더" : "개의 벤더들"}에게 추가 정보를 요청하시겠습니까? + <br /><br /> + 요청시 벤더에게 이메일이 발송되며, 벤더는 별도 페이지에서 신용 평가 및 현금 흐름 정보와 같은 + 추가 정보를 입력하게 됩니다. + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">취소</Button> + </DialogClose> + <Button + aria-label="Send request to selected vendors" + variant="default" + onClick={onApprove} + disabled={isRequestPending} + > + {isRequestPending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 요청 발송 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Send className="size-4" aria-hidden="true" /> + 추가 정보 요청 ({vendors.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>벤더 추가 정보 요청 확인</DrawerTitle> + <DrawerDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개의 벤더" : "개의 벤더들"}에게 추가 정보를 요청하시겠습니까? + <br /><br /> + 요청시 벤더에게 이메일이 발송되며, 벤더는 별도 페이지에서 신용 평가 및 현금 흐름 정보와 같은 + 추가 정보를 입력하게 됩니다. + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">취소</Button> + </DrawerClose> + <Button + aria-label="Send request to selected vendors" + variant="default" + onClick={onApprove} + disabled={isRequestPending} + > + {isRequestPending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + 요청 발송 + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/vendors/table/request-project-pq-dialog.tsx b/lib/vendors/table/request-project-pq-dialog.tsx new file mode 100644 index 00000000..c590d7ec --- /dev/null +++ b/lib/vendors/table/request-project-pq-dialog.tsx @@ -0,0 +1,242 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, ChevronDown, BuildingIcon } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Vendor } from "@/db/schema/vendors" +import { requestPQVendors } from "../service" +import { getProjects, type Project } from "@/lib/rfqs/service" + +interface RequestProjectPQDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + vendors: Row<Vendor>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function RequestProjectPQDialog({ + vendors, + showTrigger = true, + onSuccess, + ...props +}: RequestProjectPQDialogProps) { + const [isApprovePending, startApproveTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + const [projects, setProjects] = React.useState<Project[]>([]) + const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null) + const [isLoadingProjects, setIsLoadingProjects] = React.useState(false) + + // 프로젝트 목록 로드 + React.useEffect(() => { + async function loadProjects() { + setIsLoadingProjects(true) + try { + const projectsList = await getProjects() + setProjects(projectsList) + } catch (error) { + console.error("프로젝트 목록 로드 오류:", error) + toast.error("프로젝트 목록을 불러오는 중 오류가 발생했습니다.") + } finally { + setIsLoadingProjects(false) + } + } + + loadProjects() + }, []) + + // 다이얼로그가 닫힐 때 선택된 프로젝트 초기화 + React.useEffect(() => { + if (!props.open) { + setSelectedProjectId(null) + } + }, [props.open]) + + // 프로젝트 선택 처리 + const handleProjectChange = (value: string) => { + setSelectedProjectId(Number(value)) + } + + function onApprove() { + if (!selectedProjectId) { + toast.error("프로젝트를 선택해주세요.") + return + } + + startApproveTransition(async () => { + const { error } = await requestPQVendors({ + ids: vendors.map((vendor) => vendor.id), + projectId: selectedProjectId, + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + + toast.success(`벤더에게 프로젝트 PQ가 성공적으로 요청되었습니다.`) + onSuccess?.() + }) + } + + const dialogContent = ( + <> + <div className="space-y-4 py-2"> + <div className="space-y-2"> + <Label htmlFor="project-selection">프로젝트 선택</Label> + <Select + onValueChange={handleProjectChange} + disabled={isLoadingProjects || isApprovePending} + > + <SelectTrigger id="project-selection" className="w-full"> + <SelectValue placeholder="프로젝트를 선택하세요" /> + </SelectTrigger> + <SelectContent> + {isLoadingProjects ? ( + <SelectItem value="loading" disabled>프로젝트 로딩 중...</SelectItem> + ) : projects.length === 0 ? ( + <SelectItem value="empty" disabled>등록된 프로젝트가 없습니다</SelectItem> + ) : ( + projects.map((project) => ( + <SelectItem key={project.id} value={project.id.toString()}> + {project.projectCode} - {project.projectName} + </SelectItem> + )) + )} + </SelectContent> + </Select> + </div> + </div> + </> + ) + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <BuildingIcon className="size-4" aria-hidden="true" /> + 프로젝트 PQ 요청 ({vendors.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>프로젝트 PQ 요청 확인</DialogTitle> + <DialogDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개의 벤더" : "개의 벤더들"}에게 프로젝트 PQ 제출을 요청하시겠습니까? + 요청을 보내면 벤더에게 알림이 발송되고 프로젝트 PQ 정보를 입력할 수 있게 됩니다. + </DialogDescription> + </DialogHeader> + + {dialogContent} + + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">취소</Button> + </DialogClose> + <Button + aria-label="선택한 벤더에게 요청하기" + variant="default" + onClick={onApprove} + disabled={isApprovePending || !selectedProjectId} + > + {isApprovePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 요청하기 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <BuildingIcon className="size-4" aria-hidden="true" /> + 프로젝트 PQ 요청 ({vendors.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>프로젝트 PQ 요청 확인</DrawerTitle> + <DrawerDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개의 벤더" : "개의 벤더들"}에게 프로젝트 PQ 제출을 요청하시겠습니까? + 요청을 보내면 벤더에게 알림이 발송되고 프로젝트 PQ 정보를 입력할 수 있게 됩니다. + </DrawerDescription> + </DrawerHeader> + + <div className="px-4"> + {dialogContent} + </div> + + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">취소</Button> + </DrawerClose> + <Button + aria-label="선택한 벤더에게 요청하기" + variant="default" + onClick={onApprove} + disabled={isApprovePending || !selectedProjectId} + > + {isApprovePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + 요청하기 + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/vendors/table/request-vendor-investigate-dialog.tsx b/lib/vendors/table/request-vendor-investigate-dialog.tsx new file mode 100644 index 00000000..0309ee4a --- /dev/null +++ b/lib/vendors/table/request-vendor-investigate-dialog.tsx @@ -0,0 +1,152 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Check, SendHorizonal } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Vendor } from "@/db/schema/vendors" +import { requestInvestigateVendors } from "@/lib/vendor-investigation/service" + +interface ApprovalVendorDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + vendors: Row<Vendor>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function RequestVendorsInvestigateDialog({ + vendors, + showTrigger = true, + onSuccess, + ...props +}: ApprovalVendorDialogProps) { + + console.log(vendors) + const [isApprovePending, startApproveTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onApprove() { + startApproveTransition(async () => { + const { error } = await requestInvestigateVendors({ + ids: vendors.map((vendor) => vendor.id), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success("Vendor Investigation successfully sent to 벤더실사담당자") + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <SendHorizonal className="size-4" aria-hidden="true" /> + Vendor Investigation Request ({vendors.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>Confirm Vendor Investigation Requst</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. + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">Cancel</Button> + </DialogClose> + <Button + aria-label="Request selected vendors" + variant="default" + onClick={onApprove} + disabled={isApprovePending} + > + {isApprovePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + Request + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Check className="size-4" aria-hidden="true" /> + Investigation Request ({vendors.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <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. + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">Cancel</Button> + </DrawerClose> + <Button + aria-label="Request selected vendors" + variant="default" + onClick={onApprove} + disabled={isApprovePending} + > + {isApprovePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + Request + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/vendors/table/send-vendor-dialog.tsx b/lib/vendors/table/send-vendor-dialog.tsx index a34abb77..1f93bd7f 100644 --- a/lib/vendors/table/send-vendor-dialog.tsx +++ b/lib/vendors/table/send-vendor-dialog.tsx @@ -28,7 +28,7 @@ import { DrawerTrigger, } from "@/components/ui/drawer" import { Vendor } from "@/db/schema/vendors" -import { requestPQVendors, sendVendors } from "../service" +import { sendVendors } from "../service" interface ApprovalVendorDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { @@ -58,7 +58,7 @@ export function SendVendorsDialog({ } props.onOpenChange?.(false) - toast.success("PQ successfully sent to vendors") + toast.success("Vendor Information successfully sent to MDG") onSuccess?.() }) } diff --git a/lib/vendors/table/vendors-table-columns.tsx b/lib/vendors/table/vendors-table-columns.tsx index c503e369..77750c47 100644 --- a/lib/vendors/table/vendors-table-columns.tsx +++ b/lib/vendors/table/vendors-table-columns.tsx @@ -79,82 +79,96 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef // ---------------------------------------------------------------- // 2) actions 컬럼 (Dropdown 메뉴) // ---------------------------------------------------------------- - const actionsColumn: ColumnDef<Vendor> = { - id: "actions", - enableHiding: false, - cell: function Cell({ row }) { - const [isUpdatePending, startUpdateTransition] = React.useTransition() +// ---------------------------------------------------------------- +// 2) actions 컬럼 (Dropdown 메뉴) +// ---------------------------------------------------------------- +const actionsColumn: ColumnDef<Vendor> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const isApproved = row.original.status === "APPROVED"; - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - aria-label="Open menu" - variant="ghost" - className="flex size-8 p-0 data-[state=open]:bg-muted" - > - <Ellipsis className="size-4" aria-hidden="true" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-40"> - <DropdownMenuItem - onSelect={() => setRowAction({ row, type: "update" })} - > - Edit - </DropdownMenuItem> - <DropdownMenuItem - onSelect={() => { - // 1) 만약 rowAction을 열고 싶다면 - // setRowAction({ row, type: "update" }) + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-56"> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "update" })} + > + Edit + </DropdownMenuItem> + <DropdownMenuItem + onSelect={() => { + // 1) 만약 rowAction을 열고 싶다면 + // setRowAction({ row, type: "update" }) - // 2) 자세히 보기 페이지로 클라이언트 라우팅 - router.push(`/evcp/vendors/${row.original.id}/info`); - }} + // 2) 자세히 보기 페이지로 클라이언트 라우팅 + router.push(`/evcp/vendors/${row.original.id}/info`); + }} + > + Details + </DropdownMenuItem> + + {/* APPROVED 상태일 때만 추가 정보 기입 메뉴 표시 */} + {isApproved && ( + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "requestInfo" })} + className="text-blue-600 font-medium" > - Details + 추가 정보 기입 </DropdownMenuItem> - <Separator /> - <DropdownMenuSub> - <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger> - <DropdownMenuSubContent> - <DropdownMenuRadioGroup - value={row.original.status} - onValueChange={(value) => { - startUpdateTransition(() => { - toast.promise( - modifyVendor({ - id: String(row.original.id), - status: value as Vendor["status"], - }), - { - loading: "Updating...", - success: "Label updated", - error: (err) => getErrorMessage(err), - } - ) - }) - }} - > - {vendors.status.enumValues.map((status) => ( - <DropdownMenuRadioItem - key={status} - value={status} - className="capitalize" - disabled={isUpdatePending} - > - {status} - </DropdownMenuRadioItem> - ))} - </DropdownMenuRadioGroup> - </DropdownMenuSubContent> - </DropdownMenuSub> - - </DropdownMenuContent> - </DropdownMenu> - ) - }, - size: 40, - } + )} + + <Separator /> + <DropdownMenuSub> + <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger> + <DropdownMenuSubContent> + <DropdownMenuRadioGroup + value={row.original.status} + onValueChange={(value) => { + startUpdateTransition(() => { + toast.promise( + modifyVendor({ + id: String(row.original.id), + status: value as Vendor["status"], + }), + { + loading: "Updating...", + success: "Label updated", + error: (err) => getErrorMessage(err), + } + ) + }) + }} + > + {vendors.status.enumValues.map((status) => ( + <DropdownMenuRadioItem + key={status} + value={status} + className="capitalize" + disabled={isUpdatePending} + > + {status} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> + </DropdownMenuSubContent> + </DropdownMenuSub> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, +} // ---------------------------------------------------------------- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 diff --git a/lib/vendors/table/vendors-table-toolbar-actions.tsx b/lib/vendors/table/vendors-table-toolbar-actions.tsx index c0605191..3cb2c552 100644 --- a/lib/vendors/table/vendors-table-toolbar-actions.tsx +++ b/lib/vendors/table/vendors-table-toolbar-actions.tsx @@ -2,15 +2,24 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { Download, Upload, Check } from "lucide-react" +import { Download, Upload, Check, BuildingIcon } from "lucide-react" import { toast } from "sonner" import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { Vendor } from "@/db/schema/vendors" import { ApproveVendorsDialog } from "./approve-vendor-dialog" import { RequestPQVendorsDialog } from "./request-vendor-pg-dialog" +import { RequestProjectPQDialog } from "./request-project-pq-dialog" import { SendVendorsDialog } from "./send-vendor-dialog" +import { RequestVendorsInvestigateDialog } from "./request-vendor-investigate-dialog" +import { RequestInfoDialog } from "./request-additional-Info-dialog" interface VendorsTableToolbarActionsProps { table: Table<Vendor> @@ -19,7 +28,7 @@ interface VendorsTableToolbarActionsProps { export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActionsProps) { // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 const fileInputRef = React.useRef<HTMLInputElement>(null) - + // 선택된 벤더 중 PENDING_REVIEW 상태인 벤더만 필터링 const pendingReviewVendors = React.useMemo(() => { return table @@ -28,9 +37,8 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions .map(row => row.original) .filter(vendor => vendor.status === "PENDING_REVIEW"); }, [table.getFilteredSelectedRowModel().rows]); - - - // 선택된 벤더 중 PENDING_REVIEW 상태인 벤더만 필터링 + + // 선택된 벤더 중 IN_REVIEW 상태인 벤더만 필터링 const inReviewVendors = React.useMemo(() => { return table .getFilteredSelectedRowModel() @@ -38,7 +46,7 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions .map(row => row.original) .filter(vendor => vendor.status === "IN_REVIEW"); }, [table.getFilteredSelectedRowModel().rows]); - + const approvedVendors = React.useMemo(() => { return table .getFilteredSelectedRowModel() @@ -46,14 +54,36 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions .map(row => row.original) .filter(vendor => vendor.status === "APPROVED"); }, [table.getFilteredSelectedRowModel().rows]); - - - + + const sendVendors = React.useMemo(() => { + return table + .getFilteredSelectedRowModel() + .rows + .map(row => row.original) + .filter(vendor => vendor.status === "READY_TO_SEND"); + }, [table.getFilteredSelectedRowModel().rows]); + + const pqApprovedVendors = React.useMemo(() => { + return table + .getFilteredSelectedRowModel() + .rows + .map(row => row.original) + .filter(vendor => vendor.status === "PQ_APPROVED"); + }, [table.getFilteredSelectedRowModel().rows]); + + // 프로젝트 PQ를 보낼 수 있는 벤더 상태 필터링 + const projectPQEligibleVendors = React.useMemo(() => { + return table + .getFilteredSelectedRowModel() + .rows + .map(row => row.original) + .filter(vendor => + ["PENDING_REVIEW", "IN_REVIEW", "IN_PQ", "PQ_APPROVED", "APPROVED", "READY_TO_SEND", "ACTIVE"].includes(vendor.status) + ); + }, [table.getFilteredSelectedRowModel().rows]); + return ( <div className="flex items-center gap-2"> - - - {/* 승인 다이얼로그: PENDING_REVIEW 상태인 벤더가 있을 때만 표시 */} {pendingReviewVendors.length > 0 && ( <ApproveVendorsDialog @@ -61,22 +91,44 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions onSuccess={() => table.toggleAllRowsSelected(false)} /> )} - + + {/* 일반 PQ 요청: IN_REVIEW 상태인 벤더가 있을 때만 표시 */} {inReviewVendors.length > 0 && ( <RequestPQVendorsDialog vendors={inReviewVendors} onSuccess={() => table.toggleAllRowsSelected(false)} /> )} - + + {/* 프로젝트 PQ 요청: 적격 상태의 벤더가 있을 때만 표시 */} + {projectPQEligibleVendors.length > 0 && ( + <RequestProjectPQDialog + vendors={projectPQEligibleVendors} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + )} + {approvedVendors.length > 0 && ( - <SendVendorsDialog + <RequestInfoDialog vendors={approvedVendors} onSuccess={() => table.toggleAllRowsSelected(false)} /> )} - - + + {sendVendors.length > 0 && ( + <RequestInfoDialog + vendors={sendVendors} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + )} + + {pqApprovedVendors.length > 0 && ( + <RequestVendorsInvestigateDialog + vendors={pqApprovedVendors} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + )} + {/** 4) Export 버튼 */} <Button variant="outline" diff --git a/lib/vendors/table/vendors-table.tsx b/lib/vendors/table/vendors-table.tsx index c04d57a9..36fd45bd 100644 --- a/lib/vendors/table/vendors-table.tsx +++ b/lib/vendors/table/vendors-table.tsx @@ -20,6 +20,7 @@ import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions" import { VendorsTableFloatingBar } from "./vendors-table-floating-bar" import { UpdateTaskSheet } from "@/lib/tasks/table/update-task-sheet" import { UpdateVendorSheet } from "./update-vendor-sheet" +import { getVendorStatusIcon } from "@/lib/vendors/utils" interface VendorsTableProps { promises: Promise< @@ -72,9 +73,11 @@ export function VendorsTable({ promises }: VendorsTableProps) { label: "Status", type: "multi-select", options: vendors.status.enumValues.map((status) => ({ - label: toSentenceCase(status), + label: (status), value: status, count: statusCounts[status], + icon: getVendorStatusIcon(status), + })), }, { id: "createdAt", label: "Created at", type: "date" }, |
