import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum,parseAsBoolean } from "nuqs/server" import * as z from "zod" import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" import { FinalRfqDetailView, VendorAttachmentResponse } from "@/db/schema"; export const searchParamsRFQDashboardCache = createSearchParamsCache({ // 공통 플래그 flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), // 페이징 page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), // 정렬 - rfqDashboardView 기반 sort: getSortingStateParser<{ rfqId: number; rfqCode: string; description: string; status: string; dueDate: Date; projectCode: string; projectName: string; packageNo: string; packageName: string; picName: string; totalAttachments: number; initialVendorCount: number; finalVendorCount: number; initialResponseRate: number; finalResponseRate: number; overallProgress: number; daysToDeadline: number; createdAt: Date; }>().withDefault([ { id: "createdAt", desc: true }, ]), // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 기본 필터 rfqBasicFilters: getFiltersStateParser().withDefault([]), rfqBasicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), basicFilters: getFiltersStateParser().withDefault([]), basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 검색 키워드 search: parseAsString.withDefault(""), // RFQ 특화 필터 rfqCode: parseAsString.withDefault(""), projectName: parseAsString.withDefault(""), projectCode: parseAsString.withDefault(""), picName: parseAsString.withDefault(""), packageNo: parseAsString.withDefault(""), status: parseAsStringEnum([ "DRAFT", "Doc. Received", "PIC Assigned", "Doc. Confirmed", "Init. RFQ Sent", "Init. RFQ Answered", "TBE started", "TBE finished", "Final RFQ Sent", "Quotation Received", "Vendor Selected" ]), dueDateFrom: parseAsString.withDefault(""), dueDateTo: parseAsString.withDefault(""), progressMin: parseAsInteger.withDefault(0), progressMax: parseAsInteger.withDefault(100), }); export type GetRFQDashboardSchema = Awaited> export const createRfqServerSchema = z.object({ projectId: z.number().min(1, "프로젝트를 선택해주세요"), // 필수로 변경 dueDate: z.date(), // Date 객체로 직접 받기 picCode: z.string().min(1, "구매 담당자 코드를 입력해주세요"), picName: z.string().optional(), engPicName: z.string().optional(), packageNo: z.string().min(1, "패키지 번호를 입력해주세요"), packageName: z.string().min(1, "패키지명을 입력해주세요"), remark: z.string().optional(), projectCompany: z.string().optional(), projectFlag: z.string().optional(), projectSite: z.string().optional(), createdBy: z.number(), updatedBy: z.number(), }) export type CreateRfqInput = z.infer export type RfqAttachment = { id: number attachmentType: string serialNo: string rfqId: number fileName: string originalFileName: string filePath: string fileSize: number | null fileType: string | null description: string | null createdBy: number createdAt: Date createdByName?: string responseStats?: { totalVendors: number respondedCount: number pendingCount: number waivedCount: number responseRate: number } } // RFQ Attachments용 검색 파라미터 캐시 export const searchParamsRfqAttachmentsCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), sort: getSortingStateParser().withDefault([ { id: "createdAt", desc: true }, ]), // 기본 필터 attachmentType: parseAsArrayOf(z.string()).withDefault([]), fileType: parseAsArrayOf(z.string()).withDefault([]), search: parseAsString.withDefault(""), // advanced filter filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), }) // 스키마 타입들 export type GetRfqAttachmentsSchema = Awaited> // 첨부파일 레코드 타입 export const attachmentRecordSchema = z.object({ rfqId: z.number().positive(), attachmentType: z.enum(["구매", "설계"]), // serialNo: z.string().min(1), description: z.string().optional(), fileName: z.string(), originalFileName: z.string(), filePath: z.string(), fileSize: z.number(), fileType: z.string(), }) export type AttachmentRecord = z.infer export const deleteAttachmentsSchema = z.object({ ids: z.array(z.number()).min(1, "삭제할 첨부파일을 선택해주세요."), }) export type DeleteAttachmentsInput = z.infer //Inital RFQ export const searchParamsInitialRfqDetailCache = createSearchParamsCache({ // 공통 플래그 flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), // 페이징 page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), // 정렬 - initialRfqDetailView 기반 sort: getSortingStateParser<{ rfqId: number; rfqCode: string; rfqStatus: string; initialRfqId: number; initialRfqStatus: string; vendorId: number; vendorCode: string; vendorName: string; vendorCountry: string; vendorBusinessSize: string; dueDate: Date; validDate: Date; incotermsCode: string; incotermsDescription: string; shortList: boolean; returnYn: boolean; cpRequestYn: boolean; prjectGtcYn: boolean; returnRevision: number; gtc: string; gtcValidDate: string; classification: string; sparepart: string; createdAt: Date; updatedAt: Date; }>().withDefault([ { id: "createdAt", desc: true }, ]), // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 기본 필터 basicFilters: getFiltersStateParser().withDefault([]), basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 검색 키워드 search: parseAsString.withDefault(""), // Initial RFQ Detail 특화 필터 rfqCode: parseAsString.withDefault(""), rfqStatus: parseAsStringEnum([ "DRAFT", "Doc. Received", "PIC Assigned", "Doc. Confirmed", "Init. RFQ Sent", "Init. RFQ Answered", "TBE started", "TBE finished", "Final RFQ Sent", "Quotation Received", "Vendor Selected" ]), initialRfqStatus: parseAsStringEnum([ "PENDING", "SENT", "RESPONDED", "EXPIRED", "CANCELLED" ]), vendorName: parseAsString.withDefault(""), vendorCode: parseAsString.withDefault(""), vendorCountry: parseAsString.withDefault(""), vendorBusinessSize: parseAsStringEnum([ "LARGE", "MEDIUM", "SMALL", "STARTUP" ]), incotermsCode: parseAsString.withDefault(""), dueDateFrom: parseAsString.withDefault(""), dueDateTo: parseAsString.withDefault(""), validDateFrom: parseAsString.withDefault(""), validDateTo: parseAsString.withDefault(""), shortList: parseAsStringEnum(["true", "false"]), returnYn: parseAsStringEnum(["true", "false"]), cpRequestYn: parseAsStringEnum(["true", "false"]), prjectGtcYn: parseAsStringEnum(["true", "false"]), classification: parseAsString.withDefault(""), sparepart: parseAsString.withDefault(""), }); export type GetInitialRfqDetailSchema = Awaited>; export const updateInitialRfqSchema = z.object({ initialRfqStatus: z.enum(["DRAFT", "Init. RFQ Sent", "S/L Decline", "Init. RFQ Answered"]), dueDate: z.date({ required_error: "마감일을 선택해주세요.", }), validDate: z.date().optional(), gtc: z.string().optional(), gtcValidDate: z.string().optional(), incotermsCode: z.string().max(20, "Incoterms 코드는 20자 이하여야 합니다.").optional(), classification: z.string().max(255, "분류는 255자 이하여야 합니다.").optional(), sparepart: z.string().max(255, "예비부품은 255자 이하여야 합니다.").optional(), shortList: z.boolean().default(false), returnYn: z.boolean().default(false), cpRequestYn: z.boolean().default(false), prjectGtcYn: z.boolean().default(false), rfqRevision: z.number().int().min(0, "RFQ 리비전은 0 이상이어야 합니다.").default(0), }) export const removeInitialRfqsSchema = z.object({ ids: z.array(z.number()).min(1, "최소 하나의 항목을 선택해주세요."), }) export type UpdateInitialRfqSchema = z.infer export type RemoveInitialRfqsSchema = z.infer // 벌크 이메일 발송 스키마 export const bulkEmailSchema = z.object({ initialRfqIds: z.array(z.number()).min(1, "최소 하나의 초기 RFQ를 선택해주세요."), language: z.enum(["en", "ko"]).default("en"), }) export type BulkEmailInput = z.infer // 검색 파라미터 캐시 설정 export type ResponseStatus = "NOT_RESPONDED" | "RESPONDED" | "REVISION_REQUESTED" | "WAIVED"; export type RfqType = "INITIAL" | "FINAL"; export type VendorRfqResponseColumns = { id: string; vendorId: number; rfqRecordId: number; rfqType: RfqType; overallStatus: ResponseStatus; totalAttachments: number; respondedCount: number; pendingCount: number; responseRate: number; completionRate: number; requestedAt: Date; lastRespondedAt: Date | null; }; // 검색 파라미터 캐시 설정 export const searchParamsVendorResponseCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), sort: getSortingStateParser().withDefault([ { id: "requestedAt", desc: true }, ]), // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 기본 필터 basicFilters: getFiltersStateParser().withDefault([]), basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 검색 및 필터 search: parseAsString.withDefault(""), rfqType: parseAsStringEnum(["INITIAL", "FINAL", "ALL"]).withDefault("ALL"), responseStatus: parseAsStringEnum(["NOT_RESPONDED", "RESPONDED", "REVISION_REQUESTED", "WAIVED", "ALL"]).withDefault("ALL"), // 날짜 범위 from: parseAsString.withDefault(""), to: parseAsString.withDefault(""), }); export type GetVendorResponsesSchema = Awaited>; // vendorId + rfqRecordId로 그룹핑된 응답 요약 타입 export type VendorRfqResponseSummary = { id: string; // vendorId + rfqRecordId + rfqType 조합으로 생성된 고유 ID vendorId: number; rfqRecordId: number; rfqType: RfqType; // RFQ 정보 rfq: { id: number; rfqCode: string | null; description: string | null; status: string; dueDate: Date; } | null; // 벤더 정보 vendor: { id: number; vendorCode: string; vendorName: string; country: string | null; businessSize: string | null; } | null; // 응답 통계 totalAttachments: number; respondedCount: number; pendingCount: number; revisionRequestedCount: number; waivedCount: number; responseRate: number; completionRate: number; overallStatus: ResponseStatus; // 전체적인 상태 // 날짜 정보 requestedAt: Date; lastRespondedAt: Date | null; // 기타 hasComments: boolean; }; // 수정 요청 스키마 export const requestRevisionSchema = z.object({ responseId: z.number().positive(), revisionReason: z.string().min(10, "수정 요청 사유를 최소 10자 이상 입력해주세요").max(500), }); // 수정 요청 결과 타입 export type RequestRevisionResult = { success: boolean; message: string; error?: string; }; export const shortListConfirmSchema = z.object({ rfqId: z.number(), selectedVendorIds: z.array(z.number()).min(1), rejectedVendorIds: z.array(z.number()), }) export type ShortListConfirmInput = z.infer export const searchParamsFinalRfqDetailCache = createSearchParamsCache({ // 공통 플래그 flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), // 페이징 page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), // 정렬 - initialRfqDetailView 기반 sort: getSortingStateParser().withDefault([ { id: "createdAt", desc: true }, ]), // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 기본 필터 basicFilters: getFiltersStateParser().withDefault([]), basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 검색 키워드 search: parseAsString.withDefault(""), }); export type GetFinalRfqDetailSchema = Awaited>;