imports.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. 'use server';
  2. import { prisma } from '@/lib/prisma';
  3. import { revalidatePath } from 'next/cache';
  4. import { z } from 'zod';
  5. import { ImportProcessor } from '@/app/lib/excel-import/import-processor';
  6. // Validation schemas
  7. const createImportSchema = z.object({
  8. name: z.string().min(1, 'Import name is required'),
  9. layoutId: z.number().int().positive('Layout configuration is required'),
  10. fileId: z.string().optional(),
  11. });
  12. const updateImportSchema = z.object({
  13. id: z.number().int().positive(),
  14. name: z.string().min(1, 'Import name is required'),
  15. fileId: z.string().optional(),
  16. });
  17. // Create a new import
  18. export async function createImport(data: {
  19. name: string;
  20. layoutId: number;
  21. fileId?: string;
  22. }) {
  23. try {
  24. const validatedData = createImportSchema.parse(data);
  25. const importRecord = await prisma.import.create({
  26. data: {
  27. name: validatedData.name,
  28. layoutId: validatedData.layoutId,
  29. importDate: new Date(),
  30. ...(validatedData.fileId && { fileId: validatedData.fileId }),
  31. },
  32. include: {
  33. layout: true,
  34. },
  35. });
  36. revalidatePath('/imports');
  37. return { success: true, data: importRecord };
  38. } catch (error) {
  39. console.error('Error creating import:', error);
  40. return { success: false, error: 'Failed to create import' };
  41. }
  42. }
  43. // Get all imports for the current user
  44. export async function getImports(userId?: string) {
  45. try {
  46. const imports = await prisma.import.findMany({
  47. where: userId ? {
  48. fileId: {
  49. not: null,
  50. },
  51. } : {},
  52. include: {
  53. layout: true,
  54. },
  55. orderBy: {
  56. importDate: 'desc',
  57. },
  58. });
  59. // Filter by userId manually since Prisma doesn't have direct relation
  60. let filteredImports = imports;
  61. if (userId) {
  62. const fileIds = await prisma.file.findMany({
  63. where: {
  64. userId: userId,
  65. },
  66. select: {
  67. id: true,
  68. },
  69. });
  70. const userFileIds = new Set(fileIds.map(f => f.id));
  71. filteredImports = imports.filter(imp =>
  72. imp.fileId && userFileIds.has(imp.fileId)
  73. );
  74. }
  75. return { success: true, data: filteredImports };
  76. } catch (error) {
  77. console.error('Error fetching imports:', error);
  78. return { success: false, error: 'Failed to fetch imports' };
  79. }
  80. }
  81. // Get a single import by ID
  82. export async function getImportById(id: number) {
  83. try {
  84. const importRecord = await prisma.import.findUnique({
  85. where: { id },
  86. include: {
  87. layout: {
  88. include: {
  89. sections: {
  90. include: {
  91. fields: true,
  92. },
  93. },
  94. },
  95. },
  96. },
  97. });
  98. if (!importRecord) {
  99. return { success: false, error: 'Import not found' };
  100. }
  101. // Fetch file separately if fileId exists
  102. let file = undefined;
  103. if (importRecord.fileId) {
  104. file = await prisma.file.findUnique({
  105. where: { id: importRecord.fileId },
  106. });
  107. }
  108. return { success: true, data: { ...importRecord, file } };
  109. } catch (error) {
  110. console.error('Error fetching import:', error);
  111. return { success: false, error: 'Failed to fetch import' };
  112. }
  113. }
  114. // Update an import
  115. export async function updateImport(data: {
  116. id: number;
  117. name: string;
  118. fileId?: string;
  119. }) {
  120. try {
  121. const validatedData = updateImportSchema.parse(data);
  122. const importRecord = await prisma.import.update({
  123. where: { id: validatedData.id },
  124. data: {
  125. name: validatedData.name,
  126. ...(validatedData.fileId !== undefined && { fileId: validatedData.fileId }),
  127. },
  128. include: {
  129. layout: true,
  130. },
  131. });
  132. revalidatePath('/imports');
  133. return { success: true, data: importRecord };
  134. } catch (error) {
  135. console.error('Error updating import:', error);
  136. return { success: false, error: 'Failed to update import' };
  137. }
  138. }
  139. // Delete an import
  140. export async function deleteImport(id: number) {
  141. try {
  142. await prisma.import.delete({
  143. where: { id },
  144. });
  145. revalidatePath('/imports');
  146. return { success: true };
  147. } catch (error) {
  148. console.error('Error deleting import:', error);
  149. return { success: false, error: 'Failed to delete import' };
  150. }
  151. }
  152. // Calculate Cintas summaries for an import
  153. export async function calculateCintasSummaries(importId: number) {
  154. try {
  155. // This would typically call a stored procedure or perform calculations
  156. // For now, we'll simulate the calculation
  157. // In a real implementation, you might call:
  158. // await prisma.$executeRaw`CALL cintas_calculate_summary(${importId})`;
  159. // For demo purposes, we'll create some sample data
  160. const summaries = [
  161. {
  162. importId,
  163. week: '2024-W01',
  164. trrTotal: 100,
  165. fourWkAverages: 95,
  166. trrPlus4Wk: 195,
  167. powerAdds: 25,
  168. weekId: 1,
  169. },
  170. {
  171. importId,
  172. week: '2024-W02',
  173. trrTotal: 110,
  174. fourWkAverages: 100,
  175. trrPlus4Wk: 210,
  176. powerAdds: 30,
  177. weekId: 2,
  178. },
  179. ];
  180. // Clear existing summaries for this import
  181. await prisma.cintasSummary.deleteMany({
  182. where: { importId },
  183. });
  184. // Create new summaries
  185. const createdSummaries = await Promise.all(
  186. summaries.map(summary =>
  187. prisma.cintasSummary.create({
  188. data: summary,
  189. })
  190. )
  191. );
  192. return { success: true, data: createdSummaries };
  193. } catch (error) {
  194. console.error('Error calculating Cintas summaries:', error);
  195. return { success: false, error: 'Failed to calculate summaries' };
  196. }
  197. }
  198. // Get available layout configurations
  199. export async function getLayoutConfigurations() {
  200. try {
  201. const layouts = await prisma.layoutConfiguration.findMany({
  202. include: {
  203. sections: {
  204. include: {
  205. fields: true,
  206. },
  207. },
  208. },
  209. orderBy: {
  210. name: 'asc',
  211. },
  212. });
  213. return { success: true, data: layouts };
  214. } catch (error) {
  215. console.error('Error fetching layout configurations:', error);
  216. return { success: false, error: 'Failed to fetch layout configurations' };
  217. }
  218. }
  219. // Get import summary (GET /api/imports/[id]/summary)
  220. export async function getImportSummary(importId: number) {
  221. try {
  222. if (!importId || isNaN(importId)) {
  223. return { success: false, error: 'Invalid import ID' };
  224. }
  225. // Check if import exists
  226. const importRecord = await prisma.import.findUnique({
  227. where: { id: importId }
  228. });
  229. if (!importRecord) {
  230. return { success: false, error: 'Import not found' };
  231. }
  232. // Get basic summary data
  233. const totalRecords = await prisma.cintasInstallCalendar.count({
  234. where: { importId }
  235. });
  236. const cintasSummaries = await prisma.cintasSummary.findMany({
  237. where: { importId },
  238. orderBy: { weekId: 'desc' }
  239. });
  240. // Get file info
  241. const file = importRecord.fileId ? await prisma.file.findUnique({
  242. where: { id: importRecord.fileId }
  243. }) : null;
  244. const summary = {
  245. totalRecords,
  246. totalWeeks: cintasSummaries.length,
  247. cintasSummaries: cintasSummaries.map((summary: any) => ({
  248. id: summary.id,
  249. week: summary.week,
  250. trrTotal: summary.trrTotal,
  251. fourWkAverages: summary.fourWkAverages,
  252. trrPlus4Wk: summary.trrPlus4Wk,
  253. powerAdds: summary.powerAdds,
  254. weekId: summary.weekId
  255. }))
  256. };
  257. return {
  258. success: true,
  259. data: {
  260. importId,
  261. fileName: file?.filename || 'Unknown',
  262. uploadDate: importRecord.createdAt,
  263. summary,
  264. summaryExists: cintasSummaries.length > 0
  265. }
  266. };
  267. } catch (error) {
  268. console.error('Error fetching import summary:', error);
  269. return { success: false, error: 'Failed to fetch import summary' };
  270. }
  271. }
  272. // Generate import summary (POST /api/imports/[id]/summary)
  273. export async function generateImportSummary(importId: number) {
  274. try {
  275. if (!importId || isNaN(importId)) {
  276. return { success: false, error: 'Invalid import ID' };
  277. }
  278. // Check if import exists
  279. const importRecord = await prisma.import.findUnique({
  280. where: { id: importId }
  281. });
  282. if (!importRecord) {
  283. return { success: false, error: 'Import not found' };
  284. }
  285. // Check if summary already exists
  286. const existingSummaries = await prisma.cintasSummary.count({
  287. where: { importId }
  288. });
  289. if (existingSummaries > 0) {
  290. // Return existing summary
  291. const cintasSummaries = await prisma.cintasSummary.findMany({
  292. where: { importId },
  293. orderBy: { weekId: 'desc' }
  294. });
  295. return {
  296. success: true,
  297. data: {
  298. importId,
  299. summaryGenerated: false,
  300. message: 'Summary already exists',
  301. summary: cintasSummaries.map((summary: any) => ({
  302. id: summary.id,
  303. week: summary.week,
  304. trrTotal: summary.trrTotal,
  305. fourWkAverages: summary.fourWkAverages,
  306. trrPlus4Wk: summary.trrPlus4Wk,
  307. powerAdds: summary.powerAdds,
  308. weekId: summary.weekId
  309. }))
  310. }
  311. };
  312. }
  313. // Generate new summary using stored procedure
  314. await prisma.$executeRawUnsafe(
  315. `CALL cintas_calculate_summary(${importId})`
  316. );
  317. // Fetch the newly generated summary
  318. const cintasSummaries = await prisma.cintasSummary.findMany({
  319. where: { importId },
  320. orderBy: { weekId: 'desc' }
  321. });
  322. return {
  323. success: true,
  324. data: {
  325. importId,
  326. summaryGenerated: true,
  327. message: 'Summary generated successfully',
  328. summary: cintasSummaries.map((summary: any) => ({
  329. id: summary.id,
  330. week: summary.week,
  331. trrTotal: summary.trrTotal,
  332. fourWkAverages: summary.fourWkAverages,
  333. trrPlus4Wk: summary.trrPlus4Wk,
  334. powerAdds: summary.powerAdds,
  335. weekId: summary.weekId
  336. }))
  337. }
  338. };
  339. } catch (error) {
  340. console.error('Error generating summary:', error);
  341. return { success: false, error: 'Failed to generate summary' };
  342. }
  343. }
  344. // Get import progress (GET /api/imports/[id]/progress)
  345. export async function getImportProgress(importId: number) {
  346. try {
  347. if (!importId || isNaN(importId)) {
  348. return { success: false, error: 'Invalid import ID' };
  349. }
  350. // Check if import exists
  351. const importRecord = await prisma.import.findUnique({
  352. where: { id: importId }
  353. });
  354. if (!importRecord) {
  355. return { success: false, error: 'Import not found' };
  356. }
  357. // Check all possible tables for records
  358. const [cintasCount, gowDataCount, gowFacCount, gowCorpCount] = await Promise.all([
  359. prisma.cintasInstallCalendar.count({ where: { importId } }),
  360. prisma.gowData.count({ where: { importId } }),
  361. prisma.gowFacId.count({ where: { importId } }),
  362. prisma.gowCorpRef.count({ where: { importId } }),
  363. ]);
  364. const totalRecords = cintasCount + gowDataCount + gowFacCount + gowCorpCount;
  365. // Since we don't have status fields, we'll use record count as proxy
  366. const hasRecords = totalRecords > 0;
  367. return {
  368. success: true,
  369. data: {
  370. importId,
  371. status: hasRecords ? 'completed' : 'pending',
  372. progress: hasRecords ? 100 : 0,
  373. processedRecords: totalRecords,
  374. totalRecords: totalRecords,
  375. errorMessage: null,
  376. lastUpdated: importRecord.updatedAt,
  377. timestamp: new Date().toISOString()
  378. }
  379. };
  380. } catch (error) {
  381. console.error('Error fetching import progress:', error);
  382. return { success: false, error: 'Failed to fetch import progress' };
  383. }
  384. }
  385. // Trigger import process (POST /api/imports/[id]/trigger)
  386. export async function triggerImportProcess(importId: number) {
  387. try {
  388. if (!importId || isNaN(importId)) {
  389. console.error('[TRIGGER_IMPORT] ❌ Invalid import ID:', importId);
  390. return { success: false, error: 'Invalid import ID' };
  391. }
  392. console.log('[TRIGGER_IMPORT] 📥 Starting triggerImportProcess for ID:', importId);
  393. // Validate import exists
  394. const importRecord = await prisma.import.findUnique({
  395. where: { id: importId }
  396. });
  397. console.log('[TRIGGER_IMPORT] 📄 Import record found:', { id: importRecord?.id, hasFileId: !!importRecord?.fileId, layoutId: importRecord?.layoutId });
  398. if (!importRecord) {
  399. console.error('[TRIGGER_IMPORT] ❌ Import not found for ID:', importId);
  400. return { success: false, error: 'Import not found' };
  401. }
  402. if (!importRecord.fileId) {
  403. console.error('[TRIGGER_IMPORT] ❌ No file attached to import:', importId);
  404. return { success: false, error: 'No file attached to import' };
  405. }
  406. // Check if layout exists
  407. const layout = await prisma.layoutConfiguration.findUnique({
  408. where: { id: importRecord.layoutId }
  409. });
  410. console.log('[TRIGGER_IMPORT] 📋 Layout found:', { id: layout?.id, name: layout?.name });
  411. if (!layout) {
  412. console.error('[TRIGGER_IMPORT] ❌ No layout configuration found for ID:', importRecord.layoutId);
  413. return { success: false, error: 'No layout configuration found' };
  414. }
  415. // Check if data already exists for this import
  416. const existingRecords = await prisma.cintasInstallCalendar.count({
  417. where: { importId }
  418. });
  419. console.log('[TRIGGER_IMPORT] 📊 Existing records count:', existingRecords);
  420. if (existingRecords > 0) {
  421. console.log('[TRIGGER_IMPORT] ✅ Import already processed, returning early');
  422. return {
  423. success: true,
  424. message: 'Import already processed',
  425. importId,
  426. existingRecords
  427. };
  428. }
  429. // Use the ImportProcessor to actually process the import
  430. console.log('[TRIGGER_IMPORT] 🔄 Starting ImportProcessor for import ID:', importId);
  431. const processor = new ImportProcessor();
  432. const result = await processor.processImport(importId);
  433. console.log('[TRIGGER_IMPORT] 📈 ImportProcessor result:', { success: result.success, totalInserted: result.totalInserted, sectionsCount: result.sections?.length, errors: result.errors });
  434. if (result.success) {
  435. console.log('[TRIGGER_IMPORT] ✅ Import completed successfully');
  436. revalidatePath('/imports');
  437. return {
  438. success: true,
  439. message: 'Import process completed successfully',
  440. importId,
  441. totalInserted: result.totalInserted
  442. };
  443. } else {
  444. console.error('[TRIGGER_IMPORT] ❌ Import processing failed:', result.errors);
  445. return {
  446. success: false,
  447. error: 'Import processing failed',
  448. errors: result.errors
  449. };
  450. }
  451. } catch (error) {
  452. console.error('[TRIGGER_IMPORT] ❌ Error triggering import:', error);
  453. return { success: false, error: 'Failed to trigger import' };
  454. }
  455. }
  456. // Update import progress (for internal use by ImportProcessor)
  457. export async function updateImportProgress(
  458. importId: number,
  459. progress: {
  460. processedRecords: number;
  461. totalRecords: number;
  462. status: string;
  463. errorMessage?: string;
  464. }
  465. ) {
  466. try {
  467. // Since the Import model doesn't have these fields, we'll just return success
  468. // In a real implementation, you would need to add these fields to the schema
  469. console.log(`Import ${importId} progress: ${progress.processedRecords}/${progress.totalRecords} (${progress.status})`);
  470. return { success: true };
  471. } catch (error) {
  472. console.error('Error updating import progress:', error);
  473. return { success: false, error: 'Failed to update progress' };
  474. }
  475. }
  476. // Get imports filtered by layout configuration name
  477. export async function getImportsByLayoutName(layoutName: string) {
  478. try {
  479. // First find the layout by name
  480. const layout = await prisma.layoutConfiguration.findFirst({
  481. where: { name: layoutName },
  482. });
  483. if (!layout) {
  484. return { success: true, data: [] };
  485. }
  486. const imports = await prisma.import.findMany({
  487. where: {
  488. layoutId: layout.id,
  489. },
  490. include: {
  491. layout: true,
  492. },
  493. orderBy: {
  494. importDate: 'desc',
  495. },
  496. });
  497. return { success: true, data: imports };
  498. } catch (error) {
  499. console.error('Error fetching imports by layout:', error);
  500. return { success: false, error: 'Failed to fetch imports' };
  501. }
  502. }
  503. // TerraTech facility summary result type
  504. export interface TerraTechSummaryRow {
  505. wellName: string | null;
  506. corpId: string | null;
  507. facilityId: string | null;
  508. gas: number | null;
  509. oil: number | null;
  510. water: number | null;
  511. state: string | null;
  512. county: string | null;
  513. daysQ1: number | null;
  514. daysQ2: number | null;
  515. daysQ3: number | null;
  516. daysQ4: number | null;
  517. daysQ1Pct: number | null;
  518. daysQ2Pct: number | null;
  519. daysQ3Pct: number | null;
  520. daysQ4Pct: number | null;
  521. gasCorr: number | null;
  522. oilCorr: number | null;
  523. waterCorr: number | null;
  524. isMissingFacilityId: string | null;
  525. isMissingCounty: string | null;
  526. }
  527. // Get TerraTech facility summary for an import
  528. export async function getTerraTechFacilitySummary(importId: number) {
  529. try {
  530. if (!importId || isNaN(importId)) {
  531. return { success: false, error: 'Invalid import ID' };
  532. }
  533. // Check if import exists
  534. const importRecord = await prisma.import.findUnique({
  535. where: { id: importId },
  536. include: { layout: true }
  537. });
  538. if (!importRecord) {
  539. return { success: false, error: 'Import not found' };
  540. }
  541. // Execute the complex aggregation query using raw SQL
  542. const results = await prisma.$queryRaw<TerraTechSummaryRow[]>`
  543. SELECT
  544. s.well_name AS "wellName",
  545. s."corpId" AS "corpId",
  546. gfi.odeq_fac_id AS "facilityId",
  547. CAST(SUM(CAST(s.gas_production AS DECIMAL)) AS DECIMAL(10,2)) AS "gas",
  548. CAST(SUM(CAST(s.oil_production AS DECIMAL)) AS DECIMAL(10,2)) AS "oil",
  549. CAST(SUM(CAST(s.water_production AS DECIMAL)) AS DECIMAL(10,2)) AS "water",
  550. s.state AS "state",
  551. cr.county_parish AS "county",
  552. SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ1",
  553. SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ2",
  554. SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ3",
  555. SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ4",
  556. CAST(
  557. (SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  558. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  559. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  560. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  561. THEN 366 ELSE 365 END * 100)
  562. AS DECIMAL(5,1)
  563. ) AS "daysQ1Pct",
  564. CAST(
  565. (SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  566. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  567. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  568. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  569. THEN 366 ELSE 365 END * 100)
  570. AS DECIMAL(5,1)
  571. ) AS "daysQ2Pct",
  572. CAST(
  573. (SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  574. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  575. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  576. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  577. THEN 366 ELSE 365 END * 100)
  578. AS DECIMAL(5,1)
  579. ) AS "daysQ3Pct",
  580. CAST(
  581. (SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  582. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  583. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  584. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  585. THEN 366 ELSE 365 END * 100)
  586. AS DECIMAL(5,1)
  587. ) AS "daysQ4Pct",
  588. CAST(CASE WHEN SUM(CAST(s.gas_production AS DECIMAL)) < 0 THEN 0 ELSE SUM(CAST(s.gas_production AS DECIMAL)) END AS DECIMAL(10,2)) AS "gasCorr",
  589. CAST(CASE WHEN SUM(CAST(s.oil_production AS DECIMAL)) < 0 THEN 0 ELSE SUM(CAST(s.oil_production AS DECIMAL)) END AS DECIMAL(10,2)) AS "oilCorr",
  590. CAST(CASE WHEN SUM(CAST(s.water_production AS DECIMAL)) < 0 THEN 0 ELSE SUM(CAST(s.water_production AS DECIMAL)) END AS DECIMAL(10,2)) AS "waterCorr",
  591. CASE WHEN gfi.odeq_fac_id IS NULL THEN 'Yes' ELSE NULL END AS "isMissingFacilityId",
  592. CASE WHEN cr.county_parish IS NULL THEN 'Yes' ELSE NULL END AS "isMissingCounty"
  593. FROM gow_data s
  594. INNER JOIN LATERAL (
  595. SELECT
  596. DATE_PART('quarter', s.month::DATE) AS "Quarter",
  597. DATE_PART('days',
  598. DATE_TRUNC('month', s.month::DATE)
  599. + '1 MONTH'::INTERVAL
  600. - '1 DAY'::INTERVAL
  601. ) AS "DaysInMonth"
  602. ) q ON TRUE
  603. LEFT JOIN gow_fac_id gfi ON s."corpId" = gfi.corp_id AND gfi."importId" = s."importId"
  604. LEFT JOIN gow_corp_ref cr ON cr.corporate_id = s."corpId" AND cr."importId" = s."importId"
  605. WHERE s."importId" = ${importId}
  606. GROUP BY s.well_name, s."corpId", gfi.odeq_fac_id, s.state, cr.county_parish
  607. ORDER BY s.well_name
  608. `;
  609. // Serialize Decimal objects to plain numbers for client components
  610. const serializedRows = results.map(row => ({
  611. wellName: row.wellName,
  612. corpId: row.corpId,
  613. facilityId: row.facilityId,
  614. gas: row.gas !== null ? Number(row.gas) : null,
  615. oil: row.oil !== null ? Number(row.oil) : null,
  616. water: row.water !== null ? Number(row.water) : null,
  617. state: row.state,
  618. county: row.county,
  619. daysQ1: row.daysQ1 !== null ? Number(row.daysQ1) : null,
  620. daysQ2: row.daysQ2 !== null ? Number(row.daysQ2) : null,
  621. daysQ3: row.daysQ3 !== null ? Number(row.daysQ3) : null,
  622. daysQ4: row.daysQ4 !== null ? Number(row.daysQ4) : null,
  623. daysQ1Pct: row.daysQ1Pct !== null ? Number(row.daysQ1Pct) : null,
  624. daysQ2Pct: row.daysQ2Pct !== null ? Number(row.daysQ2Pct) : null,
  625. daysQ3Pct: row.daysQ3Pct !== null ? Number(row.daysQ3Pct) : null,
  626. daysQ4Pct: row.daysQ4Pct !== null ? Number(row.daysQ4Pct) : null,
  627. gasCorr: row.gasCorr !== null ? Number(row.gasCorr) : null,
  628. oilCorr: row.oilCorr !== null ? Number(row.oilCorr) : null,
  629. waterCorr: row.waterCorr !== null ? Number(row.waterCorr) : null,
  630. isMissingFacilityId: row.isMissingFacilityId,
  631. isMissingCounty: row.isMissingCounty,
  632. }));
  633. return {
  634. success: true,
  635. data: {
  636. importId,
  637. importName: importRecord.name,
  638. layoutName: importRecord.layout.name,
  639. rows: serializedRows
  640. }
  641. };
  642. } catch (error) {
  643. console.error('Error fetching TerraTech facility summary:', error);
  644. return { success: false, error: 'Failed to fetch facility summary' };
  645. }
  646. }