summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx5
-rw-r--r--app/[lng]/evcp/(evcp)/edp-progress/page.tsx2
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx140
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/[id]/vendor/page.tsx133
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/page.tsx3
-rw-r--r--app/api/rfq-attachments/[id]/route.ts2
-rw-r--r--app/api/rfq-attachments/revision/route.ts6
7 files changed, 222 insertions, 69 deletions
diff --git a/app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx
index 830d7146..8f823d4c 100644
--- a/app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx
@@ -23,6 +23,7 @@ import { getGtcDocumentById } from "@/lib/gtc-contract/service"
import { searchParamsCache } from "@/lib/gtc-contract/gtc-clauses/validations"
import { GtcClausesVendorTable } from "@/lib/basic-contract/gtc-vendor/clause-table"
import { UpdateVendorDocumentStatusButton } from "@/lib/basic-contract/vendor-table/update-vendor-document-status-button"
+import { getVendorById } from "@/lib/vendors/repository"
interface GtcClausesPageProps {
params: Promise<{ id: string }>
@@ -66,7 +67,9 @@ export default async function GtcClausesPage(props: GtcClausesPageProps) {
documentId,
}),
getUsersForFilter(),
- getVendorClausesForDocument({ documentId, vendorId }) // vendorId 전달
+ getVendorClausesForDocument({ documentId, vendorId }), // vendorId 전달
+ vendorId ? getVendorById(vendorId) : Promise.resolve(null), // vendor 데이터 추가
+
])
const [_, __, vendorData] = await promises.then(values => values)
diff --git a/app/[lng]/evcp/(evcp)/edp-progress/page.tsx b/app/[lng]/evcp/(evcp)/edp-progress/page.tsx
index 7edc52c9..fe040709 100644
--- a/app/[lng]/evcp/(evcp)/edp-progress/page.tsx
+++ b/app/[lng]/evcp/(evcp)/edp-progress/page.tsx
@@ -12,7 +12,7 @@ export default async function IndexPage() {
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
<div className="flex items-center gap-2">
- <h2 className="text-2xl font-bold tracking-tight">벤더 진척도 현황</h2>
+ <h2 className="text-2xl font-bold tracking-tight">벤더 데이터 진척도 현황</h2>
<InformationButton pagePath="evcp/edp-progress" />
</div>
</div>
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
index 1ccb7559..82f9fc4c 100644
--- a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
@@ -1,26 +1,24 @@
import { Separator } from "@/components/ui/separator"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations"
-import { getRfqLastAttachments } from "@/lib/rfq-last/service"
+import { getRfqAllAttachments, getRfqVendorAttachments } from "@/lib/rfq-last/service"
import { RfqAttachmentsTable } from "@/lib/rfq-last/attachment/rfq-attachments-table"
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
-import { AlertCircle } from "lucide-react"
+import { AlertCircle, ArrowLeft, ArrowRight } from "lucide-react"
+import { Card, CardContent,CardDescription ,CardHeader ,CardTitle} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { VendorResponseTable } from "@/lib/rfq-last/attachment/vendor-response-table"
interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
params: {
lng: string
id: string
}
- searchParams: Promise<SearchParams>
+ searchParams: Promise<Record<string, any>>
}
export default async function RfqPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
+ const resolvedParams = await props.params;
const rfqId = parseInt(resolvedParams.id, 10);
-
+
if (!rfqId || isNaN(rfqId) || rfqId <= 0) {
return (
<div className="p-4">
@@ -35,65 +33,85 @@ export default async function RfqPage(props: IndexPageProps) {
);
}
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams;
- const activeTab = searchParams.tab || '설계';
+ // 모든 첨부파일 데이터 가져오기
+ const { data, success } = await getRfqAllAttachments(rfqId);
+ const { vendorData, vendorSuccess } = await getRfqVendorAttachments(rfqId);
+
+ if (!success) {
+ return (
+ <div className="p-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>오류</AlertTitle>
+ <AlertDescription>
+ 데이터를 불러오는데 실패했습니다.
+ </AlertDescription>
+ </Alert>
+ </div>
+ );
+ }
- // 활성 탭에 따라 다른 파라미터 파싱
- const designSearch = activeTab === '설계'
- ? searchParamsRfqAttachmentsCache.parse({
- ...searchParams,
- // design_ prefix가 붙은 파라미터들 추출
- page: searchParams.design_page,
- perPage: searchParams.design_perPage,
- sort: searchParams.design_sort,
- filters: searchParams.design_filters,
- })
- : { page: 1, perPage: 10, sort: [], filters: [] };
-
- const purchaseSearch = activeTab === '구매'
- ? searchParamsRfqAttachmentsCache.parse({
- ...searchParams,
- // purchase_ prefix가 붙은 파라미터들 추출
- page: searchParams.purchase_page,
- perPage: searchParams.purchase_perPage,
- sort: searchParams.purchase_sort,
- filters: searchParams.purchase_filters,
- })
- : { page: 1, perPage: 10, sort: [], filters: [] };
-
- // 활성 탭의 데이터만 실제로 가져오기
- const [designData, purchaseData] = await Promise.all([
- activeTab === '설계'
- ? getRfqLastAttachments({ ...designSearch }, rfqId, "설계")
- : { data: [], pageCount: 0 },
- activeTab === '구매'
- ? getRfqLastAttachments({ ...purchaseSearch }, rfqId, "구매")
- : { data: [], pageCount: 0 }
- ]);
-
-
- // 4) 렌더링
return (
<div className="space-y-6">
<div>
- <h3 className="text-lg font-medium">
- 견적 RFQ 문서관리
- </h3>
+ <h3 className="text-lg font-medium">견적 RFQ 문서관리</h3>
<p className="text-sm text-muted-foreground">
설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다.
</p>
</div>
<Separator />
- <div>
- <RfqAttachmentsTable
- rfqId={rfqId}
- initialDesignData={designData}
- initialPurchaseData={purchaseData}
- />
- </div>
+
+ {/* 구매자 → 벤더 문서 섹션 */}
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="space-y-1">
+ <CardTitle className="text-base font-semibold">
+ RFQ 발송 문서
+ </CardTitle>
+ <CardDescription>
+ 구매자가 벤더에게 발송하는 견적 요청 문서
+ </CardDescription>
+ </div>
+ <Badge variant="outline" className="font-mono">
+ <ArrowRight className="h-3 w-3 mr-1" />
+ To Vendor
+ </Badge>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <RfqAttachmentsTable
+ rfqId={rfqId}
+ initialData={data}
+ />
+ </CardContent>
+ </Card>
+
+ {/* 벤더 → 구매자 문서 섹션 */}
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="space-y-1">
+ <CardTitle className="text-base font-semibold">
+ 벤더 회신 문서
+ </CardTitle>
+ <CardDescription>
+ 벤더가 구매자에게 회신한 견적 및 기술 문서
+ </CardDescription>
+ </div>
+ <Badge variant="outline" className="font-mono">
+ <ArrowLeft className="h-3 w-3 mr-1" />
+ From Vendor
+ </Badge>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <VendorResponseTable
+ rfqId={rfqId}
+ initialData={vendorData}
+ />
+ </CardContent>
+ </Card>
</div>
- )
+ );
} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/vendor/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/vendor/page.tsx
new file mode 100644
index 00000000..30075cbe
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/vendor/page.tsx
@@ -0,0 +1,133 @@
+import { Separator } from "@/components/ui/separator";
+import { getRfqWithDetails, getRfqVendorResponses } from "@/lib/rfq-last/service";
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
+import { AlertCircle, Users, Send, FileText, CheckCircle2, Clock, XCircle } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { RfqVendorTable } from "@/lib/rfq-last/vendor/rfq-vendor-table";
+import { VendorResponseStatusCard } from "@/lib/rfq-last/vendor/vendor-response-status-card";
+
+interface VendorPageProps {
+ params: {
+ lng: string;
+ id: string;
+ };
+ searchParams: Promise<Record<string, any>>;
+}
+
+export default async function VendorPage(props: VendorPageProps) {
+ const resolvedParams = await props.params;
+ const rfqId = parseInt(resolvedParams.id, 10);
+
+ if (!rfqId || isNaN(rfqId) || rfqId <= 0) {
+ return (
+ <div className="p-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>오류</AlertTitle>
+ <AlertDescription>유효하지 않은 RFQ입니다.</AlertDescription>
+ </Alert>
+ </div>
+ );
+ }
+
+ // RFQ 기본 정보 및 벤더 목록 가져오기
+ const { data: rfqData, success: rfqSuccess } = await getRfqWithDetails(rfqId);
+ const { data: vendorResponses, success: responsesSuccess } = await getRfqVendorResponses(rfqId);
+
+ if (!rfqSuccess || !rfqData) {
+ return (
+ <div className="p-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>오류</AlertTitle>
+ <AlertDescription>데이터를 불러오는데 실패했습니다.</AlertDescription>
+ </Alert>
+ </div>
+ );
+ }
+
+ // 응답 상태별 집계
+ const statusSummary = {
+ total: vendorResponses?.length || 0,
+ invited: vendorResponses?.filter(v => v.status === "초대됨").length || 0,
+ drafting: vendorResponses?.filter(v => v.status === "작성중").length || 0,
+ submitted: vendorResponses?.filter(v => v.status === "제출완료").length || 0,
+ confirmed: vendorResponses?.filter(v => v.status === "최종확정").length || 0,
+ cancelled: vendorResponses?.filter(v => v.status === "취소").length || 0,
+ };
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">벤더 관리</h3>
+ <p className="text-sm text-muted-foreground">
+ 견적 요청을 보낼 벤더를 관리하고 응답 상태를 확인합니다.
+ </p>
+ </div>
+ <Separator />
+
+
+ {/* 응답 상태 요약 카드 */}
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
+ <VendorResponseStatusCard
+ title="전체 벤더"
+ count={statusSummary.total}
+ icon={Users}
+ variant="default"
+ />
+ <VendorResponseStatusCard
+ title="초대됨"
+ count={statusSummary.invited}
+ icon={Send}
+ variant="secondary"
+ />
+ <VendorResponseStatusCard
+ title="작성중"
+ count={statusSummary.drafting}
+ icon={Clock}
+ variant="warning"
+ />
+ <VendorResponseStatusCard
+ title="제출완료"
+ count={statusSummary.submitted}
+ icon={CheckCircle2}
+ variant="success"
+ />
+ <VendorResponseStatusCard
+ title="최종확정"
+ count={statusSummary.confirmed}
+ icon={FileText}
+ variant="primary"
+ />
+ </div>
+
+ {/* 벤더 목록 테이블 */}
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="space-y-1">
+ <CardTitle className="text-base font-semibold">
+ 벤더 목록
+ </CardTitle>
+ <CardDescription>
+ 견적 요청 대상 벤더와 응답 상태를 관리합니다.
+ </CardDescription>
+ </div>
+ <Badge variant="outline" className="font-mono">
+ {statusSummary.total} Vendors
+ </Badge>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <RfqVendorTable
+ rfqId={rfqId}
+ rfqCode={rfqData.rfqCode }
+ rfqDetails={rfqData.details || []}
+ vendorResponses={vendorResponses || []}
+ />
+ </CardContent>
+ </Card>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx
index 207bc2d4..0297c4f9 100644
--- a/app/[lng]/evcp/(evcp)/rfq-last/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx
@@ -37,7 +37,7 @@ interface RfqPageProps {
// 탭별 데이터 카운트를 가져오는 함수
async function getTabCounts() {
try {
- const [allData, generalData, itbData, rfqData] = await Promise.all([
+ const [generalData, itbData, rfqData] = await Promise.all([
getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "general" }),
getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "itb" }),
getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "rfq" }),
@@ -51,7 +51,6 @@ async function getTabCounts() {
} catch (error) {
console.error("Error fetching tab counts:", error);
return {
- all: 0,
general: 0,
itb: 0,
rfq: 0,
diff --git a/app/api/rfq-attachments/[id]/route.ts b/app/api/rfq-attachments/[id]/route.ts
index df99c1ad..8cb8a747 100644
--- a/app/api/rfq-attachments/[id]/route.ts
+++ b/app/api/rfq-attachments/[id]/route.ts
@@ -4,7 +4,7 @@ import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import db from "@/db/db";
import { eq } from "drizzle-orm";
import { rfqLastAttachments, rfqLastAttachmentRevisions } from "@/db/schema";
-import { deleteFile } from "@/lib/file-storage";
+import { deleteFile } from "@/lib/file-stroage";
export async function DELETE(
request: NextRequest,
diff --git a/app/api/rfq-attachments/revision/route.ts b/app/api/rfq-attachments/revision/route.ts
index 2592ae78..9a46d309 100644
--- a/app/api/rfq-attachments/revision/route.ts
+++ b/app/api/rfq-attachments/revision/route.ts
@@ -4,7 +4,7 @@ import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import db from "@/db/db";
import { eq } from "drizzle-orm";
import { rfqLastAttachments, rfqLastAttachmentRevisions } from "@/db/schema";
-import { saveFile, deleteFile } from "@/lib/file-storage";
+import { saveFile, deleteFile } from "@/lib/file-stroage";
export async function POST(request: NextRequest) {
try {
@@ -85,8 +85,8 @@ export async function POST(request: NextRequest) {
fileType: file.type || "unknown",
isLatest: true,
revisionComment,
- uploadedBy: parseInt(session.user.id),
- uploadedAt: new Date(),
+ createdBy: parseInt(session.user.id),
+ createdAt: new Date(),
})
.returning();