diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx | 81 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq-criteria/[pqListId]/page.tsx | 68 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq-criteria/page.tsx | 132 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq_new/[vendorId]/[submissionId]/page.tsx | 430 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq_new/page.tsx | 196 | ||||
| -rw-r--r-- | app/[lng]/partners/pq/page.tsx | 9 | ||||
| -rw-r--r-- | app/[lng]/partners/pq_new/[id]/page.tsx | 3 | ||||
| -rw-r--r-- | app/[lng]/partners/pq_new/page.tsx | 64 | ||||
| -rw-r--r-- | app/[lng]/partners/site-visit/page.tsx | 30 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx | 81 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx | 68 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq-criteria/page.tsx | 129 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx | 430 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq_new/page.tsx | 193 |
14 files changed, 967 insertions, 947 deletions
diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx deleted file mode 100644 index 55b1e9df..00000000 --- a/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" -import { notFound } from "next/navigation" - -interface ProjectPageProps { - params: { id: string } - searchParams: Promise<SearchParams> -} - -export default async function ProjectPage(props: ProjectPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const projectId = parseInt(id, 10) - - // 유효하지 않은 projectId 확인 - if (isNaN(projectId)) { - notFound() - } - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - const validFilters = getValidFilters(search.filters) - - // 프로젝트별 PQ 데이터 가져오기 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, projectId, false) - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Pre-Qualification Check Sheet - </h2> - <p className="text-muted-foreground"> - 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. - </p> - </div> - <ProjectSelectorWrapper selectedProjectId={projectId} /> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <PqsTable promises={promises} currentProjectId={projectId}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/[pqListId]/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/[pqListId]/page.tsx new file mode 100644 index 00000000..15cb3bf3 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/pq-criteria/[pqListId]/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { searchParamsCache } from "@/lib/pq/validations"
+import { getPQsByListId } from "@/lib/pq/service"
+import { PqsTable } from "@/lib/pq/pq-criteria/pq-table"
+import { notFound } from "next/navigation"
+
+interface PQDetailPageProps {
+ params: Promise<{ pqListId: string }>
+ searchParams: Promise<SearchParams>
+}
+
+export default async function PQDetailPage(props: PQDetailPageProps) {
+ const params = await props.params
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+
+ const pqListId = parseInt(params.pqListId)
+ if (isNaN(pqListId)) {
+ notFound()
+ }
+
+ // filters가 없는 경우를 처리
+ const validFilters = getValidFilters(search.filters)
+
+ // PQ 항목들 가져오기
+ const promises = Promise.all([
+ getPQsByListId(pqListId, {
+ ...search,
+ filters: validFilters,
+ })
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 항목 관리
+ </h2>
+ {/* <p className="text-muted-foreground">
+ 선택한 PQ 목록의 세부 항목들을 관리할 수 있습니다.
+ </p> */}
+ </div>
+ </div>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={8}
+ searchableColumnCount={1}
+ filterableColumnCount={3}
+ cellWidths={["10rem", "15rem", "20rem", "15rem", "10rem", "10rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PqsTable
+ promises={promises}
+ pqListId={pqListId}
+ />
+ </React.Suspense>
+ </Shell>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx index 20d16085..1a337cc9 100644 --- a/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx @@ -1,73 +1,61 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - - const validFilters = getValidFilters(search.filters) - - // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, null, true) - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - PQ 항목 관리 - </h2> - <InformationButton pagePath="evcp/pq-criteria" /> - </div> - {/* <p className="text-muted-foreground"> - 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. - </p> */} - </div> - <ProjectSelectorWrapper /> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <PqsTable promises={promises}/> - </React.Suspense> - </Shell> - ) +import * as React from "react"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { searchParamsCache } from "@/lib/pq/validations"
+import { getPQLists } from "@/lib/pq/service"
+import { PqListsTable } from "@/lib/pq/table/pq-lists-table"
+import { getProjects } from "@/lib/pq/service"
+
+interface ProjectPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function ProjectPage(props: ProjectPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+
+ // filters가 없는 경우를 처리
+ const validFilters = getValidFilters(search.filters)
+
+ // // 프로젝트별 PQ 데이터 가져오기
+ const promises = Promise.all([
+ getPQLists({
+ ...search,
+ filters: validFilters,
+ }),
+ getProjects()
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 리스트 관리
+ </h2>
+ {/* <p className="text-muted-foreground">
+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다.
+ </p> */}
+ </div>
+ </div>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PqListsTable
+ promises={promises}
+ />
+ </React.Suspense>
+ </Shell>
+ )
}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/evcp/(evcp)/pq_new/[vendorId]/[submissionId]/page.tsx index 28ce3128..bd8d2347 100644 --- a/app/[lng]/evcp/(evcp)/pq_new/[vendorId]/[submissionId]/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -1,215 +1,217 @@ -import * as React from "react" -import { Metadata } from "next" -import Link from "next/link" -import { notFound } from "next/navigation" -import { ArrowLeft } from "lucide-react" -import { Shell } from "@/components/shell" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Separator } from "@/components/ui/separator" -import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" -import { unstable_noStore as noStore } from 'next/cache' -import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" - -export const metadata: Metadata = { - title: "PQ 검토", - description: "협력업체의 Pre-Qualification 답변을 검토합니다.", -} - -// 페이지가 기본적으로 동적임을 나타냄 -export const dynamic = "force-dynamic" - -interface PQReviewPageProps { - params: Promise<{ - vendorId: string; - submissionId: string; - }> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - // 캐시 비활성화 - noStore() - - const params = await props.params - const vendorId = parseInt(params.vendorId, 10) - const submissionId = parseInt(params.submissionId, 10) - - try { - // PQ Submission 정보 조회 - const pqSubmission = await getPQById(submissionId, vendorId) - - // PQ 데이터 조회 (질문과 답변) - const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) - - // 프로젝트 정보 (프로젝트 PQ인 경우) - const projectInfo = pqSubmission.projectId ? { - id: pqSubmission.projectId, - projectCode: pqSubmission.projectCode || '', - projectName: pqSubmission.projectName || '', - status: pqSubmission.status, - submittedAt: pqSubmission.submittedAt, - } : null - - // PQ 유형 및 상태 레이블 - const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" - const statusLabel = getStatusLabel(pqSubmission.status) - const statusVariant = getStatusVariant(pqSubmission.status) - - // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) - const canReview = pqSubmission.status === "SUBMITTED" - - return ( - <Shell className="gap-6 max-w-5xl"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-4"> - <Button variant="outline" size="sm" asChild> - <Link href="/evcp/pq_new"> - <ArrowLeft className="w-4 h-4 mr-2" /> - 목록으로 - </Link> - </Button> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {pqSubmission.vendorName} - {typeLabel} - </h2> - <div className="flex items-center gap-2 mt-1"> - <Badge variant={statusVariant}>{statusLabel}</Badge> - {projectInfo && ( - <span className="text-muted-foreground"> - {projectInfo.projectName} ({projectInfo.projectCode}) - </span> - )} - </div> - </div> - </div> - </div> - - {/* 상태별 알림 */} - {pqSubmission.status === "SUBMITTED" && ( - <Alert> - <AlertTitle>제출 완료</AlertTitle> - <AlertDescription> - 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. - </AlertDescription> - </Alert> - )} - - {pqSubmission.status === "APPROVED" && ( - <Alert variant="success"> - <AlertTitle>승인됨</AlertTitle> - <AlertDescription> - {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. - </AlertDescription> - </Alert> - )} - - {pqSubmission.status === "REJECTED" && ( - <Alert variant="destructive"> - <AlertTitle>거부됨</AlertTitle> - <AlertDescription> - {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. - {pqSubmission.rejectReason && ( - <div className="mt-2"> - <strong>사유:</strong> {pqSubmission.rejectReason} - </div> - )} - </AlertDescription> - </Alert> - )} - - <Separator /> - - {/* PQ 검토 컴포넌트 */} - <Tabs defaultValue="review" className="w-full"> - <TabsList> - <TabsTrigger value="review">PQ 검토</TabsTrigger> - <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger> - </TabsList> - - <TabsContent value="review" className="mt-4"> - <PQReviewWrapper - pqData={pqData} - vendorId={vendorId} - pqSubmission={pqSubmission} - canReview={canReview} - /> - </TabsContent> - - <TabsContent value="vendor-info" className="mt-4"> - <div className="rounded-md border p-4"> - <h3 className="text-lg font-medium mb-4">협력업체 정보</h3> - <div className="grid grid-cols-2 gap-4"> - <div> - <p className="text-sm font-medium text-muted-foreground">업체명</p> - <p>{pqSubmission.vendorName}</p> - </div> - <div> - <p className="text-sm font-medium text-muted-foreground">업체 코드</p> - <p>{pqSubmission.vendorCode}</p> - </div> - <div> - <p className="text-sm font-medium text-muted-foreground">상태</p> - <p>{pqSubmission.vendorStatus}</p> - </div> - {/* 필요시 추가 정보 표시 */} - </div> - </div> - </TabsContent> - </Tabs> - </Shell> - ) - } catch (error) { - console.error("Error loading PQ:", error) - notFound() - } -} - -// 상태 레이블 함수 -function getStatusLabel(status: string): string { - switch (status) { - case "REQUESTED": - return "요청됨"; - case "IN_PROGRESS": - return "진행 중"; - case "SUBMITTED": - return "제출됨"; - case "APPROVED": - return "승인됨"; - case "REJECTED": - return "거부됨"; - default: - return status; - } -} - -// 상태별 Badge 스타일 -function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { - switch (status) { - case "REQUESTED": - return "outline"; - case "IN_PROGRESS": - return "secondary"; - case "SUBMITTED": - return "default"; - case "APPROVED": - return "success"; - case "REJECTED": - return "destructive"; - default: - return "outline"; - } -} - -// 날짜 형식화 함수 -function formatDate(date: Date | null) { - if (!date) return "날짜 없음"; - return new Date(date).toLocaleDateString("ko-KR", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit" - }); +import * as React from "react"
+import { Metadata } from "next"
+import Link from "next/link"
+import { notFound } from "next/navigation"
+import { ArrowLeft } from "lucide-react"
+import { Shell } from "@/components/shell"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Separator } from "@/components/ui/separator"
+import { getPQById, getPQDataByVendorId } from "@/lib/pq/service"
+import { unstable_noStore as noStore } from 'next/cache'
+import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper"
+
+export const metadata: Metadata = {
+ title: "PQ 검토",
+ description: "협력업체의 Pre-Qualification 답변을 검토합니다.",
+}
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic"
+
+interface PQReviewPageProps {
+ params: Promise<{
+ vendorId: string;
+ submissionId: string;
+ }>
+}
+
+export default async function PQReviewPage(props: PQReviewPageProps) {
+ // 캐시 비활성화
+ noStore()
+
+ const params = await props.params
+ const vendorId = parseInt(params.vendorId, 10)
+ const submissionId = parseInt(params.submissionId, 10)
+
+ try {
+ // PQ Submission 정보 조회
+ const pqSubmission = await getPQById(submissionId, vendorId)
+
+ // PQ 데이터 조회 (질문과 답변)
+ const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined)
+
+ // 프로젝트 정보 (프로젝트 PQ인 경우)
+ const projectInfo = pqSubmission.projectId ? {
+ id: pqSubmission.projectId,
+ projectCode: pqSubmission.projectCode || '',
+ projectName: pqSubmission.projectName || '',
+ status: pqSubmission.status,
+ submittedAt: pqSubmission.submittedAt,
+ } : null
+
+ // PQ 유형 및 상태 레이블
+ const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" :
+ pqSubmission.type === "PROJECT" ? "프로젝트 PQ" :
+ pqSubmission.type === "NON_INSPECTION" ? "미실사 PQ" : "일반 PQ"
+ const statusLabel = getStatusLabel(pqSubmission.status)
+ const statusVariant = getStatusVariant(pqSubmission.status)
+
+ // 수정 가능 여부 (SUBMITTED 상태일 때만 가능)
+ const canReview = pqSubmission.status === "SUBMITTED"
+
+ return (
+ <Shell className="gap-6 max-w-5xl">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Button variant="outline" size="sm" asChild>
+ <Link href="/evcp/pq_new">
+ <ArrowLeft className="w-4 h-4 mr-2" />
+ 목록으로
+ </Link>
+ </Button>
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {pqSubmission.vendorName} - {typeLabel}
+ </h2>
+ <div className="flex items-center gap-2 mt-1">
+ <Badge variant={statusVariant}>{statusLabel}</Badge>
+ {projectInfo && (
+ <span className="text-muted-foreground">
+ {projectInfo.projectName} ({projectInfo.projectCode})
+ </span>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 상태별 알림 */}
+ {pqSubmission.status === "SUBMITTED" && (
+ <Alert>
+ <AlertTitle>제출 완료</AlertTitle>
+ <AlertDescription>
+ 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {pqSubmission.status === "APPROVED" && (
+ <Alert variant="success">
+ <AlertTitle>승인됨</AlertTitle>
+ <AlertDescription>
+ {formatDate(pqSubmission.approvedAt)}에 승인되었습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {pqSubmission.status === "REJECTED" && (
+ <Alert variant="destructive">
+ <AlertTitle>거부됨</AlertTitle>
+ <AlertDescription>
+ {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다.
+ {pqSubmission.rejectReason && (
+ <div className="mt-2">
+ <strong>사유:</strong> {pqSubmission.rejectReason}
+ </div>
+ )}
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <Separator />
+
+ {/* PQ 검토 컴포넌트 */}
+ <Tabs defaultValue="review" className="w-full">
+ <TabsList>
+ <TabsTrigger value="review">PQ 검토</TabsTrigger>
+ <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="review" className="mt-4">
+ <PQReviewWrapper
+ pqData={pqData}
+ vendorId={vendorId}
+ pqSubmission={pqSubmission}
+ canReview={canReview}
+ />
+ </TabsContent>
+
+ <TabsContent value="vendor-info" className="mt-4">
+ <div className="rounded-md border p-4">
+ <h3 className="text-lg font-medium mb-4">협력업체 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">업체명</p>
+ <p>{pqSubmission.vendorName}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">업체 코드</p>
+ <p>{pqSubmission.vendorCode}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">상태</p>
+ <p>{pqSubmission.vendorStatus}</p>
+ </div>
+ {/* 필요시 추가 정보 표시 */}
+ </div>
+ </div>
+ </TabsContent>
+ </Tabs>
+ </Shell>
+ )
+ } catch (error) {
+ console.error("Error loading PQ:", error)
+ notFound()
+ }
+}
+
+// 상태 레이블 함수
+function getStatusLabel(status: string): string {
+ switch (status) {
+ case "REQUESTED":
+ return "요청됨";
+ case "IN_PROGRESS":
+ return "진행 중";
+ case "SUBMITTED":
+ return "제출됨";
+ case "APPROVED":
+ return "승인됨";
+ case "REJECTED":
+ return "거부됨";
+ default:
+ return status;
+ }
+}
+
+// 상태별 Badge 스타일
+function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" {
+ switch (status) {
+ case "REQUESTED":
+ return "outline";
+ case "IN_PROGRESS":
+ return "secondary";
+ case "SUBMITTED":
+ return "default";
+ case "APPROVED":
+ return "success";
+ case "REJECTED":
+ return "destructive";
+ default:
+ return "outline";
+ }
+}
+
+// 날짜 형식화 함수
+function formatDate(date: Date | null) {
+ if (!date) return "날짜 없음";
+ return new Date(date).toLocaleDateString("ko-KR", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq_new/page.tsx b/app/[lng]/evcp/(evcp)/pq_new/page.tsx index f2dddce5..0e6d3196 100644 --- a/app/[lng]/evcp/(evcp)/pq_new/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq_new/page.tsx @@ -1,99 +1,99 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { searchParamsPQReviewCache } from "@/lib/pq/validations" -import { getPQSubmissions } from "@/lib/pq/service" -import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "협력업체 PQ/실사 현황", - description: "", -} - -interface PQReviewPageProps { - searchParams: Promise<SearchParams> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - const searchParams = await props.searchParams - const search = searchParamsPQReviewCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 디버깅 로그 추가 - console.log("=== PQ Page Debug ==="); - console.log("Raw searchParams:", searchParams); - console.log("Raw basicFilters param:", searchParams.basicFilters); - console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); - console.log("Parsed search:", search); - console.log("search.filters:", search.filters); - console.log("search.basicFilters:", search.basicFilters); - console.log("search.pqBasicFilters:", search.pqBasicFilters); - console.log("validFilters:", validFilters); - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { - // 하위 호환성을 위해 기존 이름도 지원 - basicFilters = search.pqBasicFilters - console.log("Using search.pqBasicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - console.log("Final allFilters:", allFilters); - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; - console.log("Final joinOperator:", joinOperator); - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPQSubmissions({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - <Shell className="gap-4"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 PQ/실사 현황 - </h2> - <InformationButton pagePath="evcp/pq_new" /> - </div> - </div> - </div> - </div> - - {/* Items처럼 직접 테이블 렌더링 */} - <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <PQSubmissionsTable promises={promises} /> - </React.Suspense> - </Shell> - ) +import * as React from "react"
+import { Metadata } from "next"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { Shell } from "@/components/shell"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { searchParamsPQReviewCache } from "@/lib/pq/validations"
+import { getPQSubmissions } from "@/lib/pq/service"
+import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table"
+import { InformationButton } from "@/components/information/information-button"
+export const metadata: Metadata = {
+ title: "PQ 검토/실사 의뢰",
+ description: "",
+}
+
+interface PQReviewPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function PQReviewPage(props: PQReviewPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsPQReviewCache.parse(searchParams)
+ const validFilters = getValidFilters(search.filters)
+
+ // 디버깅 로그 추가
+ console.log("=== PQ Page Debug ===");
+ console.log("Raw searchParams:", searchParams);
+ console.log("Raw basicFilters param:", searchParams.basicFilters);
+ console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters);
+ console.log("Parsed search:", search);
+ console.log("search.filters:", search.filters);
+ console.log("search.basicFilters:", search.basicFilters);
+ console.log("search.pqBasicFilters:", search.pqBasicFilters);
+ console.log("validFilters:", validFilters);
+
+ // 기본 필터 처리 (통일된 이름 사용)
+ let basicFilters = []
+ if (search.basicFilters && search.basicFilters.length > 0) {
+ basicFilters = search.basicFilters
+ console.log("Using search.basicFilters:", basicFilters);
+ } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) {
+ // 하위 호환성을 위해 기존 이름도 지원
+ basicFilters = search.pqBasicFilters
+ console.log("Using search.pqBasicFilters:", basicFilters);
+ } else {
+ console.log("No basic filters found");
+ }
+
+ // 모든 필터를 합쳐서 처리
+ const allFilters = [...validFilters, ...basicFilters]
+
+ console.log("Final allFilters:", allFilters);
+
+ // 조인 연산자도 통일된 이름 사용
+ const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and';
+ console.log("Final joinOperator:", joinOperator);
+
+ // Promise.all로 감싸서 전달
+ const promises = Promise.all([
+ getPQSubmissions({
+ ...search,
+ filters: allFilters,
+ joinOperator,
+ })
+ ])
+
+ return (
+ <Shell className="gap-4">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 검토/실사 의뢰
+ </h2>
+ <InformationButton pagePath="evcp/pq_new" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* Items처럼 직접 테이블 렌더링 */}
+ <React.Suspense
+ key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링
+ fallback={
+ <DataTableSkeleton
+ columnCount={8}
+ searchableColumnCount={2}
+ filterableColumnCount={3}
+ cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PQSubmissionsTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
}
\ No newline at end of file diff --git a/app/[lng]/partners/pq/page.tsx b/app/[lng]/partners/pq/page.tsx index 71741c6c..87bcd409 100644 --- a/app/[lng]/partners/pq/page.tsx +++ b/app/[lng]/partners/pq/page.tsx @@ -10,11 +10,14 @@ export const dynamic = "force-dynamic" export default async function PQInputPage({ searchParams, }: { - searchParams: { projectId?: string } + searchParams: Promise<{ projectId?: string }> }) { // Opt out of caching for this route noStore() + // searchParams를 await + const resolvedSearchParams = await searchParams + // 세션 const session = await getServerSession(authOptions) // 세션에서 vendorId 가져오기 @@ -26,7 +29,7 @@ export default async function PQInputPage({ const projectPQs = await getPQProjectsByVendorId(idAsNumber) // searchParams에서 projectId 파싱 - const projectIdParam = searchParams.projectId + const projectIdParam = resolvedSearchParams.projectId const projectId = projectIdParam ? parseInt(projectIdParam, 10) : undefined // 현재 선택된 프로젝트를 위한 PQ 데이터 가져오기 @@ -40,7 +43,7 @@ export default async function PQInputPage({ pqData={selectedProjectPQData} projectPQs={projectPQs} vendorId={idAsNumber} - rawSearchParams={searchParams} + rawSearchParams={resolvedSearchParams} /> ) }
\ No newline at end of file diff --git a/app/[lng]/partners/pq_new/[id]/page.tsx b/app/[lng]/partners/pq_new/[id]/page.tsx index 52085163..41c59b47 100644 --- a/app/[lng]/partners/pq_new/[id]/page.tsx +++ b/app/[lng]/partners/pq_new/[id]/page.tsx @@ -110,11 +110,14 @@ export default async function PQEditPage(props: PQEditPageProps) { const pageTitle = pqSubmission.type === "PROJECT" ? `프로젝트 PQ - ${pqSubmission.projectName || pqSubmission.projectCode}` + : pqSubmission.type === "NON_INSPECTION" + ? "미실사 PQ" : "일반 PQ"; // 프로젝트 정보 (프로젝트 PQ인 경우) const projectPQ = pqSubmission.projectId ? { id: pqSubmission.projectId, + projectId: pqSubmission.projectId, projectCode: pqSubmission.projectCode || '', projectName: pqSubmission.projectName || '', status: pqSubmission.status, diff --git a/app/[lng]/partners/pq_new/page.tsx b/app/[lng]/partners/pq_new/page.tsx index f822eacc..389a35a2 100644 --- a/app/[lng]/partners/pq_new/page.tsx +++ b/app/[lng]/partners/pq_new/page.tsx @@ -6,7 +6,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { LogIn, Edit, Eye } from "lucide-react"; +import { LogIn, Edit, Eye, Ellipsis } from "lucide-react"; import { Shell } from "@/components/shell"; import { Table, @@ -19,6 +19,13 @@ import { import { unstable_noStore as noStore } from 'next/cache'; import { getAllPQsByVendorId, getPQStatusCounts } from "@/lib/pq/service"; import { InformationButton } from "@/components/information/information-button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + export const metadata: Metadata = { title: "사전 평가 (PQ) 목록", description: "요청된 사전 평가 목록을 확인하고 작성합니다.", @@ -218,8 +225,14 @@ export default async function PQListPage() { return ( <TableRow key={pq.id}> <TableCell> - <Badge variant={pq.type === "PROJECT" ? "secondary" : "outline"}> - {pq.type === "PROJECT" ? "프로젝트" : "일반"} + <Badge variant={ + pq.type === "PROJECT" ? "default" : + pq.type === "NON_INSPECTION" ? "secondary" : + "outline" + }> + {pq.type === "PROJECT" ? "프로젝트" : + pq.type === "NON_INSPECTION" ? "미실사" : + "일반"} </Badge> </TableCell> <TableCell> @@ -238,24 +251,35 @@ export default async function PQListPage() { {getFormattedDate(pq.approvedAt)} </TableCell> <TableCell> - <div className="flex gap-2"> - {canEdit && ( - <Button size="sm" variant="outline" asChild> - <Link href={`/partners/pq_new/${pq.id}`}> - <Edit className="w-4 h-4 mr-1" /> - 작성 - </Link> - </Button> - )} - {canView && ( - <Button size="sm" variant="outline" asChild> - <Link href={`/partners/pq_new/${pq.id}`}> - <Eye className="w-4 h-4 mr-1" /> - 보기 - </Link> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="액션 메뉴 열기" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> </Button> - )} - </div> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-36"> + {canEdit && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Edit className="mr-2 h-4 w-4" /> + 작성 + </Link> + </DropdownMenuItem> + )} + {canView && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Eye className="mr-2 h-4 w-4" /> + 보기 + </Link> + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> </TableCell> </TableRow> ); diff --git a/app/[lng]/partners/site-visit/page.tsx b/app/[lng]/partners/site-visit/page.tsx new file mode 100644 index 00000000..92580b35 --- /dev/null +++ b/app/[lng]/partners/site-visit/page.tsx @@ -0,0 +1,30 @@ +import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service"
+import { ClientSiteVisitWrapper } from "@/lib/site-visit/client-site-visit-wrapper"
+import { unstable_noStore as noStore } from 'next/cache'
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic"
+
+export default async function SiteVisitPage() {
+ // Opt out of caching for this route
+ noStore()
+
+ // 세션
+ const session = await getServerSession(authOptions)
+ // 세션에서 vendorId 가져오기
+ const vendorId = session?.user.companyId
+ const idAsNumber = Number(vendorId)
+
+ // 방문실사 요청 목록 가져오기
+ const siteVisitRequests = await getSiteVisitRequestsByVendorId(idAsNumber)
+
+ // 클라이언트 컴포넌트로 데이터 전달
+ return (
+ <ClientSiteVisitWrapper
+ siteVisitRequests={siteVisitRequests}
+ vendorId={idAsNumber}
+ />
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx deleted file mode 100644 index 55b1e9df..00000000 --- a/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" -import { notFound } from "next/navigation" - -interface ProjectPageProps { - params: { id: string } - searchParams: Promise<SearchParams> -} - -export default async function ProjectPage(props: ProjectPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const projectId = parseInt(id, 10) - - // 유효하지 않은 projectId 확인 - if (isNaN(projectId)) { - notFound() - } - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - const validFilters = getValidFilters(search.filters) - - // 프로젝트별 PQ 데이터 가져오기 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, projectId, false) - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Pre-Qualification Check Sheet - </h2> - <p className="text-muted-foreground"> - 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. - </p> - </div> - <ProjectSelectorWrapper selectedProjectId={projectId} /> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <PqsTable promises={promises} currentProjectId={projectId}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx new file mode 100644 index 00000000..15cb3bf3 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { searchParamsCache } from "@/lib/pq/validations"
+import { getPQsByListId } from "@/lib/pq/service"
+import { PqsTable } from "@/lib/pq/pq-criteria/pq-table"
+import { notFound } from "next/navigation"
+
+interface PQDetailPageProps {
+ params: Promise<{ pqListId: string }>
+ searchParams: Promise<SearchParams>
+}
+
+export default async function PQDetailPage(props: PQDetailPageProps) {
+ const params = await props.params
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+
+ const pqListId = parseInt(params.pqListId)
+ if (isNaN(pqListId)) {
+ notFound()
+ }
+
+ // filters가 없는 경우를 처리
+ const validFilters = getValidFilters(search.filters)
+
+ // PQ 항목들 가져오기
+ const promises = Promise.all([
+ getPQsByListId(pqListId, {
+ ...search,
+ filters: validFilters,
+ })
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 항목 관리
+ </h2>
+ {/* <p className="text-muted-foreground">
+ 선택한 PQ 목록의 세부 항목들을 관리할 수 있습니다.
+ </p> */}
+ </div>
+ </div>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={8}
+ searchableColumnCount={1}
+ filterableColumnCount={3}
+ cellWidths={["10rem", "15rem", "20rem", "15rem", "10rem", "10rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PqsTable
+ promises={promises}
+ pqListId={pqListId}
+ />
+ </React.Suspense>
+ </Shell>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx index 4d2f2d0c..1a337cc9 100644 --- a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx +++ b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx @@ -1,70 +1,61 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - - const validFilters = getValidFilters(search.filters) - - // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, null, true) - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - PQ 항목 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. - </p> */} - </div> - <ProjectSelectorWrapper /> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <PqsTable promises={promises}/> - </React.Suspense> - </Shell> - ) +import * as React from "react"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { searchParamsCache } from "@/lib/pq/validations"
+import { getPQLists } from "@/lib/pq/service"
+import { PqListsTable } from "@/lib/pq/table/pq-lists-table"
+import { getProjects } from "@/lib/pq/service"
+
+interface ProjectPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function ProjectPage(props: ProjectPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+
+ // filters가 없는 경우를 처리
+ const validFilters = getValidFilters(search.filters)
+
+ // // 프로젝트별 PQ 데이터 가져오기
+ const promises = Promise.all([
+ getPQLists({
+ ...search,
+ filters: validFilters,
+ }),
+ getProjects()
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 리스트 관리
+ </h2>
+ {/* <p className="text-muted-foreground">
+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다.
+ </p> */}
+ </div>
+ </div>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PqListsTable
+ promises={promises}
+ />
+ </React.Suspense>
+ </Shell>
+ )
}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx index 28ce3128..b82075e9 100644 --- a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx +++ b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -1,215 +1,217 @@ -import * as React from "react" -import { Metadata } from "next" -import Link from "next/link" -import { notFound } from "next/navigation" -import { ArrowLeft } from "lucide-react" -import { Shell } from "@/components/shell" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Separator } from "@/components/ui/separator" -import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" -import { unstable_noStore as noStore } from 'next/cache' -import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" - -export const metadata: Metadata = { - title: "PQ 검토", - description: "협력업체의 Pre-Qualification 답변을 검토합니다.", -} - -// 페이지가 기본적으로 동적임을 나타냄 -export const dynamic = "force-dynamic" - -interface PQReviewPageProps { - params: Promise<{ - vendorId: string; - submissionId: string; - }> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - // 캐시 비활성화 - noStore() - - const params = await props.params - const vendorId = parseInt(params.vendorId, 10) - const submissionId = parseInt(params.submissionId, 10) - - try { - // PQ Submission 정보 조회 - const pqSubmission = await getPQById(submissionId, vendorId) - - // PQ 데이터 조회 (질문과 답변) - const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) - - // 프로젝트 정보 (프로젝트 PQ인 경우) - const projectInfo = pqSubmission.projectId ? { - id: pqSubmission.projectId, - projectCode: pqSubmission.projectCode || '', - projectName: pqSubmission.projectName || '', - status: pqSubmission.status, - submittedAt: pqSubmission.submittedAt, - } : null - - // PQ 유형 및 상태 레이블 - const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" - const statusLabel = getStatusLabel(pqSubmission.status) - const statusVariant = getStatusVariant(pqSubmission.status) - - // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) - const canReview = pqSubmission.status === "SUBMITTED" - - return ( - <Shell className="gap-6 max-w-5xl"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-4"> - <Button variant="outline" size="sm" asChild> - <Link href="/evcp/pq_new"> - <ArrowLeft className="w-4 h-4 mr-2" /> - 목록으로 - </Link> - </Button> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {pqSubmission.vendorName} - {typeLabel} - </h2> - <div className="flex items-center gap-2 mt-1"> - <Badge variant={statusVariant}>{statusLabel}</Badge> - {projectInfo && ( - <span className="text-muted-foreground"> - {projectInfo.projectName} ({projectInfo.projectCode}) - </span> - )} - </div> - </div> - </div> - </div> - - {/* 상태별 알림 */} - {pqSubmission.status === "SUBMITTED" && ( - <Alert> - <AlertTitle>제출 완료</AlertTitle> - <AlertDescription> - 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. - </AlertDescription> - </Alert> - )} - - {pqSubmission.status === "APPROVED" && ( - <Alert variant="success"> - <AlertTitle>승인됨</AlertTitle> - <AlertDescription> - {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. - </AlertDescription> - </Alert> - )} - - {pqSubmission.status === "REJECTED" && ( - <Alert variant="destructive"> - <AlertTitle>거부됨</AlertTitle> - <AlertDescription> - {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. - {pqSubmission.rejectReason && ( - <div className="mt-2"> - <strong>사유:</strong> {pqSubmission.rejectReason} - </div> - )} - </AlertDescription> - </Alert> - )} - - <Separator /> - - {/* PQ 검토 컴포넌트 */} - <Tabs defaultValue="review" className="w-full"> - <TabsList> - <TabsTrigger value="review">PQ 검토</TabsTrigger> - <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger> - </TabsList> - - <TabsContent value="review" className="mt-4"> - <PQReviewWrapper - pqData={pqData} - vendorId={vendorId} - pqSubmission={pqSubmission} - canReview={canReview} - /> - </TabsContent> - - <TabsContent value="vendor-info" className="mt-4"> - <div className="rounded-md border p-4"> - <h3 className="text-lg font-medium mb-4">협력업체 정보</h3> - <div className="grid grid-cols-2 gap-4"> - <div> - <p className="text-sm font-medium text-muted-foreground">업체명</p> - <p>{pqSubmission.vendorName}</p> - </div> - <div> - <p className="text-sm font-medium text-muted-foreground">업체 코드</p> - <p>{pqSubmission.vendorCode}</p> - </div> - <div> - <p className="text-sm font-medium text-muted-foreground">상태</p> - <p>{pqSubmission.vendorStatus}</p> - </div> - {/* 필요시 추가 정보 표시 */} - </div> - </div> - </TabsContent> - </Tabs> - </Shell> - ) - } catch (error) { - console.error("Error loading PQ:", error) - notFound() - } -} - -// 상태 레이블 함수 -function getStatusLabel(status: string): string { - switch (status) { - case "REQUESTED": - return "요청됨"; - case "IN_PROGRESS": - return "진행 중"; - case "SUBMITTED": - return "제출됨"; - case "APPROVED": - return "승인됨"; - case "REJECTED": - return "거부됨"; - default: - return status; - } -} - -// 상태별 Badge 스타일 -function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { - switch (status) { - case "REQUESTED": - return "outline"; - case "IN_PROGRESS": - return "secondary"; - case "SUBMITTED": - return "default"; - case "APPROVED": - return "success"; - case "REJECTED": - return "destructive"; - default: - return "outline"; - } -} - -// 날짜 형식화 함수 -function formatDate(date: Date | null) { - if (!date) return "날짜 없음"; - return new Date(date).toLocaleDateString("ko-KR", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit" - }); +import * as React from "react"
+import { Metadata } from "next"
+import Link from "next/link"
+import { notFound } from "next/navigation"
+import { ArrowLeft } from "lucide-react"
+import { Shell } from "@/components/shell"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Separator } from "@/components/ui/separator"
+import { getPQById, getPQDataByVendorId } from "@/lib/pq/service"
+import { unstable_noStore as noStore } from 'next/cache'
+import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper"
+
+export const metadata: Metadata = {
+ title: "PQ 검토",
+ description: "협력업체의 Pre-Qualification 답변을 검토합니다.",
+}
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic"
+
+interface PQReviewPageProps {
+ params: Promise<{
+ vendorId: string;
+ submissionId: string;
+ }>
+}
+
+export default async function PQReviewPage(props: PQReviewPageProps) {
+ // 캐시 비활성화
+ noStore()
+
+ const params = await props.params
+ const vendorId = parseInt(params.vendorId, 10)
+ const submissionId = parseInt(params.submissionId, 10)
+
+ try {
+ // PQ Submission 정보 조회
+ const pqSubmission = await getPQById(submissionId, vendorId)
+
+ // PQ 데이터 조회 (질문과 답변)
+ const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined)
+
+ // 프로젝트 정보 (프로젝트 PQ인 경우)
+ const projectInfo = pqSubmission.projectId ? {
+ id: pqSubmission.projectId,
+ projectCode: pqSubmission.projectCode || '',
+ projectName: pqSubmission.projectName || '',
+ status: pqSubmission.status,
+ submittedAt: pqSubmission.submittedAt,
+ } : null
+
+ // PQ 유형 및 상태 레이블
+ const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" :
+ pqSubmission.type === "PROJECT" ? "프로젝트 PQ" :
+ pqSubmission.type === "NON_INSPECTION" ? "미실사 PQ" : "일반 PQ"
+ const statusLabel = getStatusLabel(pqSubmission.status)
+ const statusVariant = getStatusVariant(pqSubmission.status)
+
+ // 수정 가능 여부 (SUBMITTED 상태일 때만 가능)
+ const canReview = pqSubmission.status === "SUBMITTED"
+
+ return (
+ <Shell className="gap-6 max-w-5xl">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Button variant="outline" size="sm" asChild>
+ <Link href="/procurement/pq_new">
+ <ArrowLeft className="w-4 h-4 mr-2" />
+ 목록으로
+ </Link>
+ </Button>
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {pqSubmission.vendorName} - {typeLabel}
+ </h2>
+ <div className="flex items-center gap-2 mt-1">
+ <Badge variant={statusVariant}>{statusLabel}</Badge>
+ {projectInfo && (
+ <span className="text-muted-foreground">
+ {projectInfo.projectName} ({projectInfo.projectCode})
+ </span>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 상태별 알림 */}
+ {pqSubmission.status === "SUBMITTED" && (
+ <Alert>
+ <AlertTitle>제출 완료</AlertTitle>
+ <AlertDescription>
+ 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {pqSubmission.status === "APPROVED" && (
+ <Alert variant="success">
+ <AlertTitle>승인됨</AlertTitle>
+ <AlertDescription>
+ {formatDate(pqSubmission.approvedAt)}에 승인되었습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {pqSubmission.status === "REJECTED" && (
+ <Alert variant="destructive">
+ <AlertTitle>거부됨</AlertTitle>
+ <AlertDescription>
+ {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다.
+ {pqSubmission.rejectReason && (
+ <div className="mt-2">
+ <strong>사유:</strong> {pqSubmission.rejectReason}
+ </div>
+ )}
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <Separator />
+
+ {/* PQ 검토 컴포넌트 */}
+ <Tabs defaultValue="review" className="w-full">
+ <TabsList>
+ <TabsTrigger value="review">PQ 검토</TabsTrigger>
+ <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="review" className="mt-4">
+ <PQReviewWrapper
+ pqData={pqData}
+ vendorId={vendorId}
+ pqSubmission={pqSubmission}
+ canReview={canReview}
+ />
+ </TabsContent>
+
+ <TabsContent value="vendor-info" className="mt-4">
+ <div className="rounded-md border p-4">
+ <h3 className="text-lg font-medium mb-4">협력업체 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">업체명</p>
+ <p>{pqSubmission.vendorName}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">업체 코드</p>
+ <p>{pqSubmission.vendorCode}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">상태</p>
+ <p>{pqSubmission.vendorStatus}</p>
+ </div>
+ {/* 필요시 추가 정보 표시 */}
+ </div>
+ </div>
+ </TabsContent>
+ </Tabs>
+ </Shell>
+ )
+ } catch (error) {
+ console.error("Error loading PQ:", error)
+ notFound()
+ }
+}
+
+// 상태 레이블 함수
+function getStatusLabel(status: string): string {
+ switch (status) {
+ case "REQUESTED":
+ return "요청됨";
+ case "IN_PROGRESS":
+ return "진행 중";
+ case "SUBMITTED":
+ return "제출됨";
+ case "APPROVED":
+ return "승인됨";
+ case "REJECTED":
+ return "거부됨";
+ default:
+ return status;
+ }
+}
+
+// 상태별 Badge 스타일
+function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" {
+ switch (status) {
+ case "REQUESTED":
+ return "outline";
+ case "IN_PROGRESS":
+ return "secondary";
+ case "SUBMITTED":
+ return "default";
+ case "APPROVED":
+ return "success";
+ case "REJECTED":
+ return "destructive";
+ default:
+ return "outline";
+ }
+}
+
+// 날짜 형식화 함수
+function formatDate(date: Date | null) {
+ if (!date) return "날짜 없음";
+ return new Date(date).toLocaleDateString("ko-KR", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/page.tsx index 550ac87d..0e6d3196 100644 --- a/app/[lng]/procurement/(procurement)/pq_new/page.tsx +++ b/app/[lng]/procurement/(procurement)/pq_new/page.tsx @@ -1,96 +1,99 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { searchParamsPQReviewCache } from "@/lib/pq/validations" -import { getPQSubmissions } from "@/lib/pq/service" -import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" - -export const metadata: Metadata = { - title: "협력업체 PQ/실사 현황", - description: "", -} - -interface PQReviewPageProps { - searchParams: Promise<SearchParams> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - const searchParams = await props.searchParams - const search = searchParamsPQReviewCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 디버깅 로그 추가 - console.log("=== PQ Page Debug ==="); - console.log("Raw searchParams:", searchParams); - console.log("Raw basicFilters param:", searchParams.basicFilters); - console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); - console.log("Parsed search:", search); - console.log("search.filters:", search.filters); - console.log("search.basicFilters:", search.basicFilters); - console.log("search.pqBasicFilters:", search.pqBasicFilters); - console.log("validFilters:", validFilters); - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { - // 하위 호환성을 위해 기존 이름도 지원 - basicFilters = search.pqBasicFilters - console.log("Using search.pqBasicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - console.log("Final allFilters:", allFilters); - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; - console.log("Final joinOperator:", joinOperator); - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPQSubmissions({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - <Shell className="gap-4"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 PQ/실사 현황 - </h2> - </div> - </div> - </div> - - {/* Items처럼 직접 테이블 렌더링 */} - <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <PQSubmissionsTable promises={promises} /> - </React.Suspense> - </Shell> - ) +import * as React from "react"
+import { Metadata } from "next"
+import { type SearchParams } from "@/types/table"
+import { getValidFilters } from "@/lib/data-table"
+import { Shell } from "@/components/shell"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { searchParamsPQReviewCache } from "@/lib/pq/validations"
+import { getPQSubmissions } from "@/lib/pq/service"
+import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table"
+import { InformationButton } from "@/components/information/information-button"
+export const metadata: Metadata = {
+ title: "PQ 검토/실사 의뢰",
+ description: "",
+}
+
+interface PQReviewPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function PQReviewPage(props: PQReviewPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsPQReviewCache.parse(searchParams)
+ const validFilters = getValidFilters(search.filters)
+
+ // 디버깅 로그 추가
+ console.log("=== PQ Page Debug ===");
+ console.log("Raw searchParams:", searchParams);
+ console.log("Raw basicFilters param:", searchParams.basicFilters);
+ console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters);
+ console.log("Parsed search:", search);
+ console.log("search.filters:", search.filters);
+ console.log("search.basicFilters:", search.basicFilters);
+ console.log("search.pqBasicFilters:", search.pqBasicFilters);
+ console.log("validFilters:", validFilters);
+
+ // 기본 필터 처리 (통일된 이름 사용)
+ let basicFilters = []
+ if (search.basicFilters && search.basicFilters.length > 0) {
+ basicFilters = search.basicFilters
+ console.log("Using search.basicFilters:", basicFilters);
+ } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) {
+ // 하위 호환성을 위해 기존 이름도 지원
+ basicFilters = search.pqBasicFilters
+ console.log("Using search.pqBasicFilters:", basicFilters);
+ } else {
+ console.log("No basic filters found");
+ }
+
+ // 모든 필터를 합쳐서 처리
+ const allFilters = [...validFilters, ...basicFilters]
+
+ console.log("Final allFilters:", allFilters);
+
+ // 조인 연산자도 통일된 이름 사용
+ const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and';
+ console.log("Final joinOperator:", joinOperator);
+
+ // Promise.all로 감싸서 전달
+ const promises = Promise.all([
+ getPQSubmissions({
+ ...search,
+ filters: allFilters,
+ joinOperator,
+ })
+ ])
+
+ return (
+ <Shell className="gap-4">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ PQ 검토/실사 의뢰
+ </h2>
+ <InformationButton pagePath="evcp/pq_new" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* Items처럼 직접 테이블 렌더링 */}
+ <React.Suspense
+ key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링
+ fallback={
+ <DataTableSkeleton
+ columnCount={8}
+ searchableColumnCount={2}
+ filterableColumnCount={3}
+ cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <PQSubmissionsTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
}
\ No newline at end of file |
