summaryrefslogtreecommitdiff
path: root/lib/po
diff options
context:
space:
mode:
Diffstat (limited to 'lib/po')
-rw-r--r--lib/po/service.ts4
-rw-r--r--lib/po/table/esign-dialog.tsx112
-rw-r--r--lib/po/table/item-dialog.tsx173
-rw-r--r--lib/po/table/po-table-columns.tsx300
-rw-r--r--lib/po/table/po-table-toolbar-actions.tsx4
-rw-r--r--lib/po/table/po-table.tsx123
-rw-r--r--lib/po/table/sign-request-dialog.tsx359
7 files changed, 762 insertions, 313 deletions
diff --git a/lib/po/service.ts b/lib/po/service.ts
index 5f2e4f35..f62b57fa 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..6834e543 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 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..ad159f12 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,104 @@ 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 });
+
+
+ // 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 +146,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 +159,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 +185,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 +215,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>
)