diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-01 10:28:05 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-01 10:28:05 +0000 |
| commit | cd0ce0cbe8af8719a6f542098ec78f2a5c1222ce (patch) | |
| tree | aca09d123c60852400c2fc8603928e110c53e8f5 /lib/rfq-last | |
| parent | 3a76ebfa18df15096ac21d10bf46f85b4d1e45e2 (diff) | |
(최겸) 구매 입찰 사전견적 개발(rfq-last)
Diffstat (limited to 'lib/rfq-last')
| -rw-r--r-- | lib/rfq-last/quotation-compare-view.tsx | 22 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 37 | ||||
| -rw-r--r-- | lib/rfq-last/shared/rfq-items-dialog.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-columns.tsx | 338 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table.tsx | 9 | ||||
| -rw-r--r-- | lib/rfq-last/validations.ts | 7 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/quotation-items-table.tsx | 2 |
7 files changed, 392 insertions, 25 deletions
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index be19f738..86ef4444 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -55,6 +55,7 @@ import { ComparisonData, selectVendor, cancelVendorSelection, VendorResponseVers import { createPO, createGeneralContract, createBidding } from "./contract-actions"; import { toast } from "sonner"; import { useRouter } from "next/navigation" +import { GENERAL_CONTRACT_TYPES, GENERAL_CONTRACT_TYPE_LABELS } from "@/lib/general-contracts/types"; interface QuotationCompareViewProps { data: ComparisonData; @@ -82,23 +83,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [contractEndDate, setContractEndDate] = React.useState(""); // 계약종류 옵션 - const contractTypes = [ - { value: 'UP', label: 'UP - 자재단가계약' }, - { value: 'LE', label: 'LE - 임대차계약' }, - { value: 'IL', label: 'IL - 개별운송계약' }, - { value: 'AL', label: 'AL - 연간운송계약' }, - { value: 'OS', label: 'OS - 외주용역계약' }, - { value: 'OW', label: 'OW - 도급계약' }, - { value: 'IS', label: 'IS - 검사계약' }, - { value: 'LO', label: 'LO - LOI (의향서)' }, - { value: 'FA', label: 'FA - Frame Agreement' }, - { value: 'SC', label: 'SC - 납품합의계약' }, - { value: 'OF', label: 'OF - 클레임상계계약' }, - { value: 'AW', label: 'AW - 사전작업합의' }, - { value: 'AD', label: 'AD - 사전납품합의' }, - { value: 'AM', label: 'AM - 설계계약' }, - { value: 'SC_SELL', label: 'SC - 폐기물매각계약' }, - ]; + const contractTypes = GENERAL_CONTRACT_TYPES.map(type => ({ + value: type, + label: `${type} - ${GENERAL_CONTRACT_TYPE_LABELS[type]}` + })); // 입찰 관련 state 추가 const [biddingContractType, setBiddingContractType] = React.useState<"unit_price" | "general" | "sale">("unit_price"); diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 7ebec795..68cfdac7 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -27,6 +27,37 @@ export async function getDefaultDueDate(): Promise<Date> { return defaultDueDate; } +export type Project = { + id: number; + projectCode: string; + projectName: string; + type: string; +} + +export async function getProjects(): Promise<Project[]> { + try { + // 트랜잭션을 사용하여 프로젝트 데이터 조회 + const projectList = await db.transaction(async (tx) => { + // 모든 프로젝트 조회 + const results = await tx + .select({ + id: projects.id, + projectCode: projects.code, // 테이블의 실제 컬럼명에 맞게 조정 + projectName: projects.name, // 테이블의 실제 컬럼명에 맞게 조정 + type: projects.type, // 테이블의 실제 컬럼명에 맞게 조정 + }) + .from(projects) + .orderBy(projects.code); + + return results; + }); + + return projectList; + } catch (error) { + console.error("프로젝트 목록 가져오기 실패:", error); + return []; // 오류 발생 시 빈 배열 반환 + } +} export async function getRfqs(input: GetRfqsSchema) { unstable_noStore(); @@ -47,6 +78,11 @@ export async function getRfqs(input: GetRfqsSchema) { typeFilter = like(rfqsLastView.rfqCode,'F%'); break; + case "pre_bidding": + // 사전견적(입찰): rfqCode가 B로 시작하는 경우 + typeFilter = + like(rfqsLastView.rfqCode,'B%'); + break; case "itb": // ITB: projectCompany가 있는 경우 typeFilter = @@ -106,6 +142,7 @@ export async function getRfqs(input: GetRfqsSchema) { ilike(rfqsLastView.projectName, s), ilike(rfqsLastView.rfqTitle, s), ilike(rfqsLastView.prNumber, s), + ilike(rfqsLastView.biddingNumber, s), ].filter(Boolean); if (searchConditions.length > 0) { diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index eed3d154..afed9576 100644 --- a/lib/rfq-last/shared/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -345,7 +345,7 @@ export function RfqItemsDialog({ <span className="font-mono text-sm font-medium">{item.materialCode || "-"}</span> {item.acc && ( <span className="text-xs text-muted-foreground font-mono"> - ACC: {item.acc} + {item.acc} </span> )} </div> diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index 62f14579..58c45aa0 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -24,7 +24,7 @@ type NextRouter = ReturnType<typeof useRouter>; interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<RfqsLastView> | null>>; - rfqCategory?: "general" | "itb" | "rfq"; + rfqCategory?: "general" | "itb" | "rfq" | "pre_bidding"; router: NextRouter; } @@ -756,6 +756,342 @@ export function getRfqColumns({ } // ═══════════════════════════════════════════════════════════════ + // 사전견적(입찰) 컬럼 정의 + // ═══════════════════════════════════════════════════════════════ + if (rfqCategory === "pre_bidding") { + return [ + // Checkbox + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} + onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)} + aria-label="select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(v) => row.toggleSelected(!!v)} + aria-label="select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // 견적 No. + { + accessorKey: "rfqCode", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 No." />, + cell: ({ row }) => ( + <span className="font-mono font-medium">{row.original.rfqCode}</span> + ), + size: 120, + }, + + // 입찰 No. (추가) + { + accessorKey: "biddingNumber", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰 No." />, + cell: ({ row }) => ( + <span className="font-mono font-medium">{row.original.biddingNumber || "-"}</span> + ), + size: 120, + }, + + // 상세 - 수정됨 + { + id: "detail", + header: "상세", + cell: ({ row }) => ( + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => router.push(`/evcp/rfq-last/${row.original.id}`)} + > + <Eye className="h-4 w-4" /> + </Button> + ), + size: 60, + }, + + // 견적상태 + { + accessorKey: "status", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적상태" />, + cell: ({ row }) => ( + <Badge variant={getStatusBadgeVariant(row.original.status)}> + {row.original.status} + </Badge> + ), + size: 120, + }, + + // 프로젝트 (프로젝트명) + { + accessorKey: "projectName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트 (프로젝트명)" />, + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-mono text-xs text-muted-foreground"> + {row.original.projectCode} + </span> + <span className="max-w-[200px] truncate" title={row.original.projectName || ""}> + {row.original.projectName || "-"} + </span> + </div> + ), + size: 220, + }, + + // // 시리즈 + // { + // accessorKey: "series", + // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="시리즈" />, + // cell: ({ row }) => { + // const series = row.original.series; + // if (!series) return "-"; + // const label = series === "SS" ? "시리즈 통합" : series === "II" ? "품목 통합" : series; + // return <Badge variant="outline">{label}</Badge>; + // }, + // size: 100, + // }, + + // // 선급 + // { + // accessorKey: "classNo", + // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="선급" />, + // cell: ({ row }) => row.original.classNo || "-", + // size: 80, + // }, + + // 견적명 + { + accessorKey: "rfqTitle", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적명" />, + cell: ({ row }) => ( + <div className="max-w-[200px] truncate" title={row.original.rfqTitle || ""}> + {row.original.rfqTitle || "-"} + </div> + ), + size: 200, + }, + + // 자재그룹 (자재그룹명) + { + accessorKey: "majorItemMaterialDescription", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재그룹 (자재그룹명)" />, + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-mono text-xs text-muted-foreground"> + {row.original.majorItemMaterialCategory} + </span> + <span className="max-w-[150px] truncate" title={row.original.majorItemMaterialDescription || ""}> + {row.original.majorItemMaterialDescription || "-"} + </span> + </div> + ), + size: 180, + }, + + // 자재코드 + { + accessorKey: "itemCode", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재코드" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.original.itemCode || "-"}</span> + ), + size: 100, + }, + // 자재명 + { + accessorKey: "itemName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재명" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.original.itemName || "-"}</span> + ), + size: 100, + }, + + // 견적 자료 + { + id: "rfqDocument", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 자료" />, + cell: ({ row }) => ( + <Button + variant="ghost" + size="sm" + onClick={() => setRowAction({ row, type: "attachment" })} + > + <FileText className="h-4 w-4" /> + </Button> + ), + size: 80, + }, + + // 견적품목수 + { + accessorKey: "prItemsCount", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적품목수" />, + cell: ({ row }) => ( + <Button + variant="ghost" + size="sm" + className="font-mono text-sm p-1 h-auto" + onClick={() => setRowAction({ row, type: "items" })} + > + {row.original.prItemsCount || 0} + </Button> + ), + size: 90, + }, + + // 견적생성일 + { + accessorKey: "createdAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적생성일" />, + cell: ({ row }) => { + const date = row.original.createdAt; + return date ? format(new Date(date), "yyyy-MM-dd") : "-"; + }, + size: 100, + }, + + // 견적발송일 + { + accessorKey: "rfqSendDate", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적발송일" />, + cell: ({ row }) => { + const date = row.original.rfqSendDate; + return date ? format(new Date(date), "yyyy-MM-dd") : "-"; + }, + size: 100, + }, + + // 견적마감일 + { + accessorKey: "dueDate", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적마감일" />, + cell: ({ row }) => { + const date = row.original.dueDate; + if (!date) return "-"; + + const now = new Date(); + const dueDate = new Date(date); + const daysLeft = differenceInDays(dueDate, now); + + // 상태별 스타일과 아이콘 설정 + let statusIcon; + let statusText; + let statusClass; + + if (daysLeft < 0) { + const daysOverdue = Math.abs(daysLeft); + statusIcon = <XCircle className="h-4 w-4" />; + statusText = `${daysOverdue}일 지남`; + statusClass = "text-red-600"; + } else if (daysLeft === 0) { + statusIcon = <AlertTriangle className="h-4 w-4" />; + statusText = "오늘 마감"; + statusClass = "text-orange-600"; + } else if (daysLeft <= 3) { + statusIcon = <AlertCircle className="h-4 w-4" />; + statusText = `${daysLeft}일 남음`; + statusClass = "text-amber-600"; + } else if (daysLeft <= 7) { + statusIcon = <Clock className="h-4 w-4" />; + statusText = `${daysLeft}일 남음`; + statusClass = "text-blue-600"; + } else { + statusIcon = <CheckCircle className="h-4 w-4" />; + statusText = `${daysLeft}일 남음`; + statusClass = "text-green-600"; + } + + return ( + <div className="flex flex-col gap-1"> + <span className="text-sm text-muted-foreground"> + {format(dueDate, "yyyy-MM-dd")} + </span> + <div className={`flex items-center gap-1 ${statusClass}`}> + {statusIcon} + <span className="text-xs font-medium">{statusText}</span> + </div> + </div> + ); + }, + size: 120, + }, + + // 계약기간 (추가) + { + id: "contractPeriod", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약기간" />, + cell: ({ row }) => { + const start = row.original.contractStartDate; + const end = row.original.contractEndDate; + + if (!start && !end) return "-"; + + return ( + <div className="flex flex-col text-xs"> + <span>{start ? format(new Date(start), "yyyy-MM-dd") : "미정"}</span> + <span className="text-muted-foreground">~</span> + <span>{end ? format(new Date(end), "yyyy-MM-dd") : "미정"}</span> + </div> + ); + }, + size: 120, + }, + + // 구매담당자 + { + accessorKey: "picUserName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구매담당자" />, + cell: ({ row }) => { + const name = row.original.picUserName || row.original.picName || "-"; + const picCode = row.original.picCode || ""; + return name === "-" ? "-" : `${name}(${picCode})`; + }, + size: 100, + }, + + // 최종수정일 + { + accessorKey: "updatedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정일" />, + cell: ({ row }) => { + const date = row.original.updatedAt; + return date ? format(new Date(date), "yyyy-MM-dd HH:mm") : "-"; + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "updatedByUserName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정자" />, + cell: ({ row }) => row.original.updatedByUserName || "-", + size: 100, + }, + + // 비고 + { + accessorKey: "remark", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="비고" />, + cell: ({ row }) => row.original.remark || "-", + size: 150, + }, + ]; + } + + // ═══════════════════════════════════════════════════════════════ // 일반견적 컬럼 정의 // ═══════════════════════════════════════════════════════════════ if (rfqCategory === "general") { diff --git a/lib/rfq-last/table/rfq-table.tsx b/lib/rfq-last/table/rfq-table.tsx index 80f1422e..e8dd299d 100644 --- a/lib/rfq-last/table/rfq-table.tsx +++ b/lib/rfq-last/table/rfq-table.tsx @@ -29,7 +29,7 @@ import { RfqItemsDialog } from "../shared/rfq-items-dialog"; interface RfqTableProps { data: Awaited<ReturnType<typeof getRfqs>>; - rfqCategory?: "general" | "itb" | "rfq"; + rfqCategory?: "general" | "itb" | "rfq" | "pre_bidding"; className?: string; } @@ -274,7 +274,7 @@ export function RfqTable({ { id: "vendorCount", label: "업체수", type: "number" }, { id: "dueDate", label: "마감일", type: "date" }, { id: "rfqSendDate", label: "발송일", type: "date" }, - ...(rfqCategory === "general" ? [ + ...(rfqCategory === "general" || rfqCategory === "pre_bidding" ? [ { id: "rfqType", label: "견적 유형", @@ -283,10 +283,14 @@ export function RfqTable({ { label: "단가계약", value: "단가계약" }, { label: "매각계약", value: "매각계약" }, { label: "일반계약", value: "일반계약" }, + ...(rfqCategory === "pre_bidding" ? [{ label: "사전견적(입찰)", value: "사전견적(입찰)" }] : []) ] }, { id: "rfqTitle", label: "견적 제목", type: "text" }, ] as DataTableAdvancedFilterField<RfqsLastView>[] : []), + ...(rfqCategory === "pre_bidding" ? [ + { id: "biddingNumber", label: "입찰 No.", type: "text" }, + ] as DataTableAdvancedFilterField<RfqsLastView>[] : []), ...(rfqCategory === "itb" ? [ { id: "smCode", label: "SM 코드", type: "text" }, ] as DataTableAdvancedFilterField<RfqsLastView>[] : []), @@ -414,6 +418,7 @@ export function RfqTable({ <Badge variant="outline" className="text-sm"> {rfqCategory === "general" ? "일반견적" : + rfqCategory === "pre_bidding" ? "사전견적(입찰)" : rfqCategory === "itb" ? "ITB" : "RFQ"} </Badge> diff --git a/lib/rfq-last/validations.ts b/lib/rfq-last/validations.ts index 6b39d52d..a7f9a405 100644 --- a/lib/rfq-last/validations.ts +++ b/lib/rfq-last/validations.ts @@ -35,12 +35,13 @@ import { RfqLastAttachments } from "@/db/schema"; export const RFQ_CATEGORY_OPTIONS = [ { value: "all", label: "전체" }, { value: "general", label: "일반견적" }, + { value: "pre_bidding", label: "사전견적(입찰)" }, { value: "itb", label: "ITB" }, { value: "rfq", label: "RFQ" }, ]; - + // ============= 메인 검색 파라미터 스키마 ============= - + export const searchParamsRfqCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), @@ -56,7 +57,7 @@ import { RfqLastAttachments } from "@/db/schema"; search: parseAsString.withDefault(""), // RFQ 카테고리 (전체/일반견적/ITB/RFQ) - rfqCategory: parseAsStringEnum(["all", "general", "itb", "rfq"]), + rfqCategory: parseAsStringEnum(["all", "general", "pre_bidding", "itb", "rfq"]), }); // ============= 타입 정의 ============= diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index 281316eb..f8f11327 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -741,7 +741,7 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot <span className="font-mono text-sm font-medium">{prItem?.materialCode || "-"}</span> {prItem?.acc && ( <span className="text-xs text-muted-foreground font-mono"> - ACC: {prItem.acc} + {prItem.acc} </span> )} </div> |
