imports.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  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. // Remove accidental header rows that can break stored procedure casts
  314. await prisma.cintasInstallCalendar.deleteMany({
  315. where: {
  316. importId,
  317. OR: [
  318. { installDate: { equals: 'Date of Install', mode: 'insensitive' } },
  319. { opportunityStatus: { equals: 'Opportunity Status', mode: 'insensitive' } },
  320. { trr: { equals: 'TRR', mode: 'insensitive' } },
  321. ]
  322. }
  323. });
  324. // Generate new summary using stored procedure
  325. await prisma.$executeRaw`CALL cintas_calculate_summary(${importId})`;
  326. // Fetch the newly generated summary
  327. const cintasSummaries = await prisma.cintasSummary.findMany({
  328. where: { importId },
  329. orderBy: { weekId: 'desc' }
  330. });
  331. return {
  332. success: true,
  333. data: {
  334. importId,
  335. summaryGenerated: true,
  336. message: 'Summary generated successfully',
  337. summary: cintasSummaries.map((summary: any) => ({
  338. id: summary.id,
  339. week: summary.week,
  340. trrTotal: summary.trrTotal,
  341. fourWkAverages: summary.fourWkAverages,
  342. trrPlus4Wk: summary.trrPlus4Wk,
  343. powerAdds: summary.powerAdds,
  344. weekId: summary.weekId
  345. }))
  346. }
  347. };
  348. } catch (error) {
  349. console.error('Error generating summary:', error);
  350. return { success: false, error: 'Failed to generate summary' };
  351. }
  352. }
  353. // Get import progress (GET /api/imports/[id]/progress)
  354. export async function getImportProgress(importId: number) {
  355. try {
  356. if (!importId || isNaN(importId)) {
  357. return { success: false, error: 'Invalid import ID' };
  358. }
  359. // Check if import exists
  360. const importRecord = await prisma.import.findUnique({
  361. where: { id: importId }
  362. });
  363. if (!importRecord) {
  364. return { success: false, error: 'Import not found' };
  365. }
  366. // Check all possible tables for records
  367. const [cintasCount, gowDataCount, gowFacCount, gowCorpCount] = await Promise.all([
  368. prisma.cintasInstallCalendar.count({ where: { importId } }),
  369. prisma.gowData.count({ where: { importId } }),
  370. prisma.gowFacId.count({ where: { importId } }),
  371. prisma.gowCorpRef.count({ where: { importId } }),
  372. ]);
  373. const totalRecords = cintasCount + gowDataCount + gowFacCount + gowCorpCount;
  374. // Since we don't have status fields, we'll use record count as proxy
  375. const hasRecords = totalRecords > 0;
  376. return {
  377. success: true,
  378. data: {
  379. importId,
  380. status: hasRecords ? 'completed' : 'pending',
  381. progress: hasRecords ? 100 : 0,
  382. processedRecords: totalRecords,
  383. totalRecords: totalRecords,
  384. errorMessage: null,
  385. lastUpdated: importRecord.updatedAt,
  386. timestamp: new Date().toISOString()
  387. }
  388. };
  389. } catch (error) {
  390. console.error('Error fetching import progress:', error);
  391. return { success: false, error: 'Failed to fetch import progress' };
  392. }
  393. }
  394. // Trigger import process (POST /api/imports/[id]/trigger)
  395. export async function triggerImportProcess(importId: number) {
  396. try {
  397. if (!importId || isNaN(importId)) {
  398. console.error('[TRIGGER_IMPORT] ❌ Invalid import ID:', importId);
  399. return { success: false, error: 'Invalid import ID' };
  400. }
  401. console.log('[TRIGGER_IMPORT] 📥 Starting triggerImportProcess for ID:', importId);
  402. // Validate import exists
  403. const importRecord = await prisma.import.findUnique({
  404. where: { id: importId }
  405. });
  406. console.log('[TRIGGER_IMPORT] 📄 Import record found:', { id: importRecord?.id, hasFileId: !!importRecord?.fileId, layoutId: importRecord?.layoutId });
  407. if (!importRecord) {
  408. console.error('[TRIGGER_IMPORT] ❌ Import not found for ID:', importId);
  409. return { success: false, error: 'Import not found' };
  410. }
  411. if (!importRecord.fileId) {
  412. console.error('[TRIGGER_IMPORT] ❌ No file attached to import:', importId);
  413. return { success: false, error: 'No file attached to import' };
  414. }
  415. // Check if layout exists
  416. const layout = await prisma.layoutConfiguration.findUnique({
  417. where: { id: importRecord.layoutId }
  418. });
  419. console.log('[TRIGGER_IMPORT] 📋 Layout found:', { id: layout?.id, name: layout?.name });
  420. if (!layout) {
  421. console.error('[TRIGGER_IMPORT] ❌ No layout configuration found for ID:', importRecord.layoutId);
  422. return { success: false, error: 'No layout configuration found' };
  423. }
  424. // Check if data already exists for this import
  425. const existingRecords = await prisma.cintasInstallCalendar.count({
  426. where: { importId }
  427. });
  428. console.log('[TRIGGER_IMPORT] 📊 Existing records count:', existingRecords);
  429. if (existingRecords > 0) {
  430. console.log('[TRIGGER_IMPORT] ✅ Import already processed, returning early');
  431. return {
  432. success: true,
  433. message: 'Import already processed',
  434. importId,
  435. existingRecords
  436. };
  437. }
  438. // Use the ImportProcessor to actually process the import
  439. console.log('[TRIGGER_IMPORT] 🔄 Starting ImportProcessor for import ID:', importId);
  440. const processor = new ImportProcessor();
  441. const result = await processor.processImport(importId);
  442. console.log('[TRIGGER_IMPORT] 📈 ImportProcessor result:', { success: result.success, totalInserted: result.totalInserted, sectionsCount: result.sections?.length, errors: result.errors });
  443. if (result.success) {
  444. console.log('[TRIGGER_IMPORT] ✅ Import completed successfully');
  445. revalidatePath('/imports');
  446. return {
  447. success: true,
  448. message: 'Import process completed successfully',
  449. importId,
  450. totalInserted: result.totalInserted
  451. };
  452. } else {
  453. console.error('[TRIGGER_IMPORT] ❌ Import processing failed:', result.errors);
  454. return {
  455. success: false,
  456. error: 'Import processing failed',
  457. errors: result.errors
  458. };
  459. }
  460. } catch (error) {
  461. console.error('[TRIGGER_IMPORT] ❌ Error triggering import:', error);
  462. return { success: false, error: 'Failed to trigger import' };
  463. }
  464. }
  465. // Update import progress (for internal use by ImportProcessor)
  466. export async function updateImportProgress(
  467. importId: number,
  468. progress: {
  469. processedRecords: number;
  470. totalRecords: number;
  471. status: string;
  472. errorMessage?: string;
  473. }
  474. ) {
  475. try {
  476. // Since the Import model doesn't have these fields, we'll just return success
  477. // In a real implementation, you would need to add these fields to the schema
  478. console.log(`Import ${importId} progress: ${progress.processedRecords}/${progress.totalRecords} (${progress.status})`);
  479. return { success: true };
  480. } catch (error) {
  481. console.error('Error updating import progress:', error);
  482. return { success: false, error: 'Failed to update progress' };
  483. }
  484. }
  485. // Get imports filtered by layout configuration name
  486. export async function getImportsByLayoutName(layoutName: string) {
  487. try {
  488. // First find the layout by name
  489. const layout = await prisma.layoutConfiguration.findFirst({
  490. where: { name: layoutName },
  491. });
  492. if (!layout) {
  493. return { success: true, data: [] };
  494. }
  495. const imports = await prisma.import.findMany({
  496. where: {
  497. layoutId: layout.id,
  498. },
  499. include: {
  500. layout: true,
  501. },
  502. orderBy: {
  503. importDate: 'desc',
  504. },
  505. });
  506. return { success: true, data: imports };
  507. } catch (error) {
  508. console.error('Error fetching imports by layout:', error);
  509. return { success: false, error: 'Failed to fetch imports' };
  510. }
  511. }
  512. // TerraTech facility summary result type
  513. export interface TerraTechSummaryRow {
  514. wellName: string | null;
  515. corpId: string | null;
  516. facilityId: string | null;
  517. gas: number | null;
  518. oil: number | null;
  519. water: number | null;
  520. state: string | null;
  521. county: string | null;
  522. daysQ1: number | null;
  523. daysQ2: number | null;
  524. daysQ3: number | null;
  525. daysQ4: number | null;
  526. daysQ1Pct: number | null;
  527. daysQ2Pct: number | null;
  528. daysQ3Pct: number | null;
  529. daysQ4Pct: number | null;
  530. gasCorr: number | null;
  531. oilCorr: number | null;
  532. waterCorr: number | null;
  533. isMissingFacilityId: string | null;
  534. isMissingCounty: string | null;
  535. }
  536. // Get TerraTech facility summary for an import
  537. export async function getTerraTechFacilitySummary(importId: number) {
  538. try {
  539. if (!importId || isNaN(importId)) {
  540. return { success: false, error: 'Invalid import ID' };
  541. }
  542. // Check if import exists
  543. const importRecord = await prisma.import.findUnique({
  544. where: { id: importId },
  545. include: { layout: true }
  546. });
  547. if (!importRecord) {
  548. return { success: false, error: 'Import not found' };
  549. }
  550. // Execute the complex aggregation query using raw SQL
  551. const results = await prisma.$queryRaw<TerraTechSummaryRow[]>`
  552. SELECT
  553. s.well_name AS "wellName",
  554. s."corpId" AS "corpId",
  555. gfi.odeq_fac_id AS "facilityId",
  556. CAST(SUM(CAST(s.gas_production AS DECIMAL)) AS DECIMAL(10,2)) AS "gas",
  557. CAST(SUM(CAST(s.oil_production AS DECIMAL)) AS DECIMAL(10,2)) AS "oil",
  558. CAST(SUM(CAST(s.water_production AS DECIMAL)) AS DECIMAL(10,2)) AS "water",
  559. s.state AS "state",
  560. cr.county_parish AS "county",
  561. SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ1",
  562. SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ2",
  563. SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ3",
  564. SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ4",
  565. CAST(
  566. (SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  567. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  568. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  569. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  570. THEN 366 ELSE 365 END * 100)
  571. AS DECIMAL(5,1)
  572. ) AS "daysQ1Pct",
  573. CAST(
  574. (SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  575. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  576. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  577. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  578. THEN 366 ELSE 365 END * 100)
  579. AS DECIMAL(5,1)
  580. ) AS "daysQ2Pct",
  581. CAST(
  582. (SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  583. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  584. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  585. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  586. THEN 366 ELSE 365 END * 100)
  587. AS DECIMAL(5,1)
  588. ) AS "daysQ3Pct",
  589. CAST(
  590. (SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL /
  591. CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0
  592. AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0
  593. OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0))
  594. THEN 366 ELSE 365 END * 100)
  595. AS DECIMAL(5,1)
  596. ) AS "daysQ4Pct",
  597. 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",
  598. 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",
  599. 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",
  600. CASE WHEN gfi.odeq_fac_id IS NULL THEN 'Yes' ELSE NULL END AS "isMissingFacilityId",
  601. CASE WHEN cr.county_parish IS NULL THEN 'Yes' ELSE NULL END AS "isMissingCounty"
  602. FROM gow_data s
  603. INNER JOIN LATERAL (
  604. SELECT
  605. DATE_PART('quarter', s.month::DATE) AS "Quarter",
  606. DATE_PART('days',
  607. DATE_TRUNC('month', s.month::DATE)
  608. + '1 MONTH'::INTERVAL
  609. - '1 DAY'::INTERVAL
  610. ) AS "DaysInMonth"
  611. ) q ON TRUE
  612. LEFT JOIN gow_fac_id gfi ON s."corpId" = gfi.corp_id AND gfi."importId" = s."importId"
  613. LEFT JOIN gow_corp_ref cr ON cr.corporate_id = s."corpId" AND cr."importId" = s."importId"
  614. WHERE s."importId" = ${importId}
  615. GROUP BY s.well_name, s."corpId", gfi.odeq_fac_id, s.state, cr.county_parish
  616. ORDER BY s.well_name
  617. `;
  618. // Serialize Decimal objects to plain numbers for client components
  619. const serializedRows = results.map(row => ({
  620. wellName: row.wellName,
  621. corpId: row.corpId,
  622. facilityId: row.facilityId,
  623. gas: row.gas !== null ? Number(row.gas) : null,
  624. oil: row.oil !== null ? Number(row.oil) : null,
  625. water: row.water !== null ? Number(row.water) : null,
  626. state: row.state,
  627. county: row.county,
  628. daysQ1: row.daysQ1 !== null ? Number(row.daysQ1) : null,
  629. daysQ2: row.daysQ2 !== null ? Number(row.daysQ2) : null,
  630. daysQ3: row.daysQ3 !== null ? Number(row.daysQ3) : null,
  631. daysQ4: row.daysQ4 !== null ? Number(row.daysQ4) : null,
  632. daysQ1Pct: row.daysQ1Pct !== null ? Number(row.daysQ1Pct) : null,
  633. daysQ2Pct: row.daysQ2Pct !== null ? Number(row.daysQ2Pct) : null,
  634. daysQ3Pct: row.daysQ3Pct !== null ? Number(row.daysQ3Pct) : null,
  635. daysQ4Pct: row.daysQ4Pct !== null ? Number(row.daysQ4Pct) : null,
  636. gasCorr: row.gasCorr !== null ? Number(row.gasCorr) : null,
  637. oilCorr: row.oilCorr !== null ? Number(row.oilCorr) : null,
  638. waterCorr: row.waterCorr !== null ? Number(row.waterCorr) : null,
  639. isMissingFacilityId: row.isMissingFacilityId,
  640. isMissingCounty: row.isMissingCounty,
  641. }));
  642. return {
  643. success: true,
  644. data: {
  645. importId,
  646. importName: importRecord.name,
  647. layoutName: importRecord.layout.name,
  648. rows: serializedRows
  649. }
  650. };
  651. } catch (error) {
  652. console.error('Error fetching TerraTech facility summary:', error);
  653. return { success: false, error: 'Failed to fetch facility summary' };
  654. }
  655. }