diff options
Diffstat (limited to 'lib/vendor-document-list/plant/upload/columns.tsx')
| -rw-r--r-- | lib/vendor-document-list/plant/upload/columns.tsx | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/lib/vendor-document-list/plant/upload/columns.tsx b/lib/vendor-document-list/plant/upload/columns.tsx new file mode 100644 index 00000000..c0f17afc --- /dev/null +++ b/lib/vendor-document-list/plant/upload/columns.tsx @@ -0,0 +1,379 @@ +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { formatDate, formatDateTime } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { DataTableRowAction } from "@/types/table" +import { StageSubmissionView } from "@/db/schema" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Ellipsis, + Upload, + Eye, + RefreshCw, + CheckCircle2, + XCircle, + AlertCircle, + Clock +} from "lucide-react" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<StageSubmissionView> | null>> +} + +export function getColumns({ + setRowAction, +}: GetColumnsProps): ColumnDef<StageSubmissionView>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "docNumber", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Doc Number" /> + ), + cell: ({ row }) => { + const vendorDocNumber = row.original.vendorDocNumber + return ( + <div className="space-y-1"> + <div className="font-medium">{row.getValue("docNumber")}</div> + {vendorDocNumber && ( + <div className="text-xs text-muted-foreground">{vendorDocNumber}</div> + )} + </div> + ) + }, + size: 150, + }, + { + accessorKey: "documentTitle", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Document Title" /> + ), + cell: ({ row }) => ( + <div className="max-w-[300px] truncate" title={row.getValue("documentTitle")}> + {row.getValue("documentTitle")} + </div> + ), + size: 250, + }, + { + accessorKey: "projectCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Project" /> + ), + cell: ({ row }) => ( + <Badge variant="outline">{row.getValue("projectCode")}</Badge> + ), + size: 100, + }, + { + accessorKey: "stageName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Stage" /> + ), + cell: ({ row }) => { + const stageName = row.getValue("stageName") as string + const stageStatus = row.original.stageStatus + const stageOrder = row.original.stageOrder + + return ( + <div className="space-y-1"> + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {stageOrder ? `#${stageOrder}` : ""} + </Badge> + <span className="text-sm">{stageName}</span> + </div> + {stageStatus && ( + <Badge + variant={ + stageStatus === "COMPLETED" ? "success" : + stageStatus === "IN_PROGRESS" ? "default" : + stageStatus === "REJECTED" ? "destructive" : + "secondary" + } + className="text-xs" + > + {stageStatus} + </Badge> + )} + </div> + ) + }, + size: 200, + }, + { + accessorKey: "stagePlanDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Due Date" /> + ), + cell: ({ row }) => { + const planDate = row.getValue("stagePlanDate") as Date | null + const isOverdue = row.original.isOverdue + const daysUntilDue = row.original.daysUntilDue + + if (!planDate) return <span className="text-muted-foreground">-</span> + + return ( + <div className="space-y-1"> + <div className={isOverdue ? "text-destructive font-medium" : ""}> + {formatDate(planDate)} + </div> + {daysUntilDue !== null && ( + <div className="text-xs"> + {isOverdue ? ( + <Badge variant="destructive" className="gap-1"> + <AlertCircle className="h-3 w-3" /> + {Math.abs(daysUntilDue)} days overdue + </Badge> + ) : daysUntilDue === 0 ? ( + <Badge variant="warning" className="gap-1"> + <Clock className="h-3 w-3" /> + Due today + </Badge> + ) : ( + <span className="text-muted-foreground"> + {daysUntilDue} days remaining + </span> + )} + </div> + )} + </div> + ) + }, + size: 150, + }, + { + accessorKey: "latestSubmissionStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Submission Status" /> + ), + cell: ({ row }) => { + const status = row.getValue("latestSubmissionStatus") as string | null + const reviewStatus = row.original.latestReviewStatus + const revisionNumber = row.original.latestRevisionNumber + const revisionCode = row.original.latestRevisionCode + + if (!status) { + return ( + <Badge variant="outline" className="gap-1"> + <AlertCircle className="h-3 w-3" /> + Not submitted + </Badge> + ) + } + + return ( + <div className="space-y-1"> + <Badge + variant={ + reviewStatus === "APPROVED" ? "success" : + reviewStatus === "REJECTED" ? "destructive" : + status === "SUBMITTED" ? "default" : + "secondary" + } + > + {reviewStatus || status} + </Badge> + {revisionCode !== null &&( + <div className="text-xs text-muted-foreground"> + {revisionCode} + </div> + )} + </div> + ) + }, + size: 150, + }, + { + id: "syncStatus", + accessorKey: "latestSyncStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Sync Status" /> + ), + cell: ({ row }) => { + const syncStatus = row.getValue("latestSyncStatus") as string | null + const syncProgress = row.original.syncProgress + const requiresSync = row.original.requiresSync + + if (!syncStatus || syncStatus === "pending") { + if (requiresSync) { + return ( + <Badge variant="outline" className="gap-1"> + <Clock className="h-3 w-3" /> + Pending + </Badge> + ) + } + return <span className="text-muted-foreground">-</span> + } + + return ( + <div className="space-y-2"> + <Badge + variant={ + syncStatus === "synced" ? "success" : + syncStatus === "failed" ? "destructive" : + syncStatus === "syncing" ? "default" : + "secondary" + } + className="gap-1" + > + {syncStatus === "syncing" && <RefreshCw className="h-3 w-3 animate-spin" />} + {syncStatus === "synced" && <CheckCircle2 className="h-3 w-3" />} + {syncStatus === "failed" && <XCircle className="h-3 w-3" />} + {syncStatus} + </Badge> + {syncProgress !== null && syncProgress !== undefined && syncStatus === "syncing" && ( + <Progress value={syncProgress} className="h-1.5 w-20" /> + )} + </div> + ) + }, + size: 120, + }, + { + accessorKey: "totalFiles", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Files" /> + ), + cell: ({ row }) => { + const totalFiles = row.getValue("totalFiles") as number + const syncedFiles = row.original.syncedFilesCount + + if (!totalFiles) return <span className="text-muted-foreground">0</span> + + return ( + <div className="text-sm"> + {syncedFiles !== null && syncedFiles !== undefined ? ( + <span>{syncedFiles}/{totalFiles}</span> + ) : ( + <span>{totalFiles}</span> + )} + </div> + ) + }, + size: 80, + }, + // { + // accessorKey: "vendorName", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="Vendor" /> + // ), + // cell: ({ row }) => { + // const vendorName = row.getValue("vendorName") as string + // const vendorCode = row.original.vendorCode + + // return ( + // <div className="space-y-1"> + // <div className="text-sm">{vendorName}</div> + // {vendorCode && ( + // <div className="text-xs text-muted-foreground">{vendorCode}</div> + // )} + // </div> + // ) + // }, + // size: 150, + // }, + { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const requiresSubmission = row.original.requiresSubmission + const requiresSync = row.original.requiresSync + const latestSubmissionId = row.original.latestSubmissionId + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-7 p-0" + > + <Ellipsis className="size-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-48"> + {requiresSubmission && ( + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "upload" })} + className="gap-2" + > + <Upload className="h-4 w-4" /> + Upload Documents + </DropdownMenuItem> + )} + + {latestSubmissionId && ( + <> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "view" })} + className="gap-2" + > + <Eye className="h-4 w-4" /> + View Submission + </DropdownMenuItem> + + {requiresSync && ( + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "sync" })} + className="gap-2" + > + <RefreshCw className="h-4 w-4" /> + Retry Sync + </DropdownMenuItem> + )} + </> + )} + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "history" })} + className="gap-2" + > + <Clock className="h-4 w-4" /> + View History + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, + } + ] +}
\ No newline at end of file |
