diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/basic-contract/vendor-gtc/[id]/page.tsx | 5 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/edp-progress/page.tsx | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx | 140 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/[id]/vendor/page.tsx | 133 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/page.tsx | 3 | ||||
| -rw-r--r-- | app/api/rfq-attachments/[id]/route.ts | 2 | ||||
| -rw-r--r-- | app/api/rfq-attachments/revision/route.ts | 6 |
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(); |
