page.tsx 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. 'use client';
  2. import { useState, useEffect, useCallback } from 'react';
  3. import { FileText, Fuel, Upload, Database, BarChart3, CheckCircle, Loader2, History, PlusCircle } from 'lucide-react';
  4. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
  5. import { Button } from '@/components/ui/button';
  6. import { UploadForm } from '@/app/components/uploadForm';
  7. import { createTerraTechImportRecord, processTerraTechImportData } from '@/app/actions/terratech-workflow';
  8. import { getImportsByLayoutName, getTerraTechFacilitySummary } from '@/app/actions/imports';
  9. import { TerraTechImportsTable } from '@/app/components/terratech/TerraTechImportsTable';
  10. import { TerraTechSummaryDialog } from '@/app/components/terratech/TerraTechSummaryDialog';
  11. import { useToast } from '@/hooks/use-toast';
  12. interface FileData {
  13. id: string;
  14. filename: string;
  15. mimetype: string;
  16. size: number;
  17. createdAt: string;
  18. updatedAt: string;
  19. }
  20. interface Import {
  21. id: number;
  22. name: string;
  23. importDate: string;
  24. layoutId: number;
  25. layout: {
  26. id: number;
  27. name: string;
  28. };
  29. }
  30. interface RawImportData {
  31. id: number;
  32. name: string;
  33. importDate: Date | string;
  34. layoutId: number;
  35. layout: {
  36. id: number;
  37. name: string;
  38. };
  39. }
  40. interface SummaryData {
  41. importId: number;
  42. importName: string;
  43. layoutName: string;
  44. rows: any[];
  45. }
  46. const LAYOUT_NAME = 'TerraTech - GasOilWater Summary';
  47. export default function TerraTechFacilitySummariesPage() {
  48. const [imports, setImports] = useState<Import[]>([]);
  49. const [loading, setLoading] = useState(true);
  50. const [summaryDialogOpen, setSummaryDialogOpen] = useState(false);
  51. const [selectedImportId, setSelectedImportId] = useState<number | null>(null);
  52. const { toast } = useToast();
  53. // Workflow state
  54. const [viewMode, setViewMode] = useState<'imports' | 'new-import' | 'summary'>('imports');
  55. const [currentStep, setCurrentStep] = useState(1);
  56. const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
  57. const [isProcessing, setIsProcessing] = useState(false);
  58. const [importRecord, setImportRecord] = useState<Import | null>(null);
  59. const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
  60. const [error, setError] = useState<string | null>(null);
  61. const [selectedSummaryImport, setSelectedSummaryImport] = useState<Import | null>(null);
  62. const loadImports = useCallback(async () => {
  63. try {
  64. setLoading(true);
  65. setError(null);
  66. const result = await getImportsByLayoutName(LAYOUT_NAME);
  67. if (result.success && result.data) {
  68. const transformedImports = result.data.map((item: RawImportData) => ({
  69. id: item.id,
  70. name: item.name,
  71. importDate: item.importDate instanceof Date
  72. ? item.importDate.toISOString()
  73. : String(item.importDate),
  74. layoutId: item.layoutId,
  75. layout: {
  76. id: item.layout.id,
  77. name: item.layout.name,
  78. },
  79. }));
  80. setImports(transformedImports);
  81. } else {
  82. toast({
  83. title: 'Error',
  84. description: result.error || 'Failed to load imports',
  85. variant: 'destructive',
  86. });
  87. setImports([]);
  88. }
  89. } catch {
  90. toast({
  91. title: 'Error',
  92. description: 'Failed to load imports',
  93. variant: 'destructive',
  94. });
  95. } finally {
  96. setLoading(false);
  97. }
  98. }, [toast]);
  99. useEffect(() => {
  100. loadImports();
  101. }, [loadImports]);
  102. // Workflow handlers
  103. const handleFileUploaded = (file: FileData) => {
  104. setUploadedFile(file);
  105. setCurrentStep(2);
  106. setError(null);
  107. };
  108. const handleCreateImportRecord = async () => {
  109. if (!uploadedFile) return;
  110. setIsProcessing(true);
  111. setError(null);
  112. try {
  113. const result = await createTerraTechImportRecord(uploadedFile.id, uploadedFile.filename);
  114. if (result.success && result.data) {
  115. const importData = result.data;
  116. const importRecordData: Import = {
  117. id: importData.id,
  118. name: importData.name,
  119. importDate: importData.importDate instanceof Date
  120. ? importData.importDate.toISOString()
  121. : String(importData.importDate),
  122. layoutId: importData.layoutId,
  123. layout: {
  124. id: importData.layout?.id || 0,
  125. name: importData.layout?.name || LAYOUT_NAME,
  126. },
  127. };
  128. setImportRecord(importRecordData);
  129. setCurrentStep(3);
  130. } else {
  131. setError(result.error || 'Failed to create import record');
  132. }
  133. } catch (err) {
  134. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  135. } finally {
  136. setIsProcessing(false);
  137. }
  138. };
  139. const handleProcessImportData = async () => {
  140. if (!importRecord) return;
  141. setIsProcessing(true);
  142. setError(null);
  143. try {
  144. const result = await processTerraTechImportData(importRecord.id);
  145. if (result.success) {
  146. setCurrentStep(4);
  147. } else {
  148. setError(result.error || 'Failed to process import data');
  149. }
  150. } catch (err) {
  151. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  152. } finally {
  153. setIsProcessing(false);
  154. }
  155. };
  156. const handleGenerateSummary = async () => {
  157. if (!importRecord) return;
  158. setIsProcessing(true);
  159. setError(null);
  160. try {
  161. const result = await getTerraTechFacilitySummary(importRecord.id);
  162. if (result.success && result.data) {
  163. setSummaryData(result.data);
  164. setSelectedSummaryImport(importRecord);
  165. setViewMode('summary');
  166. } else {
  167. setError(result.error || 'Failed to generate summary');
  168. }
  169. } catch (err) {
  170. setError(err instanceof Error ? err.message : 'Failed to generate summary');
  171. } finally {
  172. setIsProcessing(false);
  173. }
  174. };
  175. const handleStartNewImport = () => {
  176. setViewMode('new-import');
  177. setCurrentStep(1);
  178. setUploadedFile(null);
  179. setImportRecord(null);
  180. setSummaryData(null);
  181. setError(null);
  182. };
  183. const handleBackToImports = () => {
  184. setViewMode('imports');
  185. setSelectedSummaryImport(null);
  186. setSummaryData(null);
  187. loadImports();
  188. };
  189. function handleViewSummary(importRecord: Import) {
  190. setSelectedSummaryImport(importRecord);
  191. setSelectedImportId(importRecord.id);
  192. setSummaryDialogOpen(true);
  193. }
  194. const formatDate = (dateString: string) => {
  195. return new Date(dateString).toLocaleDateString('en-US', {
  196. year: 'numeric',
  197. month: 'short',
  198. day: 'numeric',
  199. hour: '2-digit',
  200. minute: '2-digit'
  201. });
  202. };
  203. if (loading && viewMode === 'imports') {
  204. return (
  205. <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
  206. <div className="container mx-auto px-4 py-8">
  207. <div className="flex justify-center">
  208. <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
  209. </div>
  210. </div>
  211. </div>
  212. );
  213. }
  214. return (
  215. <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
  216. <div className="container mx-auto px-4 py-8">
  217. <div className="flex justify-between items-center mb-8">
  218. <div className="flex items-center gap-4">
  219. <div className="bg-amber-500 w-12 h-12 rounded-full flex items-center justify-center text-white">
  220. <Fuel className="w-6 h-6" />
  221. </div>
  222. <div>
  223. <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
  224. TerraTech Facility Summaries
  225. </h1>
  226. <p className="text-gray-600 dark:text-gray-300">
  227. View Gas, Oil, and Water production summaries for imported data
  228. </p>
  229. </div>
  230. </div>
  231. </div>
  232. {error && (
  233. <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
  234. <p className="text-sm text-red-800">{error}</p>
  235. </div>
  236. )}
  237. {/* Navigation Buttons */}
  238. <div className="mb-6 flex gap-4">
  239. <Button
  240. variant={viewMode === 'imports' ? 'default' : 'outline'}
  241. onClick={handleBackToImports}
  242. className="flex items-center gap-2"
  243. >
  244. <History className="h-4 w-4" />
  245. Prior Imports
  246. </Button>
  247. <Button
  248. variant={viewMode === 'new-import' ? 'default' : 'outline'}
  249. onClick={handleStartNewImport}
  250. className="flex items-center gap-2"
  251. >
  252. <PlusCircle className="h-4 w-4" />
  253. New Import
  254. </Button>
  255. </div>
  256. {/* Prior Imports View */}
  257. {viewMode === 'imports' && (
  258. <>
  259. {imports.length === 0 ? (
  260. <Card className="dark:bg-gray-800 dark:border-gray-700">
  261. <CardContent className="flex flex-col items-center justify-center py-12">
  262. <FileText className="h-12 w-12 text-muted-foreground mb-4" />
  263. <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">No imports found</h3>
  264. <p className="text-muted-foreground mb-4 dark:text-gray-300 text-center">
  265. No imports using the "{LAYOUT_NAME}" layout configuration were found.
  266. <br />
  267. Import data using this layout to see it here.
  268. </p>
  269. <Button onClick={handleStartNewImport}>
  270. Create First Import
  271. </Button>
  272. </CardContent>
  273. </Card>
  274. ) : (
  275. <div className="w-full">
  276. <TerraTechImportsTable
  277. data={imports}
  278. onViewSummary={handleViewSummary}
  279. />
  280. </div>
  281. )}
  282. </>
  283. )}
  284. {/* New Import Workflow */}
  285. {viewMode === 'new-import' && (
  286. <div className="space-y-6">
  287. {/* Workflow Steps */}
  288. <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
  289. {[
  290. {
  291. id: 1,
  292. title: 'Upload Excel File',
  293. description: 'Upload the TerraTech GasOilWater Excel file',
  294. icon: Upload,
  295. status: currentStep >= 1 ? (uploadedFile ? 'completed' : 'pending') : 'pending',
  296. },
  297. {
  298. id: 2,
  299. title: 'Create Import Record',
  300. description: 'Create import record with layout configuration',
  301. icon: FileText,
  302. status: currentStep >= 2 ? (importRecord ? 'completed' : 'pending') : 'disabled',
  303. },
  304. {
  305. id: 3,
  306. title: 'Import Data',
  307. description: 'Process Excel file and import data',
  308. icon: Database,
  309. status: currentStep > 3 ? 'completed' : (currentStep === 3 ? 'pending' : 'disabled'),
  310. },
  311. {
  312. id: 4,
  313. title: 'Generate Summary',
  314. description: 'Run summary calculations and display results',
  315. icon: BarChart3,
  316. status: currentStep >= 4 ? (summaryData ? 'completed' : 'pending') : 'disabled',
  317. },
  318. ].map((step) => {
  319. const Icon = step.icon;
  320. const getStatusColor = (status: string) => {
  321. switch (status) {
  322. case 'completed':
  323. return 'text-green-600 bg-green-50 border-green-200';
  324. case 'pending':
  325. return 'text-amber-600 bg-amber-50 border-amber-200';
  326. case 'disabled':
  327. return 'text-gray-400 bg-gray-50 border-gray-200';
  328. default:
  329. return 'text-gray-600 bg-gray-50 border-gray-200';
  330. }
  331. };
  332. return (
  333. <Card
  334. key={step.id}
  335. className={`border-2 ${getStatusColor(step.status)}`}
  336. >
  337. <CardHeader className="pb-3">
  338. <div className="flex items-center space-x-2">
  339. <Icon className="h-5 w-5" />
  340. <div>
  341. <CardTitle className="text-sm font-medium">
  342. Step {step.id}: {step.title}
  343. </CardTitle>
  344. </div>
  345. </div>
  346. </CardHeader>
  347. <CardContent>
  348. <CardDescription className="text-xs">
  349. {step.description}
  350. </CardDescription>
  351. </CardContent>
  352. </Card>
  353. );
  354. })}
  355. </div>
  356. {/* Step Content */}
  357. <div className="space-y-6">
  358. {currentStep === 1 && (
  359. <Card>
  360. <CardHeader>
  361. <CardTitle>Step 1: Upload Excel File</CardTitle>
  362. <CardDescription>
  363. Upload your TerraTech GasOilWater Excel file to begin processing
  364. </CardDescription>
  365. </CardHeader>
  366. <CardContent>
  367. <div className="space-y-4">
  368. <UploadForm onFileUploaded={handleFileUploaded} />
  369. {uploadedFile && (
  370. <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
  371. <div className="flex items-center space-x-2">
  372. <CheckCircle className="h-5 w-5 text-green-600" />
  373. <div>
  374. <p className="text-sm font-medium text-green-800">File uploaded successfully!</p>
  375. <p className="text-sm text-green-600">
  376. {uploadedFile.filename} ({(uploadedFile.size / 1024 / 1024).toFixed(2)} MB)
  377. </p>
  378. </div>
  379. </div>
  380. </div>
  381. )}
  382. </div>
  383. </CardContent>
  384. </Card>
  385. )}
  386. {currentStep === 2 && (
  387. <Card>
  388. <CardHeader>
  389. <CardTitle>Step 2: Create Import Record</CardTitle>
  390. <CardDescription>
  391. Creating import record with TerraTech - GasOilWater Summary layout configuration
  392. </CardDescription>
  393. </CardHeader>
  394. <CardContent>
  395. <div className="space-y-4">
  396. <p className="text-sm text-muted-foreground">
  397. File: {uploadedFile?.filename}
  398. </p>
  399. <p className="text-sm text-muted-foreground">
  400. Layout: {LAYOUT_NAME}
  401. </p>
  402. <Button
  403. onClick={handleCreateImportRecord}
  404. disabled={isProcessing}
  405. className="w-full bg-amber-500 hover:bg-amber-600"
  406. >
  407. {isProcessing ? (
  408. <>
  409. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  410. Creating Import Record...
  411. </>
  412. ) : (
  413. 'Create Import Record'
  414. )}
  415. </Button>
  416. </div>
  417. </CardContent>
  418. </Card>
  419. )}
  420. {currentStep === 3 && (
  421. <Card>
  422. <CardHeader>
  423. <CardTitle>Step 3: Import Data</CardTitle>
  424. <CardDescription>
  425. Processing Excel file and importing data into database
  426. </CardDescription>
  427. </CardHeader>
  428. <CardContent>
  429. <div className="space-y-4">
  430. <p className="text-sm text-muted-foreground">
  431. Import ID: {importRecord?.id}
  432. </p>
  433. <p className="text-sm text-muted-foreground">
  434. Import Name: {importRecord?.name}
  435. </p>
  436. <Button
  437. onClick={handleProcessImportData}
  438. disabled={isProcessing}
  439. className="w-full bg-amber-500 hover:bg-amber-600"
  440. >
  441. {isProcessing ? (
  442. <>
  443. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  444. Processing Import...
  445. </>
  446. ) : (
  447. 'Process Import'
  448. )}
  449. </Button>
  450. </div>
  451. </CardContent>
  452. </Card>
  453. )}
  454. {currentStep === 4 && (
  455. <Card>
  456. <CardHeader>
  457. <CardTitle>Step 4: Generate Summary</CardTitle>
  458. <CardDescription>
  459. Running summary calculations and displaying results
  460. </CardDescription>
  461. </CardHeader>
  462. <CardContent>
  463. <div className="space-y-4">
  464. <p className="text-sm text-muted-foreground">
  465. Import ID: {importRecord?.id}
  466. </p>
  467. <Button
  468. onClick={handleGenerateSummary}
  469. disabled={isProcessing}
  470. className="w-full bg-amber-500 hover:bg-amber-600"
  471. >
  472. {isProcessing ? (
  473. <>
  474. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  475. Generating Summary...
  476. </>
  477. ) : (
  478. 'Generate Summary'
  479. )}
  480. </Button>
  481. </div>
  482. </CardContent>
  483. </Card>
  484. )}
  485. </div>
  486. </div>
  487. )}
  488. {/* Summary Results View */}
  489. {viewMode === 'summary' && selectedSummaryImport && summaryData && (
  490. <Card>
  491. <CardHeader>
  492. <div className="flex justify-between items-start">
  493. <div>
  494. <CardTitle>Summary Results</CardTitle>
  495. <CardDescription>
  496. {selectedSummaryImport.name} - {formatDate(selectedSummaryImport.importDate)}
  497. </CardDescription>
  498. </div>
  499. <Button
  500. variant="outline"
  501. onClick={handleBackToImports}
  502. className="flex items-center gap-2"
  503. >
  504. <History className="h-4 w-4" />
  505. Back to Imports
  506. </Button>
  507. </div>
  508. </CardHeader>
  509. <CardContent>
  510. {isProcessing ? (
  511. <div className="flex justify-center py-8">
  512. <Loader2 className="h-8 w-8 animate-spin text-amber-500" />
  513. </div>
  514. ) : summaryData.rows.length === 0 ? (
  515. <div className="text-center py-8">
  516. <p className="text-muted-foreground mb-4">No summary data available</p>
  517. </div>
  518. ) : (
  519. <div className="overflow-x-auto">
  520. <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
  521. <thead className="bg-gray-50 dark:bg-gray-800">
  522. <tr>
  523. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Well Name</th>
  524. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">CorpID</th>
  525. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Facility ID</th>
  526. <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Gas</th>
  527. <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Oil</th>
  528. <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Water</th>
  529. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">State</th>
  530. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">County</th>
  531. </tr>
  532. </thead>
  533. <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
  534. {summaryData.rows.slice(0, 50).map((row, index) => (
  535. <tr
  536. key={`${row.wellName}-${index}`}
  537. className={index % 2 === 0
  538. ? 'bg-white dark:bg-gray-800'
  539. : 'bg-gray-50 dark:bg-gray-700/50'
  540. }
  541. >
  542. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{row.wellName || '-'}</td>
  543. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{row.corpId || '-'}</td>
  544. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{row.facilityId || '-'}</td>
  545. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 text-right">
  546. {row.gas !== null ? row.gas.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '-'}
  547. </td>
  548. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 text-right">
  549. {row.oil !== null ? row.oil.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '-'}
  550. </td>
  551. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 text-right">
  552. {row.water !== null ? row.water.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '-'}
  553. </td>
  554. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{row.state || '-'}</td>
  555. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{row.county || '-'}</td>
  556. </tr>
  557. ))}
  558. </tbody>
  559. </table>
  560. {summaryData.rows.length > 50 && (
  561. <p className="text-sm text-muted-foreground mt-4 text-center">
  562. Showing 50 of {summaryData.rows.length} records. Use the dialog to view all records.
  563. </p>
  564. )}
  565. </div>
  566. )}
  567. </CardContent>
  568. </Card>
  569. )}
  570. <TerraTechSummaryDialog
  571. open={summaryDialogOpen}
  572. onOpenChange={setSummaryDialogOpen}
  573. importId={selectedImportId || 0}
  574. />
  575. </div>
  576. </div>
  577. );
  578. }