TerraTechSummaryDialog.tsx 14 KB

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