From b8e8328b1ffffb80bf4ebb776a4a24e5680fc5bc Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 28 Mar 2025 00:42:08 +0000 Subject: 테이블 칼럼 리사이즈 및 핀 충돌 해결 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/po/service.ts | 4 + lib/po/table/esign-dialog.tsx | 112 ++++++++++ lib/po/table/item-dialog.tsx | 173 ++++++++++++++ lib/po/table/po-table-columns.tsx | 300 +++++++++++++++---------- lib/po/table/po-table-toolbar-actions.tsx | 4 +- lib/po/table/po-table.tsx | 125 +++++++++-- lib/po/table/sign-request-dialog.tsx | 359 +++++++++++++++--------------- 7 files changed, 764 insertions(+), 313 deletions(-) create mode 100644 lib/po/table/esign-dialog.tsx create mode 100644 lib/po/table/item-dialog.tsx (limited to 'lib') diff --git a/lib/po/service.ts b/lib/po/service.ts index f697bd58..a6c53d9c 100644 --- a/lib/po/service.ts +++ b/lib/po/service.ts @@ -136,6 +136,7 @@ export async function getPOs(input: GetPOSchema) { } const countResult = await countBuilder; + total = countResult[0]?.count || 0; } catch (queryErr) { console.error("Query execution failed:", queryErr); @@ -144,6 +145,9 @@ export async function getPOs(input: GetPOSchema) { const pageCount = Math.ceil(total / input.perPage); + // console.log(data) + // console.log(pageCount) + return { data, pageCount }; } catch (err) { // More detailed error logging diff --git a/lib/po/table/esign-dialog.tsx b/lib/po/table/esign-dialog.tsx new file mode 100644 index 00000000..ee517a4a --- /dev/null +++ b/lib/po/table/esign-dialog.tsx @@ -0,0 +1,112 @@ +"use client" + +import * as React from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Badge } from "@/components/ui/badge" +// import { PenIcon, InfoIcon } from "lucide-react" // 필요하다면 + +import type { ContractDetailParsed } from "@/db/schema/contract" + +/** + * E-sign Status Dialog Props + */ +interface EsignStatusDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + po: ContractDetailParsed | null; // 실제로 표시할 계약 데이터 +} + +export function EsignStatusDialog({ + open, + onOpenChange, + po, +}: EsignStatusDialogProps) { + console.log(open) + console.log(po) + return ( + + + + Electronic Signature Status + + {po + ? `Check the e-sign envelopes and signers for contract #${po.contractNo}.` + : "No contract selected." + } + + + + {/* 본문 영역 */} + {po ? ( + po.envelopes.length === 0 ? ( +
+ No e-sign envelopes found for this contract. +
+ ) : ( + + {po.envelopes.map((envelope, idx) => { + return ( +
+ {/* Envelope Header */} +
+ + Envelope #{envelope.id} + + + {envelope.envelopeStatus} + +
+
+ Updated at: {envelope.updatedAt} +
+ + {/* Signers */} + + {envelope.signers && envelope.signers.length > 0 ? ( +
+ {envelope.signers.map((signer) => ( +
+
+ + {signer.signerName} ({signer.signerEmail}) + + + {signer.signerStatus} + +
+ {signer.signedAt && ( +
+ Signed at: {signer.signedAt} +
+ )} +
+ ))} +
+ ) : ( +
+ No signers found. +
+ )} +
+ ) + })} +
+ ) + ) : ( +
+ Please select a contract to see its e-sign status. +
+ )} + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/po/table/item-dialog.tsx b/lib/po/table/item-dialog.tsx new file mode 100644 index 00000000..a6690e75 --- /dev/null +++ b/lib/po/table/item-dialog.tsx @@ -0,0 +1,173 @@ +"use client" + +import * as React from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Package, Info, DollarSign, Tag, Clock, Hash } from "lucide-react" + +import type { ContractDetailParsed } from "@/db/schema/contract" + +interface ItemsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: ContractDetailParsed | null +} + +export function ItemsDialog({ open, onOpenChange, po }: ItemsDialogProps) { + console.log(po) + + // Format currency with appropriate symbol + const formatCurrency = (value: number | null, currency?: string) => { + if (value === null) return '-'; + const currencySymbol = currency === 'USD' ? '$' : currency || ''; + return `${currencySymbol}${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }; + + // Format date to a readable format + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleString(); + }; + + return ( + + + + + + Contract Items + + + {po + ? `Item list for contract #${po.contractNo} - ${po.contractName}` + : "No contract selected." + } + + + + {/* Main content */} + {po ? ( + po.items.length === 0 ? ( +
+ + No items found for this contract. +
+ ) : ( +
+
+
+ Total Items: {po.items.length} +
+
+ Currency: {po.currency || "Default"} +
+
+ + +
+ {po.items.map((item) => ( + + +
+ + + Item #{item.itemId} + + {item.quantity > 1 && ( + + Qty: {item.quantity} + + )} +
+
+ + {item.description && ( +
+
+ + Description +
+
{item.description}
+
+ )} + + + + + Unit Price + + {item.unitPrice !== null ? ( +
+ + {formatCurrency(item.unitPrice, po.currency??"KRW")} +
+ ) : "-"} +
+
+ + {item.taxRate !== null && ( + + Tax Rate + {item.taxRate}% + + )} + + {item.taxAmount !== null && ( + + Tax Amount + {formatCurrency(item.taxAmount, po.currency??"KRW")} + + )} + + {item.totalLineAmount !== null && ( + + Total Amount + + {formatCurrency(item.totalLineAmount, po.currency??"KRW")} + + + )} +
+
+ + {item.remark && ( +
+
+ + Remark +
+
{item.remark}
+
+ )} + +
+ + Updated: {formatDate(item.updatedAt)} +
+
+
+ ))} +
+
+
+ ) + ) : ( +
+ Please select a contract to see its items. +
+ )} + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/po/table/po-table-columns.tsx b/lib/po/table/po-table-columns.tsx index a13b2acf..6517b9b3 100644 --- a/lib/po/table/po-table-columns.tsx +++ b/lib/po/table/po-table-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { InfoIcon, PenIcon } from "lucide-react" +import { InfoIcon, FileTextIcon, SendIcon, FileSignatureIcon, MoreHorizontalIcon, ExternalLinkIcon } from "lucide-react" import { formatDate } from "@/lib/utils" import { Button } from "@/components/ui/button" @@ -13,19 +13,28 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Badge } from "@/components/ui/badge" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { poColumnsConfig } from "@/config/poColumnsConfig" -import { ContractDetail } from "@/db/schema/contract" +import { ContractDetailParsed } from "@/db/schema/contract" interface GetColumnsProps { - setRowAction: React.Dispatch | null>> + setRowAction: React.Dispatch | null>> } /** * tanstack table column definitions with nested headers */ -export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { // ---------------------------------------------------------------- // 1) select column (checkbox) - if needed // ---------------------------------------------------------------- @@ -33,164 +42,229 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef = { + const actionsColumn: ColumnDef = { id: "actions", enableHiding: false, + header: () =>
Actions
, cell: function Cell({ row }) { // Check if this contract already has a signature envelope const hasSignature = row.original.hasSignature; - + const contract = row.original; + return ( -
- {/* Item Info Button */} +
+ {/* Items Button - Visually distinct with badge showing count */} - View Item Info + View contract items and details - - {/* Signature Request Button - only show if no signature exists */} - {!hasSignature && ( + + {/* Signature related actions */} + {hasSignature ? ( + + + + + + + View signature status and details + + + + ) : ( - Request Electronic Signature + Send electronic signature requests )} + + {/* Alternative: Dropdown menu for more actions */} + {/* + + + + + + Actions + setRowAction({ row, type: "items" })}> + + View Items + + + {hasSignature ? ( + setRowAction({ row, type: "esign-detail" })}> + + View Signatures + + ) : ( + setRowAction({ row, type: "signature" })}> + + Request Signatures + + )} + + + */}
); }, - size: 80, // Increased width to accommodate both buttons + size: 340, // Adjusted for multiple buttons + minSize:280 }; // ---------------------------------------------------------------- // 3) Regular columns grouped by group name // ---------------------------------------------------------------- - // 3-1) groupMap: { [groupName]: ColumnDef[] } - const groupMap: Record[]> = {}; - - // (1) JSON config를 읽어서 ColumnDef를 생성하는 부분 (일부 발췌) -poColumnsConfig.forEach((cfg) => { - const groupName = cfg.group || "_noGroup" - if (!groupMap[groupName]) { - groupMap[groupName] = [] - } - - let childCol: ColumnDef - - if (cfg.type === "custom" && cfg.customType === "esignStatus") { - // ======================================== - // (2) 전자서명 전용 커스텀 컬럼 - // ======================================== - childCol = { - id: cfg.id, - header: ({ column }) => ( - - ), - // 여기서 row.original.envelopes 등 활용하여 최신 전자서명 상태 표시 - cell: ({ row }) => { - const data = row.original - if (!data.envelopes || data.envelopes.length === 0) { + // 3-1) groupMap: { [groupName]: ColumnDef[] } + const groupMap: Record[]> = {}; + + // (1) JSON config를 읽어서 ColumnDef를 생성하는 부분 (일부 발췌) + poColumnsConfig.forEach((cfg) => { + const groupName = cfg.group || "_noGroup" + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + let childCol: ColumnDef + + if (cfg.type === "custom") { + // ======================================== + // (2) 전자서명 전용 커스텀 컬럼 + // ======================================== + childCol = { + id: cfg.id, + header: ({ column }) => ( + + ), + // 여기서 row.original.envelopes 등 활용하여 최신 전자서명 상태 표시 + cell: ({ row }) => { + const data = row.original + if (!data.envelopes || data.envelopes.length === 0) { + return ( +
+ No E-Sign +
+ ) + } + + // envelopes가 여러 개 있으면 최신(가장 최근 updatedAt) 가져오기 + const sorted = [...data.envelopes].sort((a, b) => { + const dateA = new Date(a.updatedAt) + const dateB = new Date(b.updatedAt) + return dateB.getTime() - dateA.getTime() + }) + const latest = sorted[0] + + const status = latest.envelopeStatus; + + if (!status) { + // status가 null이면, 기본 색상이나 별도 표시 + return No Status + } + + const colorMap: Record = { + completed: "text-green-600", + sent: "text-blue-600", + voided: "text-red-600", + }; + + const colorClass = colorMap[status] ?? "text-gray-700"; + return ( -
- No E-Sign +
+ setRowAction({ row, type: "esign-detail" })} + className={`${colorClass} cursor-pointer flex items-center`} + > + {status} + +
) - } - - // envelopes가 여러 개 있으면 최신(가장 최근 updatedAt) 가져오기 - const sorted = [...data.envelopes].sort((a, b) => { - const dateA = new Date(a.updatedAt) - const dateB = new Date(b.updatedAt) - return dateB.getTime() - dateA.getTime() - }) - const latest = sorted[0] - - // 상태에 따라 다른 UI 색상/아이콘 - const status = latest.envelopeStatus // "sent", "completed", ... - const colorMap: Record = { - completed: "text-green-600", - sent: "text-blue-600", - voided: "text-red-600", + }, + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + } + } else { + // ======================================== + // (3) 일반 컬럼 (type: text/date/number 등) + // ======================================== + childCol = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + // 날짜 포맷, 숫자 포맷 등 처리 + if (cfg.type === "date") { + const dateVal = cell.getValue() as Date + return formatDate(dateVal) + } // ... - } - const colorClass = colorMap[status] || "text-gray-700" - - return ( - - ) - }, - meta: { - excelHeader: cfg.excelHeader, - group: cfg.group, - type: cfg.type, - }, - } - } else { - // ======================================== - // (3) 일반 컬럼 (type: text/date/number 등) - // ======================================== - childCol = { - accessorKey: cfg.id, - enableResizing: true, - header: ({ column }) => ( - - ), - meta: { - excelHeader: cfg.excelHeader, - group: cfg.group, - type: cfg.type, - }, - cell: ({ row, cell }) => { - // 날짜 포맷, 숫자 포맷 등 처리 - if (cfg.type === "date") { - const dateVal = cell.getValue() as Date - return formatDate(dateVal) - } - // ... - return row.getValue(cfg.id) ?? "" - }, + return row.getValue(cfg.id) ?? "" + }, + } } - } - groupMap[groupName].push(childCol) -}) + groupMap[groupName].push(childCol) + }) // ---------------------------------------------------------------- // 3-2) Create actual parent columns (groups) from the groupMap // ---------------------------------------------------------------- - const nestedColumns: ColumnDef[] = []; + const nestedColumns: ColumnDef[] = []; // Order can be fixed by pre-defining group order or sorting // Here we just use Object.entries order diff --git a/lib/po/table/po-table-toolbar-actions.tsx b/lib/po/table/po-table-toolbar-actions.tsx index e6c8e79a..23507751 100644 --- a/lib/po/table/po-table-toolbar-actions.tsx +++ b/lib/po/table/po-table-toolbar-actions.tsx @@ -7,12 +7,12 @@ import { toast } from "sonner" import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" -import { ContractDetail } from "@/db/schema/contract" +import { ContractDetailParsed } from "@/db/schema/contract" interface ItemsTableToolbarActionsProps { - table: Table + table: Table } export function PoTableToolbarActions({ table }: ItemsTableToolbarActionsProps) { diff --git a/lib/po/table/po-table.tsx b/lib/po/table/po-table.tsx index 49fbdda4..9175037b 100644 --- a/lib/po/table/po-table.tsx +++ b/lib/po/table/po-table.tsx @@ -15,9 +15,11 @@ import { toast } from "sonner" import { getPOs, requestSignatures } from "../service" import { getColumns } from "./po-table-columns" -import { ContractDetail } from "@/db/schema/contract" +import { ContractDetail, ContractDetailParsed, Envelope } from "@/db/schema/contract" import { PoTableToolbarActions } from "./po-table-toolbar-actions" import { SignatureRequestModal } from "./sign-request-dialog" +import { EsignStatusDialog } from "./esign-dialog" +import { ItemsDialog } from "./item-dialog" interface ItemsTableProps { promises: Promise< @@ -36,40 +38,106 @@ interface SigningParty { vendorContactId?: number; } +export function transformContractData(data: ContractDetail[]): ContractDetailParsed[] { + return data.map((contract) => { + let parsedEnvelopes: Envelope[] = []; + let parsedItems = []; + + try { + // Check if envelopes is a string that needs parsing + if (typeof contract.envelopes === "string") { + parsedEnvelopes = JSON.parse(contract.envelopes); + } else if (Array.isArray(contract.envelopes)) { + // If it's already an array, use it directly + parsedEnvelopes = contract.envelopes as unknown as Envelope[]; + } + + // Check if items is a string that needs parsing + if (typeof contract.items === "string") { + parsedItems = JSON.parse(contract.items); + } else if (Array.isArray(contract.items)) { + // If it's already an array, use it directly + parsedItems = contract.items; + } + } catch (err) { + console.error("Error parsing JSON", err); + } + + // Return a new object with all properties from the original contract + // but replace envelopes and items with their parsed versions + return { + ...contract, + envelopes: parsedEnvelopes, + items: parsedItems + } as ContractDetailParsed; + }); +} export function PoListsTable({ promises }: ItemsTableProps) { const { featureFlags } = useFeatureFlags() - - const [{ data, pageCount }] = - React.use(promises) - - const [rowAction, setRowAction] = - React.useState | null>(null) - + + const [rawData, setRawData] = React.useState<{ + data: ContractDetail[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + // Add state for transformed data + const [transformedData, setTransformedData] = React.useState<{ + data: ContractDetailParsed[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + console.log(rawData) + console.log(transformedData) + + // Load raw data from promises + React.useEffect(() => { + promises.then(([result]) => { + console.log(result.data) + + setRawData(result); + // Transform the data + setTransformedData({ + data: transformContractData(result.data), + pageCount: result.pageCount + }); + }); + }, [promises]); + + const [rowAction, setRowAction] = + React.useState | null>(null) + // State for signature request modal const [signatureModalOpen, setSignatureModalOpen] = React.useState(false) - const [selectedContract, setSelectedContract] = React.useState(null) - + const [signatureDetailOpen, setSignatureDetailOpen] = React.useState(false) + const [itemsOpen, setItemsOpen] = React.useState(false) + const [selectedContract, setSelectedContract] = React.useState(null) + // Handle row actions React.useEffect(() => { if (!rowAction) return - + if (rowAction.type === "signature") { // Open signature request modal with the selected contract setSelectedContract(rowAction.row.original) setSignatureModalOpen(true) setRowAction(null) } else if (rowAction.type === "items") { - // Existing handler for "items" action type - // Your existing code here + setSelectedContract(rowAction.row.original) + setItemsOpen(true) setRowAction(null) + } else if (rowAction.type === "esign-detail") { + setSignatureDetailOpen(true) + setSelectedContract(rowAction.row.original) + console.log("E-sign details for contract:", rowAction.row.original); + setRowAction(null); } }, [rowAction]) - + const columns = React.useMemo( () => getColumns({ setRowAction }), [setRowAction] ) - + // Updated handler to work with multiple signers const handleSignatureRequest = async ( values: { signers: SigningParty[] }, @@ -80,7 +148,7 @@ export function PoListsTable({ promises }: ItemsTableProps) { contractId, signers: values.signers }); - + // Handle the result if (result.success) { toast.success(result.message || "Signature requests sent successfully"); @@ -93,11 +161,11 @@ export function PoListsTable({ promises }: ItemsTableProps) { } } - const filterFields: DataTableFilterField[] = [ + const filterFields: DataTableFilterField[] = [ // Your existing filter fields ] - const advancedFilterFields: DataTableAdvancedFilterField[] = [ + const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "contractNo", label: "Contract No", @@ -119,11 +187,11 @@ export function PoListsTable({ promises }: ItemsTableProps) { type: "date", }, ] - + const { table } = useDataTable({ - data, + data: transformedData.data, // Use the transformed data columns, - pageCount, + pageCount: transformedData.pageCount, filterFields, enablePinning: true, enableAdvancedFilter: true, @@ -149,7 +217,20 @@ export function PoListsTable({ promises }: ItemsTableProps) { - + + + + + + {/* Enhanced Dual Signature Request Modal */} {selectedContract && ( void onSubmit: ( @@ -217,7 +218,7 @@ export function SignatureRequestModal({ return ( - + Request Electronic Signatures @@ -225,185 +226,191 @@ export function SignatureRequestModal({ -
- - - {/* Requester Signature Section */} - -
- ( - - - + + + + {/* Requester Signature Section */} + +
+ ( + + + + + +
Requester Signature
+
+
+ )} + /> +
+ + {form.watch("includeRequesterSigner") && ( + + + ( + + Signer Email + + + + + + )} /> -
- -
Requester Signature
-
-
- )} - /> -
- - {form.watch("includeRequesterSigner") && ( - - - ( - - Signer Email - - - - - - )} - /> - - ( - - Signer Name - - - - - - )} - /> - - ( - - Signer Position - - - - - - )} - /> - - - )} - -
- - {/* Vendor Signature Section */} - -
- ( - - - ( + + Signer Name + + + + + + )} /> - - -
Vendor Signature
-
-
- )} - /> -
- - {form.watch("includeVendorSigner") && ( - - - ( - - Select Vendor Contact - - - {vendorContacts.length > 0 ? ( - vendorContacts.map((contact) => ( - - {contact.contactName} {contact.isPrimary ? "(Primary)" : ""} + + + )} + /> + + + )} + +
+ + {/* Vendor Signature Section */} + +
+ ( + + + + + +
Vendor Signature
+
+
+ )} + /> +
+ + {form.watch("includeVendorSigner") && ( + + + ( + + Select Vendor Contact + - - + )} + + + + + )} + /> + + {/* Display selected contact info (read-only) */} + {selectedVendorContact && ( + <> + + Contact Email +
+ {selectedVendorContact.contactEmail} +
+
+ + + Contact Name +
+ {selectedVendorContact.contactName} +
+
+ + + Contact Position +
+ {selectedVendorContact.contactPosition || "N/A"} +
+
+ )} - /> - - {/* Display selected contact info (read-only) */} - {selectedVendorContact && ( - <> - - Contact Email -
- {selectedVendorContact.contactEmail} -
-
- - - Contact Name -
- {selectedVendorContact.contactName} -
-
- - - Contact Position -
- {selectedVendorContact.contactPosition || "N/A"} -
-
- - )} -
-
- )} -
-
-
- - - - - -
- + + + )} + + + + + +
+ + + + +
+ + +
) -- cgit v1.2.3 From f3e640b666c57f133c229d2742fb214c586d21e4 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 28 Mar 2025 02:39:59 +0000 Subject: 스키마 조정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema/contract.ts | 101 +++++++++++++++++++++++++++++++++++++- lib/po/table/po-table-columns.tsx | 2 +- lib/po/table/po-table.tsx | 2 - 3 files changed, 101 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/db/schema/contract.ts b/db/schema/contract.ts index cc1f8c57..1d628442 100644 --- a/db/schema/contract.ts +++ b/db/schema/contract.ts @@ -331,4 +331,103 @@ export const contractsDetailView = pgView("contracts_detail_view").as((qb) => { export type ContractDetailParsed = Omit & { envelopes: Envelope[] items: ContractItemParsed[] - } \ No newline at end of file + } + + // ============ poa (Purchase Order Amendment) ============ +export const poa = pgTable("poa", { + // 주 키 + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // Form code는 원본과 동일하게 유지 + contractNo: varchar("contract_no", { length: 100 }).notNull(), + + // 원본 PO 참조 + originalContractNo: varchar("original_contract_no", { length: 100 }) + .notNull() + .references(() => contracts.contractNo, { onDelete: "cascade" }), + + // 원본 계약 정보 + projectId: integer("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + vendorId: integer("vendor_id") + .notNull() + .references(() => vendors.id, { onDelete: "cascade" }), + originalContractName: varchar("original_contract_name", { length: 255 }).notNull(), + originalStatus: varchar("original_status", { length: 50 }).notNull(), + + // 변경된 납품 조건 + deliveryTerms: text("delivery_terms"), // 변경된 납품 조건 + deliveryDate: date("delivery_date"), // 변경된 납품 기한 + deliveryLocation: varchar("delivery_location", { length: 255 }), // 변경된 납품 장소 + + // 변경된 가격/금액 관련 + currency: varchar("currency", { length: 10 }), // 변경된 통화 + totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 변경된 총 금액 + discount: numeric("discount", { precision: 12, scale: 2 }), // 변경된 할인 + tax: numeric("tax", { precision: 12, scale: 2 }), // 변경된 세금 + shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 변경된 배송비 + netTotal: numeric("net_total", { precision: 12, scale: 2 }), // 변경된 순 총액 + + // 변경 사유 + changeReason: text("change_reason"), + + // 승인 상태 + approvalStatus: varchar("approval_status", { length: 50 }).default("PENDING"), + + // 생성/수정 시각 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}) + +// 타입 추론 +export type POA = typeof poa.$inferSelect + +// ============ poa_detail_view ============ +export const poaDetailView = pgView("poa_detail_view").as((qb) => { + return qb + .select({ + // POA primary information + id: poa.id, + contractNo: poa.contractNo, + projectId: contracts.projectId, + vendorId: contracts.vendorId, + changeReason: poa.changeReason, + approvalStatus: poa.approvalStatus, + + // Original PO information + originalContractName: sql`${contracts.contractName}`.as('original_contract_name'), + originalStatus: sql`${contracts.status}`.as('original_status'), + originalStartDate: sql`${contracts.startDate}`.as('original_start_date'), + originalEndDate: sql`${contracts.endDate}`.as('original_end_date'), + + // Changed delivery details + deliveryTerms: poa.deliveryTerms, + deliveryDate: poa.deliveryDate, + deliveryLocation: poa.deliveryLocation, + + // Changed financial information + currency: poa.currency, + totalAmount: poa.totalAmount, + discount: poa.discount, + tax: poa.tax, + shippingFee: poa.shippingFee, + netTotal: poa.netTotal, + + // Timestamps + createdAt: poa.createdAt, + updatedAt: poa.updatedAt, + + // Electronic signature status + hasSignature: sql`EXISTS ( + SELECT 1 + FROM ${contractEnvelopes} + WHERE ${contractEnvelopes.contractId} = ${poa.id} + )`.as('has_signature'), + }) + .from(poa) + .leftJoin(contracts, eq(poa.contractNo, contracts.contractNo)) +}); + +// Type inference for the view +export type POADetail = typeof poaDetailView.$inferSelect; \ No newline at end of file diff --git a/lib/po/table/po-table-columns.tsx b/lib/po/table/po-table-columns.tsx index 6517b9b3..6834e543 100644 --- a/lib/po/table/po-table-columns.tsx +++ b/lib/po/table/po-table-columns.tsx @@ -52,7 +52,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef +
{/* Items Button - Visually distinct with badge showing count */} diff --git a/lib/po/table/po-table.tsx b/lib/po/table/po-table.tsx index 9175037b..ad159f12 100644 --- a/lib/po/table/po-table.tsx +++ b/lib/po/table/po-table.tsx @@ -86,8 +86,6 @@ export function PoListsTable({ promises }: ItemsTableProps) { pageCount: number; }>({ data: [], pageCount: 0 }); - console.log(rawData) - console.log(transformedData) // Load raw data from promises React.useEffect(() => { -- cgit v1.2.3