TerraTechSummaryDialog.tsx 14 KB

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