diff options
Diffstat (limited to 'lib/po')
| -rw-r--r-- | lib/po/service.ts | 4 | ||||
| -rw-r--r-- | lib/po/table/esign-dialog.tsx | 112 | ||||
| -rw-r--r-- | lib/po/table/item-dialog.tsx | 173 | ||||
| -rw-r--r-- | lib/po/table/po-table-columns.tsx | 300 | ||||
| -rw-r--r-- | lib/po/table/po-table-toolbar-actions.tsx | 4 | ||||
| -rw-r--r-- | lib/po/table/po-table.tsx | 125 | ||||
| -rw-r--r-- | lib/po/table/sign-request-dialog.tsx | 359 |
7 files changed, 764 insertions, 313 deletions
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 ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent> + <DialogHeader> + <DialogTitle>Electronic Signature Status</DialogTitle> + <DialogDescription> + {po + ? `Check the e-sign envelopes and signers for contract #${po.contractNo}.` + : "No contract selected." + } + </DialogDescription> + </DialogHeader> + + {/* 본문 영역 */} + {po ? ( + po.envelopes.length === 0 ? ( + <div className="text-sm text-muted-foreground"> + No e-sign envelopes found for this contract. + </div> + ) : ( + <ScrollArea className="h-64 pr-2"> + {po.envelopes.map((envelope, idx) => { + return ( + <div key={envelope.id} className="mb-4"> + {/* Envelope Header */} + <div className="flex items-center justify-between mb-1"> + <span className="font-semibold text-sm"> + Envelope #{envelope.id} + </span> + <Badge variant="outline"> + {envelope.envelopeStatus} + </Badge> + </div> + <div className="text-xs text-muted-foreground"> + Updated at: {envelope.updatedAt} + </div> + + {/* Signers */} + <Separator className="my-2" /> + {envelope.signers && envelope.signers.length > 0 ? ( + <div className="space-y-2"> + {envelope.signers.map((signer) => ( + <div key={signer.id} className="px-2 py-1 bg-muted rounded"> + <div className="flex justify-between"> + <span className="font-medium text-sm"> + {signer.signerName} ({signer.signerEmail}) + </span> + <Badge variant="secondary"> + {signer.signerStatus} + </Badge> + </div> + {signer.signedAt && ( + <div className="text-xs text-muted-foreground"> + Signed at: {signer.signedAt} + </div> + )} + </div> + ))} + </div> + ) : ( + <div className="text-sm text-muted-foreground"> + No signers found. + </div> + )} + </div> + ) + })} + </ScrollArea> + ) + ) : ( + <div className="text-sm text-muted-foreground"> + Please select a contract to see its e-sign status. + </div> + )} + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + Close + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ 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 ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[700px] max-h-[90vh]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Package className="h-5 w-5" /> + Contract Items + </DialogTitle> + <DialogDescription> + {po + ? `Item list for contract #${po.contractNo} - ${po.contractName}` + : "No contract selected." + } + </DialogDescription> + </DialogHeader> + + {/* Main content */} + {po ? ( + po.items.length === 0 ? ( + <div className="text-sm text-muted-foreground flex items-center justify-center p-8"> + <Info className="mr-2 h-4 w-4" /> + No items found for this contract. + </div> + ) : ( + <div className="overflow-hidden"> + <div className="flex justify-between items-center my-2"> + <div className="text-sm font-medium"> + Total Items: <Badge variant="outline">{po.items.length}</Badge> + </div> + <div className="text-sm text-muted-foreground"> + Currency: <Badge variant="secondary">{po.currency || "Default"}</Badge> + </div> + </div> + + <ScrollArea className="h-[350px] pr-4 rounded-md border" style={{height:450}}> + <div className="p-4 space-y-4"> + {po.items.map((item) => ( + <Card key={item.id} className="shadow-sm hover:shadow-md transition-shadow duration-200"> + <CardHeader className="pb-2 bg-muted/30"> + <div className="flex justify-between items-center"> + <CardTitle className="text-base flex items-center gap-2"> + <Hash className="h-4 w-4" /> + Item #{item.itemId} + </CardTitle> + {item.quantity > 1 && ( + <Badge className="ml-2"> + Qty: {item.quantity} + </Badge> + )} + </div> + </CardHeader> + <CardContent className="pt-3 pb-4"> + {item.description && ( + <div className="mb-3 text-sm"> + <div className="font-medium mb-1 text-muted-foreground flex items-center"> + <Info className="mr-2 h-4 w-4" /> + Description + </div> + <div className="pl-6">{item.description}</div> + </div> + )} + + <Table> + <TableBody> + <TableRow> + <TableCell className="py-2 font-medium">Unit Price</TableCell> + <TableCell className="py-2 text-right"> + {item.unitPrice !== null ? ( + <div className="flex items-center justify-end gap-1"> + <DollarSign className="h-3 w-3 text-muted-foreground" /> + {formatCurrency(item.unitPrice, po.currency??"KRW")} + </div> + ) : "-"} + </TableCell> + </TableRow> + + {item.taxRate !== null && ( + <TableRow> + <TableCell className="py-2 font-medium">Tax Rate</TableCell> + <TableCell className="py-2 text-right">{item.taxRate}%</TableCell> + </TableRow> + )} + + {item.taxAmount !== null && ( + <TableRow> + <TableCell className="py-2 font-medium">Tax Amount</TableCell> + <TableCell className="py-2 text-right">{formatCurrency(item.taxAmount, po.currency??"KRW")}</TableCell> + </TableRow> + )} + + {item.totalLineAmount !== null && ( + <TableRow className="border-t border-t-primary/20"> + <TableCell className="py-2 font-medium">Total Amount</TableCell> + <TableCell className="py-2 text-right font-semibold"> + {formatCurrency(item.totalLineAmount, po.currency??"KRW")} + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + + {item.remark && ( + <div className="mt-3 text-sm"> + <div className="font-medium mb-1 text-muted-foreground flex items-center"> + <Tag className="mr-2 h-4 w-4" /> + Remark + </div> + <div className="pl-6 italic text-muted-foreground">{item.remark}</div> + </div> + )} + + <div className="mt-3 pt-2 border-t text-xs text-muted-foreground flex items-center"> + <Clock className="mr-1 h-3 w-3" /> + Updated: {formatDate(item.updatedAt)} + </div> + </CardContent> + </Card> + ))} + </div> + </ScrollArea> + </div> + ) + ) : ( + <div className="text-sm text-muted-foreground flex items-center justify-center p-8"> + Please select a contract to see its items. + </div> + )} + + <DialogFooter className="mt-2"> + <Button variant="outline" onClick={() => onOpenChange(false)}> + Close + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ 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<React.SetStateAction<DataTableRowAction<ContractDetail> | null>> + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ContractDetailParsed> | null>> } /** * tanstack table column definitions with nested headers */ -export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ContractDetail>[] { +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ContractDetailParsed>[] { // ---------------------------------------------------------------- // 1) select column (checkbox) - if needed // ---------------------------------------------------------------- @@ -33,164 +42,229 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Contrac // ---------------------------------------------------------------- // 2) actions column (buttons for item info and signature request) // ---------------------------------------------------------------- - const actionsColumn: ColumnDef<ContractDetail> = { + const actionsColumn: ColumnDef<ContractDetailParsed> = { id: "actions", enableHiding: false, + header: () => <div className="text-center">Actions</div>, cell: function Cell({ row }) { // Check if this contract already has a signature envelope const hasSignature = row.original.hasSignature; - + const contract = row.original; + return ( - <div className="flex items-center space-x-1"> - {/* Item Info Button */} + <div className="flex items-center justify-center gap-2"> + {/* Items Button - Visually distinct with badge showing count */} <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button - variant="ghost" - size="icon" + variant="outline" + size="sm" + className="flex items-center gap-1 h-8 px-2" onClick={() => setRowAction({ row, type: "items" })} > - <InfoIcon className="h-4 w-4" aria-hidden="true" /> + <FileTextIcon className="h-3.5 w-3.5" aria-hidden="true" /> + Items + {contract.items && contract.items.length > 0 && ( + <Badge variant="secondary" className="ml-1 text-xs h-5 px-1.5"> + {contract.items.length} + </Badge> + )} </Button> </TooltipTrigger> <TooltipContent> - View Item Info + View contract items and details </TooltipContent> </Tooltip> </TooltipProvider> - - {/* Signature Request Button - only show if no signature exists */} - {!hasSignature && ( + + {/* Signature related actions */} + {hasSignature ? ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "esign-detail" })} + > + <FileSignatureIcon className="h-3.5 w-3.5 mr-1" aria-hidden="true" /> + View Signatures + </Button> + </TooltipTrigger> + <TooltipContent> + View signature status and details + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) : ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button - variant="ghost" - size="icon" + variant="outline" + size="sm" + className="h-8 px-2 text-blue-600 border-blue-200 hover:bg-blue-50" onClick={() => setRowAction({ row, type: "signature" })} > - <PenIcon className="h-4 w-4" aria-hidden="true" /> + <SendIcon className="h-3.5 w-3.5 mr-1" aria-hidden="true" /> + Request Signatures </Button> </TooltipTrigger> <TooltipContent> - Request Electronic Signature + Send electronic signature requests </TooltipContent> </Tooltip> </TooltipProvider> )} + + {/* Alternative: Dropdown menu for more actions */} + {/* + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon"> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>Actions</DropdownMenuLabel> + <DropdownMenuItem onClick={() => setRowAction({ row, type: "items" })}> + <FileTextIcon className="h-4 w-4 mr-2" /> + View Items + </DropdownMenuItem> + <DropdownMenuSeparator /> + {hasSignature ? ( + <DropdownMenuItem onClick={() => setRowAction({ row, type: "esign-detail" })}> + <FileSignatureIcon className="h-4 w-4 mr-2" /> + View Signatures + </DropdownMenuItem> + ) : ( + <DropdownMenuItem onClick={() => setRowAction({ row, type: "signature" })}> + <SendIcon className="h-4 w-4 mr-2" /> + Request Signatures + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + */} </div> ); }, - 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<ContractDetail>[] } - const groupMap: Record<string, ColumnDef<ContractDetail>[]> = {}; - - // (1) JSON config를 읽어서 ColumnDef를 생성하는 부분 (일부 발췌) -poColumnsConfig.forEach((cfg) => { - const groupName = cfg.group || "_noGroup" - if (!groupMap[groupName]) { - groupMap[groupName] = [] - } - - let childCol: ColumnDef<ContractDetail> - - if (cfg.type === "custom" && cfg.customType === "esignStatus") { - // ======================================== - // (2) 전자서명 전용 커스텀 컬럼 - // ======================================== - childCol = { - id: cfg.id, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title={cfg.label} /> - ), - // 여기서 row.original.envelopes 등 활용하여 최신 전자서명 상태 표시 - cell: ({ row }) => { - const data = row.original - if (!data.envelopes || data.envelopes.length === 0) { + // 3-1) groupMap: { [groupName]: ColumnDef<ContractDetailParsed>[] } + const groupMap: Record<string, ColumnDef<ContractDetailParsed>[]> = {}; + + // (1) JSON config를 읽어서 ColumnDef를 생성하는 부분 (일부 발췌) + poColumnsConfig.forEach((cfg) => { + const groupName = cfg.group || "_noGroup" + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + let childCol: ColumnDef<ContractDetailParsed> + + if (cfg.type === "custom") { + // ======================================== + // (2) 전자서명 전용 커스텀 컬럼 + // ======================================== + childCol = { + id: cfg.id, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title={cfg.label} /> + ), + // 여기서 row.original.envelopes 등 활용하여 최신 전자서명 상태 표시 + cell: ({ row }) => { + const data = row.original + if (!data.envelopes || data.envelopes.length === 0) { + return ( + <div className="text-sm text-gray-500"> + No E-Sign + </div> + ) + } + + // 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 <span className="text-gray-500">No Status</span> + } + + const colorMap: Record<string, string> = { + completed: "text-green-600", + sent: "text-blue-600", + voided: "text-red-600", + }; + + const colorClass = colorMap[status] ?? "text-gray-700"; + return ( - <div className="text-sm text-gray-500"> - No E-Sign + <div className="flex items-center"> + <span + onClick={() => setRowAction({ row, type: "esign-detail" })} + className={`${colorClass} cursor-pointer flex items-center`} + > + {status} + <ExternalLinkIcon className="ml-1 h-3 w-3" /> + </span> </div> ) - } - - // 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<string, string> = { - 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 }) => ( + <DataTableColumnHeaderSimple column={column} title={cfg.label} /> + ), + 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 ( - <Button - onClick={() => { - // 다이얼로그 열기 등 - // 예: setRowAction({ row, type: "esign-detail" }) - setRowAction({ row, type: "esign-detail" }) - }} - className={`underline underline-offset-2 ${colorClass}`} - > - {status} - </Button> - ) - }, - meta: { - excelHeader: cfg.excelHeader, - group: cfg.group, - type: cfg.type, - }, - } - } else { - // ======================================== - // (3) 일반 컬럼 (type: text/date/number 등) - // ======================================== - childCol = { - accessorKey: cfg.id, - enableResizing: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title={cfg.label} /> - ), - 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<ContractDetail>[] = []; + const nestedColumns: ColumnDef<ContractDetailParsed>[] = []; // 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<ContractDetail> + table: Table<ContractDetailParsed> } 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<DataTableRowAction<ContractDetail> | 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<DataTableRowAction<ContractDetailParsed> | null>(null) + // State for signature request modal const [signatureModalOpen, setSignatureModalOpen] = React.useState(false) - const [selectedContract, setSelectedContract] = React.useState<ContractDetail | null>(null) - + const [signatureDetailOpen, setSignatureDetailOpen] = React.useState(false) + const [itemsOpen, setItemsOpen] = React.useState(false) + const [selectedContract, setSelectedContract] = React.useState<ContractDetailParsed | null>(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<ContractDetail>[] = [ + const filterFields: DataTableFilterField<ContractDetailParsed>[] = [ // Your existing filter fields ] - const advancedFilterFields: DataTableAdvancedFilterField<ContractDetail>[] = [ + const advancedFilterFields: DataTableAdvancedFilterField<ContractDetailParsed>[] = [ { 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) { <PoTableToolbarActions table={table} /> </DataTableAdvancedToolbar> </DataTable> - + + <EsignStatusDialog + open={signatureDetailOpen} + onOpenChange={setSignatureDetailOpen} + po={selectedContract} + /> + + <ItemsDialog + open={itemsOpen} + onOpenChange={setItemsOpen} + po={selectedContract} + /> + + {/* Enhanced Dual Signature Request Modal */} {selectedContract && ( <SignatureRequestModal diff --git a/lib/po/table/sign-request-dialog.tsx b/lib/po/table/sign-request-dialog.tsx index f70e5e33..38e3e0f9 100644 --- a/lib/po/table/sign-request-dialog.tsx +++ b/lib/po/table/sign-request-dialog.tsx @@ -45,7 +45,8 @@ import { import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" -import { ContractDetail } from "@/db/schema/contract" +import { ScrollArea } from "@/components/ui/scroll-area" +import { ContractDetail, ContractDetailParsed } from "@/db/schema/contract" import { getVendorContacts } from "../service" // Type for vendor contact @@ -92,7 +93,7 @@ interface SigningParty { // Updated interface to accept multiple signers interface SignatureRequestModalProps { - contract: ContractDetail + contract: ContractDetailParsed open: boolean onOpenChange: (open: boolean) => void onSubmit: ( @@ -217,7 +218,7 @@ export function SignatureRequestModal({ return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[600px]"> + <DialogContent className="sm:max-w-[600px] max-h-[90vh]"> <DialogHeader> <DialogTitle>Request Electronic Signatures</DialogTitle> <DialogDescription> @@ -225,185 +226,191 @@ export function SignatureRequestModal({ </DialogDescription> </DialogHeader> - <Form {...form}> - <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> - <Accordion type="multiple" defaultValue={["requester", "vendor"]} className="w-full"> - {/* Requester Signature Section */} - <AccordionItem value="requester"> - <div className="flex items-center space-x-2"> - <FormField - control={form.control} - name="includeRequesterSigner" - render={({ field }) => ( - <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> - <FormControl> - <Checkbox - checked={field.value} - onCheckedChange={field.onChange} + + <Form {...form}> + <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> + <ScrollArea className="pr-4" style={{height:500}}> + <Accordion type="multiple" defaultValue={["requester", "vendor"]} className="w-full"> + {/* Requester Signature Section */} + <AccordionItem value="requester"> + <div className="flex items-center space-x-2"> + <FormField + control={form.control} + name="includeRequesterSigner" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <AccordionTrigger className="hover:no-underline ml-2"> + <div className="text-sm font-medium">Requester Signature</div> + </AccordionTrigger> + </FormItem> + )} + /> + </div> + <AccordionContent> + {form.watch("includeRequesterSigner") && ( + <Card className="border-none shadow-none"> + <CardContent className="p-0 space-y-4"> + <FormField + control={form.control} + name="requesterEmail" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Email</FormLabel> + <FormControl> + <Input placeholder="email@example.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <AccordionTrigger className="hover:no-underline ml-2"> - <div className="text-sm font-medium">Requester Signature</div> - </AccordionTrigger> - </FormItem> - )} - /> - </div> - <AccordionContent> - {form.watch("includeRequesterSigner") && ( - <Card className="border-none shadow-none"> - <CardContent className="p-0 space-y-4"> - <FormField - control={form.control} - name="requesterEmail" - render={({ field }) => ( - <FormItem> - <FormLabel>Signer Email</FormLabel> - <FormControl> - <Input placeholder="email@example.com" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="requesterName" - render={({ field }) => ( - <FormItem> - <FormLabel>Signer Name</FormLabel> - <FormControl> - <Input placeholder="Full Name" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="requesterPosition" - render={({ field }) => ( - <FormItem> - <FormLabel>Signer Position</FormLabel> - <FormControl> - <Input placeholder="e.g. CEO, Manager" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - )} - </AccordionContent> - </AccordionItem> - - {/* Vendor Signature Section */} - <AccordionItem value="vendor"> - <div className="flex items-center space-x-2"> - <FormField - control={form.control} - name="includeVendorSigner" - render={({ field }) => ( - <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> - <FormControl> - <Checkbox - checked={field.value} - onCheckedChange={field.onChange} + + <FormField + control={form.control} + name="requesterName" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Name</FormLabel> + <FormControl> + <Input placeholder="Full Name" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <AccordionTrigger className="hover:no-underline ml-2"> - <div className="text-sm font-medium">Vendor Signature</div> - </AccordionTrigger> - </FormItem> - )} - /> - </div> - <AccordionContent> - {form.watch("includeVendorSigner") && ( - <Card className="border-none shadow-none"> - <CardContent className="p-0 space-y-4"> - <FormField - control={form.control} - name="vendorContactId" - render={({ field }) => ( - <FormItem> - <FormLabel>Select Vendor Contact</FormLabel> - <Select - onValueChange={handleVendorContactSelect} - defaultValue={field.value?.toString()} - > + + <FormField + control={form.control} + name="requesterPosition" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Position</FormLabel> <FormControl> - <SelectTrigger> - <SelectValue placeholder="Select a contact" /> - </SelectTrigger> + <Input placeholder="e.g. CEO, Manager" {...field} /> </FormControl> - <SelectContent> - {vendorContacts.length > 0 ? ( - vendorContacts.map((contact) => ( - <SelectItem - key={contact.id} - value={contact.id.toString()} - > - {contact.contactName} {contact.isPrimary ? "(Primary)" : ""} + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + )} + </AccordionContent> + </AccordionItem> + + {/* Vendor Signature Section */} + <AccordionItem value="vendor"> + <div className="flex items-center space-x-2"> + <FormField + control={form.control} + name="includeVendorSigner" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <AccordionTrigger className="hover:no-underline ml-2"> + <div className="text-sm font-medium">Vendor Signature</div> + </AccordionTrigger> + </FormItem> + )} + /> + </div> + <AccordionContent> + {form.watch("includeVendorSigner") && ( + <Card className="border-none shadow-none"> + <CardContent className="p-0 space-y-4"> + <FormField + control={form.control} + name="vendorContactId" + render={({ field }) => ( + <FormItem> + <FormLabel>Select Vendor Contact</FormLabel> + <Select + onValueChange={handleVendorContactSelect} + defaultValue={field.value?.toString()} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select a contact" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {vendorContacts.length > 0 ? ( + vendorContacts.map((contact) => ( + <SelectItem + key={contact.id} + value={contact.id.toString()} + > + {contact.contactName} {contact.isPrimary ? "(Primary)" : ""} + </SelectItem> + )) + ) : ( + <SelectItem value="none" disabled> + No contacts available </SelectItem> - )) - ) : ( - <SelectItem value="none" disabled> - No contacts available - </SelectItem> - )} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> + )} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* Display selected contact info (read-only) */} + {selectedVendorContact && ( + <> + <FormItem className="pb-2"> + <FormLabel>Contact Email</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactEmail} + </div> + </FormItem> + + <FormItem className="pb-2"> + <FormLabel>Contact Name</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactName} + </div> + </FormItem> + + <FormItem className="pb-2"> + <FormLabel>Contact Position</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactPosition || "N/A"} + </div> + </FormItem> + </> )} - /> - - {/* Display selected contact info (read-only) */} - {selectedVendorContact && ( - <> - <FormItem className="pb-2"> - <FormLabel>Contact Email</FormLabel> - <div className="p-2 border rounded-md bg-muted"> - {selectedVendorContact.contactEmail} - </div> - </FormItem> - - <FormItem className="pb-2"> - <FormLabel>Contact Name</FormLabel> - <div className="p-2 border rounded-md bg-muted"> - {selectedVendorContact.contactName} - </div> - </FormItem> - - <FormItem className="pb-2"> - <FormLabel>Contact Position</FormLabel> - <div className="p-2 border rounded-md bg-muted"> - {selectedVendorContact.contactPosition || "N/A"} - </div> - </FormItem> - </> - )} - </CardContent> - </Card> - )} - </AccordionContent> - </AccordionItem> - </Accordion> - - <DialogFooter> - <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> - Cancel - </Button> - <Button type="submit" disabled={isSubmitting}> - {isSubmitting ? "Sending..." : "Send Requests"} - </Button> - </DialogFooter> - </form> - </Form> + </CardContent> + </Card> + )} + </AccordionContent> + </AccordionItem> + </Accordion> + </ScrollArea> + + <div className="pt-4 pb-2"> + <DialogFooter className="pt-2"> + <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> + Cancel + </Button> + <Button type="submit" disabled={isSubmitting}> + {isSubmitting ? "Sending..." : "Send Requests"} + </Button> + </DialogFooter> + </div> + </form> + </Form> + </DialogContent> </Dialog> ) |
