Spaces:
Sleeping
Sleeping
| import { useMemo, useState, useCallback } from "react"; | |
| import { hatcheryApplications, farmApplications, getStatusColor, formatStatus } from "@/data/dummyData"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { ResponsiveTable } from "@/components/ui/responsive-table"; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow, | |
| } from "@/components/ui/table"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Button } from "@/components/ui/button"; | |
| import { ApprovalFlow } from "@/components/ApprovalFlow"; | |
| import { applyCertificationOverrides, updateCertificationOverride, EntityType } from "@/lib/certificationLocal"; | |
| interface FlatAppRow { | |
| id: string; | |
| applicationNo: string; | |
| entityName: string; | |
| entityType: "hatchery" | "farm"; | |
| ownerName: string; | |
| status: string; | |
| currentStage: string; | |
| submittedDate: string; | |
| } | |
| const JdApprovalQueue = () => { | |
| const baseRows = useMemo<FlatAppRow[]>(() => { | |
| const hatcheryRows: FlatAppRow[] = hatcheryApplications.map((h) => ({ | |
| id: h.id, | |
| applicationNo: h.applicationNo, | |
| entityName: h.hatcheryName, | |
| entityType: "hatchery" as const, | |
| ownerName: h.ownerName, | |
| status: h.status, | |
| currentStage: h.currentStage, | |
| submittedDate: h.submittedDate, | |
| })); | |
| const farmRows: FlatAppRow[] = farmApplications.map((f) => ({ | |
| id: f.id, | |
| applicationNo: f.applicationNo, | |
| entityName: f.farmName, | |
| entityType: "farm" as const, | |
| ownerName: f.farmerName, | |
| status: f.status, | |
| currentStage: f.currentStage, | |
| submittedDate: f.submittedDate, | |
| })); | |
| const all = [...hatcheryRows, ...farmRows]; | |
| return all; | |
| }, []); | |
| const [refreshToken, setRefreshToken] = useState(0); | |
| const [selectedRow, setSelectedRow] = useState<FlatAppRow | null>(null); | |
| const allRows = useMemo<FlatAppRow[]>( | |
| () => applyCertificationOverrides(baseRows), | |
| [baseRows, refreshToken], | |
| ); | |
| const rows = useMemo<FlatAppRow[]>( | |
| () => | |
| allRows.filter( | |
| (row) => | |
| row.currentStage === "jd_approval" && | |
| row.status !== "rejected" && | |
| row.status !== "certified", | |
| ), | |
| [allRows], | |
| ); | |
| const stats = useMemo(() => { | |
| const total = allRows.length; | |
| const atCc = allRows.filter( | |
| (row) => row.currentStage === "cc_review" && row.status !== "rejected", | |
| ).length; | |
| const atJd = allRows.filter( | |
| (row) => | |
| row.currentStage === "jd_approval" && | |
| row.status !== "rejected" && | |
| row.status !== "certified", | |
| ).length; | |
| const atDirector = allRows.filter( | |
| (row) => | |
| row.currentStage === "director_approval" && | |
| row.status !== "rejected" && | |
| row.status !== "certified", | |
| ).length; | |
| const certified = allRows.filter((row) => row.status === "certified").length; | |
| return { total, atCc, atJd, atDirector, certified }; | |
| }, [allRows]); | |
| const handleJdAction = useCallback( | |
| (row: FlatAppRow, action: "forward" | "reject") => { | |
| const entityType = row.entityType as EntityType; | |
| if (action === "forward") { | |
| updateCertificationOverride(entityType, row.id, { | |
| currentStage: "director_approval", | |
| status: "pending_approval", | |
| }); | |
| } else { | |
| updateCertificationOverride(entityType, row.id, { | |
| status: "rejected", | |
| }); | |
| } | |
| setRefreshToken((v) => v + 1); | |
| }, | |
| [], | |
| ); | |
| return ( | |
| <div className="space-y-6 animate-fade-in"> | |
| <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
| <div> | |
| <h1 className="text-2xl font-bold font-display">JD Approval Queue</h1> | |
| <p className="text-muted-foreground text-sm"> | |
| Manage applications waiting at Joint Director approval stage. | |
| </p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <Card className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-primary/10" /> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Total in certification pipeline</p> | |
| <p className="text-2xl font-bold">{stats.total}</p> | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-orange-100" /> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Waiting at JD</p> | |
| <p className="text-2xl font-bold text-orange-600">{stats.atJd}</p> | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-blue-100" /> | |
| <div> | |
| <p className="text-sm text-muted-foreground">With Director</p> | |
| <p className="text-2xl font-bold text-blue-600">{stats.atDirector}</p> | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-emerald-100" /> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Certified</p> | |
| <p className="text-2xl font-bold text-emerald-600">{stats.certified}</p> | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-[minmax(0,2fr)_minmax(0,1.1fr)] gap-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-base">Applications pending JD approval</CardTitle> | |
| </CardHeader> | |
| <CardContent className="p-0 sm:p-6"> | |
| <ResponsiveTable> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Application</TableHead> | |
| <TableHead>Entity</TableHead> | |
| <TableHead>Type</TableHead> | |
| <TableHead>Owner</TableHead> | |
| <TableHead>Submitted</TableHead> | |
| <TableHead>Stage</TableHead> | |
| <TableHead>Status</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {rows.map((row) => ( | |
| <TableRow | |
| key={`${row.entityType}-${row.id}`} | |
| className="table-row-hover cursor-pointer" | |
| onClick={() => setSelectedRow(row)} | |
| > | |
| <TableCell className="font-mono text-sm font-medium"> | |
| {row.applicationNo} | |
| </TableCell> | |
| <TableCell>{row.entityName}</TableCell> | |
| <TableCell> | |
| <Badge variant="outline" className="capitalize"> | |
| {row.entityType} | |
| </Badge> | |
| </TableCell> | |
| <TableCell>{row.ownerName}</TableCell> | |
| <TableCell className="text-sm text-muted-foreground"> | |
| {row.submittedDate} | |
| </TableCell> | |
| <TableCell> | |
| <Badge variant="outline" className="capitalize"> | |
| {formatStatus(row.currentStage)} | |
| </Badge> | |
| </TableCell> | |
| <TableCell> | |
| <Badge className={getStatusColor(row.status)}> | |
| {formatStatus(row.status)} | |
| </Badge> | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| {rows.length === 0 && ( | |
| <TableRow> | |
| <TableCell | |
| colSpan={7} | |
| className="py-8 text-center text-sm text-muted-foreground" | |
| > | |
| No applications currently at JD approval stage. | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </TableBody> | |
| </Table> | |
| </ResponsiveTable> | |
| </CardContent> | |
| </Card> | |
| <Card className="h-full"> | |
| <CardHeader> | |
| <CardTitle className="text-base"> | |
| {selectedRow ? `Application ${selectedRow.applicationNo}` : "Application details"} | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| {!selectedRow ? ( | |
| <p className="text-sm text-muted-foreground"> | |
| Select an application from the list to view details and take action. | |
| </p> | |
| ) : ( | |
| <> | |
| <div className="grid grid-cols-1 gap-2 text-sm"> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Entity</span> | |
| <span className="font-medium text-right"> | |
| {selectedRow.entityName} | |
| </span> | |
| </div> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Type</span> | |
| <span className="font-medium capitalize text-right"> | |
| {selectedRow.entityType} | |
| </span> | |
| </div> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Owner</span> | |
| <span className="font-medium text-right"> | |
| {selectedRow.ownerName} | |
| </span> | |
| </div> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Submitted</span> | |
| <span className="font-medium text-right"> | |
| {selectedRow.submittedDate} | |
| </span> | |
| </div> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Current stage</span> | |
| <span className="font-medium text-right"> | |
| {formatStatus(selectedRow.currentStage)} | |
| </span> | |
| </div> | |
| <div className="flex justify-between gap-4"> | |
| <span className="text-muted-foreground">Status</span> | |
| <span className="font-medium text-right"> | |
| {formatStatus(selectedRow.status)} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <p className="text-xs text-muted-foreground uppercase tracking-wide"> | |
| Approval flow | |
| </p> | |
| <ApprovalFlow currentStage={selectedRow.currentStage} /> | |
| </div> | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => { | |
| handleJdAction(selectedRow, "forward"); | |
| setSelectedRow(null); | |
| }} | |
| > | |
| Approve & Send to Director | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| className="text-red-600 border-red-200" | |
| onClick={() => { | |
| handleJdAction(selectedRow, "reject"); | |
| setSelectedRow(null); | |
| }} | |
| > | |
| Reject at JD | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default JdApprovalQueue; | |