TerraTechSummaryDialog.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import {
  4. ColumnDef,
  5. flexRender,
  6. getCoreRowModel,
  7. getPaginationRowModel,
  8. getSortedRowModel,
  9. useReactTable,
  10. SortingState,
  11. } from "@tanstack/react-table";
  12. import * as XLSX from "xlsx";
  13. import {
  14. Dialog,
  15. DialogContent,
  16. DialogHeader,
  17. DialogTitle,
  18. } from "@/components/ui/dialog";
  19. import {
  20. Table,
  21. TableBody,
  22. TableCell,
  23. TableHead,
  24. TableHeader,
  25. TableRow,
  26. } from "@/components/ui/table";
  27. import { Badge } from "@/components/ui/badge";
  28. import { Button } from "@/components/ui/button";
  29. import { getTerraTechFacilitySummary, TerraTechSummaryRow } from "@/app/actions/imports";
  30. import { Fuel, AlertTriangle, Download, ChevronLeft, ChevronRight } from "lucide-react";
  31. interface TerraTechSummaryDialogProps {
  32. open: boolean;
  33. onOpenChange: (open: boolean) => void;
  34. importId: number;
  35. }
  36. interface SummaryData {
  37. importId: number;
  38. importName: string;
  39. layoutName: string;
  40. rows: TerraTechSummaryRow[];
  41. }
  42. function formatNumber(value: number | string | null | undefined, decimals: number = 2): string {
  43. if (value === null || value === undefined) return '-';
  44. const num = typeof value === 'string' ? parseFloat(value) : value;
  45. if (isNaN(num)) return '-';
  46. return num.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  47. }
  48. function formatPercent(value: number | string | null | undefined): string {
  49. if (value === null || value === undefined) return '-';
  50. const num = typeof value === 'string' ? parseFloat(value) : value;
  51. if (isNaN(num)) return '-';
  52. return `${num.toFixed(1)}%`;
  53. }
  54. const columns: ColumnDef<TerraTechSummaryRow>[] = [
  55. {
  56. accessorKey: "wellName",
  57. header: "Well Name",
  58. cell: ({ row }) => (
  59. <div className="font-medium">{row.getValue("wellName") || '-'}</div>
  60. ),
  61. },
  62. {
  63. accessorKey: "corpId",
  64. header: "CorpID",
  65. cell: ({ row }) => row.getValue("corpId") || '-',
  66. },
  67. {
  68. accessorKey: "facilityId",
  69. header: "Facility ID",
  70. cell: ({ row }) => {
  71. const value = row.getValue("facilityId") as string | null;
  72. return value || <span className="text-amber-500">-</span>;
  73. },
  74. },
  75. {
  76. accessorKey: "gas",
  77. header: "Gas",
  78. cell: ({ row }) => (
  79. <div className="text-right">{formatNumber(row.getValue("gas"))}</div>
  80. ),
  81. },
  82. {
  83. accessorKey: "oil",
  84. header: "Oil",
  85. cell: ({ row }) => (
  86. <div className="text-right">{formatNumber(row.getValue("oil"))}</div>
  87. ),
  88. },
  89. {
  90. accessorKey: "water",
  91. header: "Water",
  92. cell: ({ row }) => (
  93. <div className="text-right">{formatNumber(row.getValue("water"))}</div>
  94. ),
  95. },
  96. {
  97. accessorKey: "state",
  98. header: "State",
  99. cell: ({ row }) => row.getValue("state") || '-',
  100. },
  101. {
  102. accessorKey: "county",
  103. header: "County",
  104. cell: ({ row }) => {
  105. const value = row.getValue("county") as string | null;
  106. return value || <span className="text-amber-500">-</span>;
  107. },
  108. },
  109. {
  110. accessorKey: "daysQ1",
  111. header: "Days Q1",
  112. cell: ({ row }) => (
  113. <div className="text-right">{formatNumber(row.getValue("daysQ1"), 0)}</div>
  114. ),
  115. },
  116. {
  117. accessorKey: "daysQ2",
  118. header: "Days Q2",
  119. cell: ({ row }) => (
  120. <div className="text-right">{formatNumber(row.getValue("daysQ2"), 0)}</div>
  121. ),
  122. },
  123. {
  124. accessorKey: "daysQ3",
  125. header: "Days Q3",
  126. cell: ({ row }) => (
  127. <div className="text-right">{formatNumber(row.getValue("daysQ3"), 0)}</div>
  128. ),
  129. },
  130. {
  131. accessorKey: "daysQ4",
  132. header: "Days Q4",
  133. cell: ({ row }) => (
  134. <div className="text-right">{formatNumber(row.getValue("daysQ4"), 0)}</div>
  135. ),
  136. },
  137. {
  138. accessorKey: "daysQ1Pct",
  139. header: "Q1 %",
  140. cell: ({ row }) => (
  141. <div className="text-right">{formatPercent(row.getValue("daysQ1Pct"))}</div>
  142. ),
  143. },
  144. {
  145. accessorKey: "daysQ2Pct",
  146. header: "Q2 %",
  147. cell: ({ row }) => (
  148. <div className="text-right">{formatPercent(row.getValue("daysQ2Pct"))}</div>
  149. ),
  150. },
  151. {
  152. accessorKey: "daysQ3Pct",
  153. header: "Q3 %",
  154. cell: ({ row }) => (
  155. <div className="text-right">{formatPercent(row.getValue("daysQ3Pct"))}</div>
  156. ),
  157. },
  158. {
  159. accessorKey: "daysQ4Pct",
  160. header: "Q4 %",
  161. cell: ({ row }) => (
  162. <div className="text-right">{formatPercent(row.getValue("daysQ4Pct"))}</div>
  163. ),
  164. },
  165. {
  166. accessorKey: "gasCorr",
  167. header: "Gas-corr",
  168. cell: ({ row }) => (
  169. <div className="text-right">{formatNumber(row.getValue("gasCorr"))}</div>
  170. ),
  171. },
  172. {
  173. accessorKey: "oilCorr",
  174. header: "Oil-corr",
  175. cell: ({ row }) => (
  176. <div className="text-right">{formatNumber(row.getValue("oilCorr"))}</div>
  177. ),
  178. },
  179. {
  180. accessorKey: "waterCorr",
  181. header: "Water-corr",
  182. cell: ({ row }) => (
  183. <div className="text-right">{formatNumber(row.getValue("waterCorr"))}</div>
  184. ),
  185. },
  186. {
  187. accessorKey: "isMissingFacilityId",
  188. header: "Missing Facility ID",
  189. cell: ({ row }) => {
  190. const value = row.getValue("isMissingFacilityId");
  191. return value ? <Badge variant="destructive" className="text-xs">Yes</Badge> : null;
  192. },
  193. },
  194. {
  195. accessorKey: "isMissingCounty",
  196. header: "Missing County",
  197. cell: ({ row }) => {
  198. const value = row.getValue("isMissingCounty");
  199. return value ? <Badge variant="destructive" className="text-xs">Yes</Badge> : null;
  200. },
  201. },
  202. ];
  203. function SummaryTable({ data, importName }: { data: TerraTechSummaryRow[]; importName: string }) {
  204. const [sorting, setSorting] = useState<SortingState>([
  205. { id: "wellName", desc: false },
  206. ]);
  207. const table = useReactTable({
  208. data,
  209. columns,
  210. state: {
  211. sorting,
  212. },
  213. onSortingChange: setSorting,
  214. getCoreRowModel: getCoreRowModel(),
  215. getPaginationRowModel: getPaginationRowModel(),
  216. getSortedRowModel: getSortedRowModel(),
  217. initialState: {
  218. pagination: {
  219. pageSize: 30,
  220. },
  221. },
  222. });
  223. function exportToExcel() {
  224. // Prepare data for Excel export
  225. const exportData = data.map(row => ({
  226. "Well Name": row.wellName || '',
  227. "CorpID": row.corpId || '',
  228. "Facility ID": row.facilityId || '',
  229. "Gas": row.gas,
  230. "Oil": row.oil,
  231. "Water": row.water,
  232. "State": row.state || '',
  233. "County": row.county || '',
  234. "Days Q1": row.daysQ1,
  235. "Days Q2": row.daysQ2,
  236. "Days Q3": row.daysQ3,
  237. "Days Q4": row.daysQ4,
  238. "Q1 %": row.daysQ1Pct,
  239. "Q2 %": row.daysQ2Pct,
  240. "Q3 %": row.daysQ3Pct,
  241. "Q4 %": row.daysQ4Pct,
  242. "Gas-corr": row.gasCorr,
  243. "Oil-corr": row.oilCorr,
  244. "Water-corr": row.waterCorr,
  245. "Missing Facility ID": row.isMissingFacilityId || '',
  246. "Missing County": row.isMissingCounty || '',
  247. }));
  248. const worksheet = XLSX.utils.json_to_sheet(exportData);
  249. const workbook = XLSX.utils.book_new();
  250. XLSX.utils.book_append_sheet(workbook, worksheet, "Facility Summary");
  251. // Generate filename with import name and date
  252. const date = new Date().toISOString().split('T')[0];
  253. const filename = `${importName.replace(/[^a-z0-9]/gi, '_')}_Summary_${date}.xlsx`;
  254. XLSX.writeFile(workbook, filename);
  255. }
  256. return (
  257. <div className="space-y-4">
  258. <div className="flex justify-between items-center">
  259. <div className="text-sm text-muted-foreground">
  260. {data.length} records
  261. </div>
  262. <Button
  263. variant="outline"
  264. size="sm"
  265. onClick={exportToExcel}
  266. className="bg-green-50 hover:bg-green-100 text-green-700 border-green-300"
  267. >
  268. <Download className="h-4 w-4 mr-2" />
  269. Export to Excel
  270. </Button>
  271. </div>
  272. <div className="rounded-md border bg-white dark:bg-gray-800 dark:border-gray-700 max-h-[60vh] overflow-auto">
  273. <Table>
  274. <TableHeader className="sticky top-0 bg-white dark:bg-gray-800 z-10">
  275. {table.getHeaderGroups().map((headerGroup) => (
  276. <TableRow key={headerGroup.id} className="dark:border-gray-700">
  277. {headerGroup.headers.map((header) => (
  278. <TableHead
  279. key={header.id}
  280. onClick={header.column.getToggleSortingHandler()}
  281. 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"}
  282. >
  283. <div className="flex items-center gap-1">
  284. {flexRender(
  285. header.column.columnDef.header,
  286. header.getContext()
  287. )}
  288. {header.column.getIsSorted() && (
  289. <span>
  290. {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
  291. </span>
  292. )}
  293. </div>
  294. </TableHead>
  295. ))}
  296. </TableRow>
  297. ))}
  298. </TableHeader>
  299. <TableBody>
  300. {table.getRowModel().rows?.length ? (
  301. table.getRowModel().rows.map((row) => (
  302. <TableRow
  303. key={row.id}
  304. data-state={row.getIsSelected() && "selected"}
  305. className="dark:border-gray-700"
  306. >
  307. {row.getVisibleCells().map((cell) => (
  308. <TableCell key={cell.id} className="whitespace-nowrap">
  309. {flexRender(cell.column.columnDef.cell, cell.getContext())}
  310. </TableCell>
  311. ))}
  312. </TableRow>
  313. ))
  314. ) : (
  315. <TableRow>
  316. <TableCell colSpan={columns.length} className="h-24 text-center">
  317. No summary data available for this import.
  318. </TableCell>
  319. </TableRow>
  320. )}
  321. </TableBody>
  322. </Table>
  323. </div>
  324. <div className="flex items-center justify-between">
  325. <div className="text-sm text-muted-foreground">
  326. Showing {table.getRowModel().rows.length} of {data.length} records
  327. </div>
  328. <div className="flex items-center space-x-2">
  329. <Button
  330. variant="outline"
  331. size="sm"
  332. onClick={() => table.previousPage()}
  333. disabled={!table.getCanPreviousPage()}
  334. >
  335. <ChevronLeft className="h-4 w-4 mr-1" />
  336. Previous
  337. </Button>
  338. <div className="text-sm font-medium">
  339. Page {table.getState().pagination.pageIndex + 1} of{" "}
  340. {table.getPageCount()}
  341. </div>
  342. <Button
  343. variant="outline"
  344. size="sm"
  345. onClick={() => table.nextPage()}
  346. disabled={!table.getCanNextPage()}
  347. >
  348. Next
  349. <ChevronRight className="h-4 w-4 ml-1" />
  350. </Button>
  351. </div>
  352. </div>
  353. </div>
  354. );
  355. }
  356. export function TerraTechSummaryDialog({ open, onOpenChange, importId }: TerraTechSummaryDialogProps) {
  357. const [loading, setLoading] = useState(false);
  358. const [error, setError] = useState<string | null>(null);
  359. const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
  360. useEffect(() => {
  361. if (open && importId) {
  362. loadSummary();
  363. }
  364. // eslint-disable-next-line react-hooks/exhaustive-deps
  365. }, [open, importId]);
  366. async function loadSummary() {
  367. setLoading(true);
  368. setError(null);
  369. try {
  370. const result = await getTerraTechFacilitySummary(importId);
  371. if (result.success && result.data) {
  372. setSummaryData(result.data);
  373. } else {
  374. setError(result.error || 'Failed to load summary');
  375. }
  376. } catch {
  377. setError('An unexpected error occurred');
  378. } finally {
  379. setLoading(false);
  380. }
  381. }
  382. return (
  383. <Dialog open={open} onOpenChange={onOpenChange}>
  384. <DialogContent className="max-w-[95vw] max-h-[90vh] dark:bg-gray-800 dark:border-gray-700">
  385. <DialogHeader>
  386. <DialogTitle className="flex items-center gap-2 text-gray-900 dark:text-white">
  387. <div className="bg-amber-500 w-8 h-8 rounded-full flex items-center justify-center text-white">
  388. <Fuel className="w-4 h-4" />
  389. </div>
  390. Facility Summary
  391. {summaryData && (
  392. <Badge variant="outline" className="ml-2">
  393. {summaryData.importName}
  394. </Badge>
  395. )}
  396. </DialogTitle>
  397. </DialogHeader>
  398. {loading && (
  399. <div className="flex justify-center py-12">
  400. <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500"></div>
  401. </div>
  402. )}
  403. {error && (
  404. <div className="flex flex-col items-center justify-center py-12 text-red-500">
  405. <AlertTriangle className="h-8 w-8 mb-2" />
  406. <p>{error}</p>
  407. </div>
  408. )}
  409. {!loading && !error && summaryData && (
  410. <SummaryTable data={summaryData.rows} importName={summaryData.importName} />
  411. )}
  412. </DialogContent>
  413. </Dialog>
  414. );
  415. }