imports.ts 23 KB

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