|
|
@@ -1,6 +1,15 @@
|
|
|
"use client";
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
+import {
|
|
|
+ ColumnDef,
|
|
|
+ flexRender,
|
|
|
+ getCoreRowModel,
|
|
|
+ getSortedRowModel,
|
|
|
+ useReactTable,
|
|
|
+ SortingState,
|
|
|
+} from "@tanstack/react-table";
|
|
|
+import * as XLSX from "xlsx";
|
|
|
import {
|
|
|
Dialog,
|
|
|
DialogContent,
|
|
|
@@ -16,8 +25,9 @@ import {
|
|
|
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 } from "lucide-react";
|
|
|
+import { Fuel, AlertTriangle, Download } from "lucide-react";
|
|
|
|
|
|
interface TerraTechSummaryDialogProps {
|
|
|
open: boolean;
|
|
|
@@ -32,6 +42,296 @@ interface SummaryData {
|
|
|
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(),
|
|
|
+ getSortedRowModel: getSortedRowModel(),
|
|
|
+ });
|
|
|
+
|
|
|
+ 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 max-h-[60vh] overflow-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>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
export function TerraTechSummaryDialog({ open, onOpenChange, importId }: TerraTechSummaryDialogProps) {
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
@@ -61,20 +361,6 @@ export function TerraTechSummaryDialog({ open, onOpenChange, importId }: TerraTe
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- 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)}%`;
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
<DialogContent className="max-w-[95vw] max-h-[90vh] dark:bg-gray-800 dark:border-gray-700">
|
|
|
@@ -106,93 +392,7 @@ export function TerraTechSummaryDialog({ open, onOpenChange, importId }: TerraTe
|
|
|
)}
|
|
|
|
|
|
{!loading && !error && summaryData && (
|
|
|
- <div className="h-[70vh] overflow-auto">
|
|
|
- <div className="pr-4">
|
|
|
- <div className="mb-4 text-sm text-gray-600 dark:text-gray-300">
|
|
|
- Total records: <strong>{summaryData.rows.length}</strong>
|
|
|
- </div>
|
|
|
- <Table>
|
|
|
- <TableHeader>
|
|
|
- <TableRow className="dark:border-gray-700">
|
|
|
- <TableHead className="sticky left-0 bg-white dark:bg-gray-800 z-10">Well Name</TableHead>
|
|
|
- <TableHead>CorpID</TableHead>
|
|
|
- <TableHead>Facility ID</TableHead>
|
|
|
- <TableHead className="text-right">Gas</TableHead>
|
|
|
- <TableHead className="text-right">Oil</TableHead>
|
|
|
- <TableHead className="text-right">Water</TableHead>
|
|
|
- <TableHead>State</TableHead>
|
|
|
- <TableHead>County</TableHead>
|
|
|
- <TableHead className="text-right">Days Q1</TableHead>
|
|
|
- <TableHead className="text-right">Days Q2</TableHead>
|
|
|
- <TableHead className="text-right">Days Q3</TableHead>
|
|
|
- <TableHead className="text-right">Days Q4</TableHead>
|
|
|
- <TableHead className="text-right">Q1 %</TableHead>
|
|
|
- <TableHead className="text-right">Q2 %</TableHead>
|
|
|
- <TableHead className="text-right">Q3 %</TableHead>
|
|
|
- <TableHead className="text-right">Q4 %</TableHead>
|
|
|
- <TableHead className="text-right">Gas-corr</TableHead>
|
|
|
- <TableHead className="text-right">Oil-corr</TableHead>
|
|
|
- <TableHead className="text-right">Water-corr</TableHead>
|
|
|
- <TableHead>Missing Facility ID</TableHead>
|
|
|
- <TableHead>Missing County</TableHead>
|
|
|
- </TableRow>
|
|
|
- </TableHeader>
|
|
|
- <TableBody>
|
|
|
- {summaryData.rows.length === 0 ? (
|
|
|
- <TableRow>
|
|
|
- <TableCell colSpan={21} className="h-24 text-center">
|
|
|
- No summary data available for this import.
|
|
|
- </TableCell>
|
|
|
- </TableRow>
|
|
|
- ) : (
|
|
|
- summaryData.rows.map((row, index) => (
|
|
|
- <TableRow key={index} className="dark:border-gray-700">
|
|
|
- <TableCell className="sticky left-0 bg-white dark:bg-gray-800 font-medium">
|
|
|
- {row.wellName || '-'}
|
|
|
- </TableCell>
|
|
|
- <TableCell>{row.corpId || '-'}</TableCell>
|
|
|
- <TableCell>
|
|
|
- {row.facilityId || (
|
|
|
- <span className="text-amber-500">-</span>
|
|
|
- )}
|
|
|
- </TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.gas)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.oil)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.water)}</TableCell>
|
|
|
- <TableCell>{row.state || '-'}</TableCell>
|
|
|
- <TableCell>
|
|
|
- {row.county || (
|
|
|
- <span className="text-amber-500">-</span>
|
|
|
- )}
|
|
|
- </TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.daysQ1, 0)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.daysQ2, 0)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.daysQ3, 0)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.daysQ4, 0)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatPercent(row.daysQ1Pct)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatPercent(row.daysQ2Pct)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatPercent(row.daysQ3Pct)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatPercent(row.daysQ4Pct)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.gasCorr)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.oilCorr)}</TableCell>
|
|
|
- <TableCell className="text-right">{formatNumber(row.waterCorr)}</TableCell>
|
|
|
- <TableCell>
|
|
|
- {row.isMissingFacilityId && (
|
|
|
- <Badge variant="destructive" className="text-xs">Yes</Badge>
|
|
|
- )}
|
|
|
- </TableCell>
|
|
|
- <TableCell>
|
|
|
- {row.isMissingCounty && (
|
|
|
- <Badge variant="destructive" className="text-xs">Yes</Badge>
|
|
|
- )}
|
|
|
- </TableCell>
|
|
|
- </TableRow>
|
|
|
- ))
|
|
|
- )}
|
|
|
- </TableBody>
|
|
|
- </Table>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <SummaryTable data={summaryData.rows} importName={summaryData.importName} />
|
|
|
)}
|
|
|
</DialogContent>
|
|
|
</Dialog>
|