diff options
Diffstat (limited to 'lib/vendors/table/request-pq-dialog.tsx')
| -rw-r--r-- | lib/vendors/table/request-pq-dialog.tsx | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx new file mode 100644 index 00000000..6d477d9f --- /dev/null +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -0,0 +1,286 @@ +"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, 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 {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Label } from "@/components/ui/label"
+import { Vendor } from "@/db/schema/vendors"
+import { requestPQVendors } from "../service"
+import { getProjectsWithPQList } from "@/lib/pq/service"
+import type { Project } from "@/lib/pq/service"
+import { useSession } from "next-auth/react"
+import { DatePicker } from "@/components/ui/date-picker"
+
+interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ vendors: Row<Vendor>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+const AGREEMENT_LIST = [
+ "준법서약",
+ "표준하도급계약",
+ "안전보건관리계약",
+ "윤리규범 준수 서약",
+ "동반성장협약",
+ "내국신용장 미개설 합의",
+ "기술자료 제출 기본 동의",
+ "GTC 합의",
+]
+
+export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...props }: RequestPQDialogProps) {
+ const [isApprovePending, startApproveTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+ const { data: session } = useSession()
+
+ const [type, setType] = React.useState<"GENERAL" | "PROJECT" | "NON_INSPECTION" | null>(null)
+ const [dueDate, setDueDate] = React.useState<string | null>(null)
+ const [projects, setProjects] = React.useState<Project[]>([])
+ const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null)
+ const [agreements, setAgreements] = React.useState<Record<string, boolean>>({})
+ const [extraNote, setExtraNote] = React.useState<string>("")
+ const [pqItems, setPqItems] = React.useState<string>("")
+ const [isLoadingProjects, setIsLoadingProjects] = React.useState(false)
+
+ React.useEffect(() => {
+ if (type === "PROJECT") {
+ setIsLoadingProjects(true)
+ getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패"))
+ .finally(() => setIsLoadingProjects(false))
+ }
+ }, [type])
+
+ React.useEffect(() => {
+ if (!props.open) {
+ setType(null)
+ setSelectedProjectId(null)
+ setAgreements({})
+ setDueDate(null)
+ setPqItems("")
+ setExtraNote("")
+ }
+ }, [props.open])
+
+ const onApprove = () => {
+ if (!type) return toast.error("PQ 유형을 선택하세요.")
+ if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.")
+ if (!dueDate) return toast.error("마감일을 선택하세요.")
+ if (!session?.user?.id) return toast.error("인증 실패")
+
+ startApproveTransition(async () => {
+ const { error } = await requestPQVendors({
+ ids: vendors.map((v) => v.id),
+ userId: Number(session.user.id),
+ agreements,
+ dueDate,
+ projectId: type === "PROJECT" ? selectedProjectId : null,
+ type: type || "GENERAL",
+ extraNote,
+ pqItems,
+ })
+
+ 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>선택된 협력업체 ({vendors.length}개)</Label>
+ <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-2">
+ {vendors.map((vendor) => (
+ <div key={vendor.id} className="flex items-center justify-between text-sm">
+ <div className="flex-1">
+ <div className="font-medium">{vendor.vendorName}</div>
+ <div className="text-muted-foreground">
+ {vendor.vendorCode} • {vendor.email || "이메일 없음"}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="type">PQ 종류 선택</Label>
+ <Select onValueChange={(val: "GENERAL" | "PROJECT" | "NON_INSPECTION") => setType(val)} value={type ?? undefined}>
+ <SelectTrigger id="type"><SelectValue placeholder="PQ 종류를 선택하세요" /></SelectTrigger>
+ <SelectContent>
+ <SelectItem value="GENERAL">일반 PQ</SelectItem>
+ <SelectItem value="PROJECT">프로젝트 PQ</SelectItem>
+ <SelectItem value="NON_INSPECTION">미실사 PQ</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ {type === "PROJECT" && (
+ <div className="space-y-2">
+ <Label htmlFor="project">프로젝트 선택</Label>
+ <Select onValueChange={(val) => setSelectedProjectId(Number(val))}>
+ <SelectTrigger id="project">
+ <SelectValue placeholder="프로젝트 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {isLoadingProjects ? (
+ <SelectItem value="loading" disabled>로딩 중...</SelectItem>
+ ) : projects.map((p) => (
+ <SelectItem key={p.id} value={p.id.toString()}>{p.projectCode} - {p.projectName}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+
+ {/* 마감일 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="dueDate">PQ 제출 마감일</Label>
+ <DatePicker
+ date={dueDate ? new Date(dueDate) : undefined}
+ onSelect={(date?: Date) => setDueDate(date ? date.toISOString().slice(0, 10) : "")}
+ placeholder="마감일 선택"
+ />
+ </div>
+
+ {/* PQ 대상품목 */}
+ <div className="space-y-2">
+ <Label htmlFor="pqItems">PQ 대상품목</Label>
+ <textarea
+ id="pqItems"
+ value={pqItems}
+ onChange={(e) => setPqItems(e.target.value)}
+ placeholder="PQ 대상품목을 입력하세요 (선택사항)"
+ className="w-full rounded-md border px-3 py-2 text-sm min-h-20 resize-none"
+ />
+ </div>
+
+ {/* 추가 안내사항 */}
+ <div className="space-y-2">
+ <Label htmlFor="extraNote">추가 안내사항</Label>
+ <textarea
+ id="extraNote"
+ value={extraNote}
+ onChange={(e) => setExtraNote(e.target.value)}
+ placeholder="추가 안내사항을 입력하세요 (선택사항)"
+ className="w-full rounded-md border px-3 py-2 text-sm min-h-20 resize-none"
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label>계약 항목 선택</Label>
+ {AGREEMENT_LIST.map((label) => (
+ <div key={label} className="flex items-center gap-2">
+ <Checkbox
+ id={label}
+ checked={agreements[label] || false}
+ onCheckedChange={(val) =>
+ setAgreements((prev) => ({ ...prev, [label]: Boolean(val) }))
+ }
+ />
+ <Label htmlFor={label}>{label}</Label>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger && (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <SendHorizonal className="size-4" /> PQ 요청 ({vendors.length})
+ </Button>
+ </DialogTrigger>
+ )}
+ <DialogContent className="max-h-[80vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>PQ 요청</DialogTitle>
+ <DialogDescription>
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="flex-1 overflow-y-auto">
+ {dialogContent}
+ </div>
+ <DialogFooter>
+ <DialogClose asChild><Button variant="outline">취소</Button></DialogClose>
+ <Button onClick={onApprove} disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}>
+ {isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}요청하기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger && (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <SendHorizonal className="size-4" /> PQ 요청 ({vendors.length})
+ </Button>
+ </DrawerTrigger>
+ )}
+ <DrawerContent className="max-h-[80vh] flex flex-col">
+ <DrawerHeader>
+ <DrawerTitle>PQ 요청</DrawerTitle>
+ <DrawerDescription>
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <div className="flex-1 overflow-y-auto px-4">
+ {dialogContent}
+ </div>
+ <DrawerFooter>
+ <DrawerClose asChild><Button variant="outline">취소</Button></DrawerClose>
+ <Button onClick={onApprove} disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}>
+ {isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}요청하기
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+}
|
