| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- "use client";
- import { useEffect, useState } from "react";
- import {
- ColumnDef,
- flexRender,
- getCoreRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
- SortingState,
- } from "@tanstack/react-table";
- import * as XLSX from "xlsx";
- import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogPrimitive,
- } from "@/components/ui/dialog";
- import { X } from "lucide-react";
- import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from "@/components/ui/table";
- import { Badge } from "@/components/ui/badge";
- import { Button } from "@/components/ui/button";
- import {
- getTerraTechFacilitySummary,
- TerraTechSummaryRow,
- } from "@/app/actions/imports";
- import {
- Fuel,
- AlertTriangle,
- Download,
- ChevronLeft,
- ChevronRight,
- } from "lucide-react";
- interface TerraTechSummaryDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- importId: number;
- }
- interface SummaryData {
- importId: number;
- importName: string;
- layoutName: string;
- rows: TerraTechSummaryRow[];
- }
- function formatNumber(
- value: number | string | null | undefined,
- decimals: number = 2,
- ): string {
- if (value === null || value === undefined) return "-";
- const num = typeof value === "string" ? parseFloat(value) : value;
- if (isNaN(num)) return "-";
- return num.toLocaleString("en-US", {
- minimumFractionDigits: decimals,
- maximumFractionDigits: decimals,
- });
- }
- function formatPercent(value: number | string | null | undefined): string {
- if (value === null || value === undefined) return "-";
- const num = typeof value === "string" ? parseFloat(value) : value;
- if (isNaN(num)) return "-";
- return `${num.toFixed(1)}%`;
- }
- const columns: ColumnDef<TerraTechSummaryRow>[] = [
- {
- accessorKey: "wellName",
- header: "Well Name",
- cell: ({ row }) => (
- <div className="font-medium">{row.getValue("wellName") || "-"}</div>
- ),
- },
- {
- accessorKey: "corpId",
- header: "CorpID",
- cell: ({ row }) => row.getValue("corpId") || "-",
- },
- {
- accessorKey: "facilityId",
- header: "Facility ID",
- cell: ({ row }) => {
- const value = row.getValue("facilityId") as string | null;
- return value || <span className="text-amber-500">-</span>;
- },
- },
- {
- accessorKey: "gas",
- header: "Gas",
- cell: ({ row }) => (
- <div className="text-right">{formatNumber(row.getValue("gas"))}</div>
- ),
- },
- {
- accessorKey: "oil",
- header: "Oil",
- cell: ({ row }) => (
- <div className="text-right">{formatNumber(row.getValue("oil"))}</div>
- ),
- },
- {
- accessorKey: "water",
- header: "Water",
- cell: ({ row }) => (
- <div className="text-right">{formatNumber(row.getValue("water"))}</div>
- ),
- },
- {
- accessorKey: "state",
- header: "State",
- cell: ({ row }) => row.getValue("state") || "-",
- },
- {
- accessorKey: "county",
- header: "County",
- cell: ({ row }) => {
- const value = row.getValue("county") as string | null;
- return value || <span className="text-amber-500">-</span>;
- },
- },
- {
- accessorKey: "daysQ1",
- header: "Days Q1",
- cell: ({ row }) => (
- <div className="text-right">
- {formatNumber(row.getValue("daysQ1"), 0)}
- </div>
- ),
- },
- {
- accessorKey: "daysQ2",
- header: "Days Q2",
- cell: ({ row }) => (
- <div className="text-right">
- {formatNumber(row.getValue("daysQ2"), 0)}
- </div>
- ),
- },
- {
- accessorKey: "daysQ3",
- header: "Days Q3",
- cell: ({ row }) => (
- <div className="text-right">
- {formatNumber(row.getValue("daysQ3"), 0)}
- </div>
- ),
- },
- {
- accessorKey: "daysQ4",
- header: "Days Q4",
- cell: ({ row }) => (
- <div className="text-right">
- {formatNumber(row.getValue("daysQ4"), 0)}
- </div>
- ),
- },
- {
- accessorKey: "daysQ1Pct",
- header: "Q1 %",
- cell: ({ row }) => (
- <div className="text-right">
- {formatPercent(row.getValue("daysQ1Pct"))}
- </div>
- ),
- },
- {
- accessorKey: "daysQ2Pct",
- header: "Q2 %",
- cell: ({ row }) => (
- <div className="text-right">
- {formatPercent(row.getValue("daysQ2Pct"))}
- </div>
- ),
- },
- {
- accessorKey: "daysQ3Pct",
- header: "Q3 %",
- cell: ({ row }) => (
- <div className="text-right">
- {formatPercent(row.getValue("daysQ3Pct"))}
- </div>
- ),
- },
- {
- accessorKey: "daysQ4Pct",
- header: "Q4 %",
- cell: ({ row }) => (
- <div className="text-right">
- {formatPercent(row.getValue("daysQ4Pct"))}
- </div>
- ),
- },
- {
- accessorKey: "gasCorr",
- header: "Gas-corr",
- cell: ({ row }) => (
- <div className="text-right">{formatNumber(row.getValue("gasCorr"))}</div>
- ),
- },
- {
- accessorKey: "oilCorr",
- header: "Oil-corr",
- cell: ({ row }) => (
- <div className="text-right">{formatNumber(row.getValue("oilCorr"))}</div>
- ),
- },
- {
- accessorKey: "waterCorr",
- header: "Water-corr",
- cell: ({ row }) => (
- <div className="text-right">
- {formatNumber(row.getValue("waterCorr"))}
- </div>
- ),
- },
- {
- accessorKey: "isMissingFacilityId",
- header: "Missing Facility ID",
- cell: ({ row }) => {
- const value = row.getValue("isMissingFacilityId");
- return value ? (
- <Badge variant="destructive" className="text-xs">
- Yes
- </Badge>
- ) : null;
- },
- },
- {
- accessorKey: "isMissingCounty",
- header: "Missing County",
- cell: ({ row }) => {
- const value = row.getValue("isMissingCounty");
- return value ? (
- <Badge variant="destructive" className="text-xs">
- Yes
- </Badge>
- ) : null;
- },
- },
- ];
- function SummaryTable({
- data,
- importName,
- }: {
- data: TerraTechSummaryRow[];
- importName: string;
- }) {
- const [sorting, setSorting] = useState<SortingState>([
- { id: "wellName", desc: false },
- ]);
- const table = useReactTable({
- data,
- columns,
- state: {
- sorting,
- },
- onSortingChange: setSorting,
- getCoreRowModel: getCoreRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getSortedRowModel: getSortedRowModel(),
- initialState: {
- pagination: {
- pageSize: 30,
- },
- },
- });
- function exportToExcel() {
- // Prepare data for Excel export
- const exportData = data.map((row) => ({
- "Well Name": row.wellName || "",
- CorpID: row.corpId || "",
- "Facility ID": row.facilityId || "",
- Gas: row.gas,
- Oil: row.oil,
- Water: row.water,
- State: row.state || "",
- County: row.county || "",
- "Days Q1": row.daysQ1,
- "Days Q2": row.daysQ2,
- "Days Q3": row.daysQ3,
- "Days Q4": row.daysQ4,
- "Q1 %": row.daysQ1Pct,
- "Q2 %": row.daysQ2Pct,
- "Q3 %": row.daysQ3Pct,
- "Q4 %": row.daysQ4Pct,
- "Gas-corr": row.gasCorr,
- "Oil-corr": row.oilCorr,
- "Water-corr": row.waterCorr,
- "Missing Facility ID": row.isMissingFacilityId || "",
- "Missing County": row.isMissingCounty || "",
- }));
- const worksheet = XLSX.utils.json_to_sheet(exportData);
- const workbook = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(workbook, worksheet, "Facility Summary");
- // Generate filename with import name and date
- const date = new Date().toISOString().split("T")[0];
- const filename = `${importName.replace(/[^a-z0-9]/gi, "_")}_Summary_${date}.xlsx`;
- XLSX.writeFile(workbook, filename);
- }
- return (
- <div className="space-y-4">
- <div className="flex justify-between items-center">
- <div className="text-sm text-muted-foreground">
- {data.length} records
- </div>
- <Button
- variant="outline"
- size="sm"
- onClick={exportToExcel}
- className="bg-green-50 hover:bg-green-100 text-green-700 border-green-300"
- >
- <Download className="h-4 w-4 mr-2" />
- Export to Excel
- </Button>
- </div>
- <div className="rounded-md border bg-white dark:bg-gray-800 dark:border-gray-700 overflow-auto flex flex-col max-h-[60vh]">
- <div className="overflow-x-auto">
- <Table>
- <TableHeader className="sticky top-0 bg-white dark:bg-gray-800 z-10">
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id} className="dark:border-gray-700">
- {headerGroup.headers.map((header) => (
- <TableHead
- key={header.id}
- onClick={header.column.getToggleSortingHandler()}
- className={
- header.column.getCanSort()
- ? "cursor-pointer select-none whitespace-nowrap bg-gray-50 dark:bg-gray-900"
- : "whitespace-nowrap bg-gray-50 dark:bg-gray-900"
- }
- >
- <div className="flex items-center gap-1">
- {flexRender(
- header.column.columnDef.header,
- header.getContext(),
- )}
- {header.column.getIsSorted() && (
- <span>
- {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
- </span>
- )}
- </div>
- </TableHead>
- ))}
- </TableRow>
- ))}
- </TableHeader>
- <TableBody>
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
- <TableRow
- key={row.id}
- data-state={row.getIsSelected() && "selected"}
- className="dark:border-gray-700"
- >
- {row.getVisibleCells().map((cell) => (
- <TableCell key={cell.id} className="whitespace-nowrap">
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext(),
- )}
- </TableCell>
- ))}
- </TableRow>
- ))
- ) : (
- <TableRow>
- <TableCell
- colSpan={columns.length}
- className="h-24 text-center"
- >
- No summary data available for this import.
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </div>
- </div>
- <div className="flex items-center justify-between px-4 pb-4">
- <div className="text-sm text-muted-foreground">
- Showing {table.getRowModel().rows.length} of {data.length} records
- </div>
- <div className="flex items-center space-x-2">
- <Button
- variant="outline"
- size="sm"
- onClick={() => table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
- <ChevronLeft className="h-4 w-4 mr-1" />
- Previous
- </Button>
- <div className="text-sm font-medium">
- Page {table.getState().pagination.pageIndex + 1} of{" "}
- {table.getPageCount()}
- </div>
- <Button
- variant="outline"
- size="sm"
- onClick={() => table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
- Next
- <ChevronRight className="h-4 w-4 ml-1" />
- </Button>
- </div>
- </div>
- </div>
- );
- }
- export function TerraTechSummaryDialog({
- open,
- onOpenChange,
- importId,
- }: TerraTechSummaryDialogProps) {
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
- useEffect(() => {
- if (open && importId) {
- loadSummary();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open, importId]);
- async function loadSummary() {
- setLoading(true);
- setError(null);
- try {
- const result = await getTerraTechFacilitySummary(importId);
- if (result.success && result.data) {
- setSummaryData(result.data);
- } else {
- setError(result.error || "Failed to load summary");
- }
- } catch {
- setError("An unexpected error occurred");
- } finally {
- setLoading(false);
- }
- }
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-[95vw] max-h-[90vh] w-full overflow-y-auto dark:bg-gray-800 dark:border-gray-700">
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-full p-2 opacity-70 transition-opacity hover:opacity-100 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-ring">
- <X className="h-5 w-5" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2 text-gray-900 dark:text-white">
- <div className="bg-amber-500 w-8 h-8 rounded-full flex items-center justify-center text-white">
- <Fuel className="w-4 h-4" />
- </div>
- Facility Summary
- {summaryData && (
- <Badge variant="outline" className="ml-2">
- {summaryData.importName}
- </Badge>
- )}
- </DialogTitle>
- </DialogHeader>
- {loading && (
- <div className="flex justify-center py-12">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500"></div>
- </div>
- )}
- {error && (
- <div className="flex flex-col items-center justify-center py-12 text-red-500">
- <AlertTriangle className="h-8 w-8 mb-2" />
- <p>{error}</p>
- </div>
- )}
- {!loading && !error && summaryData && (
- <SummaryTable
- data={summaryData.rows}
- importName={summaryData.importName}
- />
- )}
- </DialogContent>
- </Dialog>
- );
- }
|