From 4dc27e9495b005b29b4d7a2ad404c8c0644769eb Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 4 Aug 2025 09:41:06 +0000
Subject: (최겸) 실사 컬럼 수정 및 업데이트 변경, 협력업체 PQ 초대 로직 변경
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/vendors/table/request-pq-dialog.tsx | 286 ++++++++++++++++++++++++++++++++
 1 file changed, 286 insertions(+)
 create mode 100644 lib/vendors/table/request-pq-dialog.tsx

(limited to 'lib/vendors/table/request-pq-dialog.tsx')

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>
+  )
+}
-- 
cgit v1.2.3

