ソースを参照

Added TerraTech page for summaries.

vtugulan 3 日 前
コミット
e64bc4ebe9

+ 182 - 0
app/actions/imports.ts

@@ -525,4 +525,186 @@ export async function updateImportProgress(
     console.error('Error updating import progress:', error);
     return { success: false, error: 'Failed to update progress' };
   }
+}
+
+// Get imports filtered by layout configuration name
+export async function getImportsByLayoutName(layoutName: string) {
+  try {
+    // First find the layout by name
+    const layout = await prisma.layoutConfiguration.findFirst({
+      where: { name: layoutName },
+    });
+
+    if (!layout) {
+      return { success: true, data: [] };
+    }
+
+    const imports = await prisma.import.findMany({
+      where: {
+        layoutId: layout.id,
+      },
+      include: {
+        layout: true,
+      },
+      orderBy: {
+        importDate: 'desc',
+      },
+    });
+
+    return { success: true, data: imports };
+  } catch (error) {
+    console.error('Error fetching imports by layout:', error);
+    return { success: false, error: 'Failed to fetch imports' };
+  }
+}
+
+// TerraTech facility summary result type
+export interface TerraTechSummaryRow {
+  wellName: string | null;
+  corpId: string | null;
+  facilityId: string | null;
+  gas: number | null;
+  oil: number | null;
+  water: number | null;
+  state: string | null;
+  county: string | null;
+  daysQ1: number | null;
+  daysQ2: number | null;
+  daysQ3: number | null;
+  daysQ4: number | null;
+  daysQ1Pct: number | null;
+  daysQ2Pct: number | null;
+  daysQ3Pct: number | null;
+  daysQ4Pct: number | null;
+  gasCorr: number | null;
+  oilCorr: number | null;
+  waterCorr: number | null;
+  isMissingFacilityId: string | null;
+  isMissingCounty: string | null;
+}
+
+// Get TerraTech facility summary for an import
+export async function getTerraTechFacilitySummary(importId: number) {
+  try {
+    if (!importId || isNaN(importId)) {
+      return { success: false, error: 'Invalid import ID' };
+    }
+
+    // Check if import exists
+    const importRecord = await prisma.import.findUnique({
+      where: { id: importId },
+      include: { layout: true }
+    });
+
+    if (!importRecord) {
+      return { success: false, error: 'Import not found' };
+    }
+
+    // Execute the complex aggregation query using raw SQL
+    const results = await prisma.$queryRaw<TerraTechSummaryRow[]>`
+      SELECT
+        s.well_name AS "wellName",
+        s."corpId" AS "corpId",
+        gfi.odeq_fac_id AS "facilityId",
+        CAST(SUM(CAST(s.gas_production AS DECIMAL)) AS DECIMAL(10,2)) AS "gas",
+        CAST(SUM(CAST(s.oil_production AS DECIMAL)) AS DECIMAL(10,2)) AS "oil",
+        CAST(SUM(CAST(s.water_production AS DECIMAL)) AS DECIMAL(10,2)) AS "water",
+        s.state AS "state",
+        cr.county_parish AS "county",
+        SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ1",
+        SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ2",
+        SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ3",
+        SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END) AS "daysQ4",
+        CAST(
+          (SUM(CASE WHEN q."Quarter" = 1 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL / 
+           CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0 
+                      AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0 
+                           OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0)) 
+                THEN 366 ELSE 365 END * 100) 
+          AS DECIMAL(5,1)
+        ) AS "daysQ1Pct",
+        CAST(
+          (SUM(CASE WHEN q."Quarter" = 2 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL / 
+           CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0 
+                      AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0 
+                           OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0)) 
+                THEN 366 ELSE 365 END * 100) 
+          AS DECIMAL(5,1)
+        ) AS "daysQ2Pct",
+        CAST(
+          (SUM(CASE WHEN q."Quarter" = 3 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL / 
+           CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0 
+                      AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0 
+                           OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0)) 
+                THEN 366 ELSE 365 END * 100) 
+          AS DECIMAL(5,1)
+        ) AS "daysQ3Pct",
+        CAST(
+          (SUM(CASE WHEN q."Quarter" = 4 THEN q."DaysInMonth" ELSE 0 END)::DECIMAL / 
+           CASE WHEN (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 4 = 0 
+                      AND (EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 100 != 0 
+                           OR EXTRACT(YEAR FROM NOW() AT TIME ZONE 'America/Detroit')::INT % 400 = 0)) 
+                THEN 366 ELSE 365 END * 100) 
+          AS DECIMAL(5,1)
+        ) AS "daysQ4Pct",
+        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",
+        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",
+        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",
+        CASE WHEN gfi.odeq_fac_id IS NULL THEN 'Yes' ELSE NULL END AS "isMissingFacilityId",
+        CASE WHEN cr.county_parish IS NULL THEN 'Yes' ELSE NULL END AS "isMissingCounty"
+      FROM gow_data s
+      INNER JOIN LATERAL (
+        SELECT
+          DATE_PART('quarter', s.month::DATE) AS "Quarter",
+          DATE_PART('days',
+            DATE_TRUNC('month', s.month::DATE)
+            + '1 MONTH'::INTERVAL
+            - '1 DAY'::INTERVAL
+          ) AS "DaysInMonth"
+      ) q ON TRUE
+      LEFT JOIN gow_fac_id gfi ON s."corpId" = gfi.corp_id AND gfi."importId" = s."importId"
+      LEFT JOIN gow_corp_ref cr ON cr.corporate_id = s."corpId" AND cr."importId" = s."importId"
+      WHERE s."importId" = ${importId}
+      GROUP BY s.well_name, s."corpId", gfi.odeq_fac_id, s.state, cr.county_parish
+      ORDER BY s.well_name
+    `;
+
+    // Serialize Decimal objects to plain numbers for client components
+    const serializedRows = results.map(row => ({
+      wellName: row.wellName,
+      corpId: row.corpId,
+      facilityId: row.facilityId,
+      gas: row.gas !== null ? Number(row.gas) : null,
+      oil: row.oil !== null ? Number(row.oil) : null,
+      water: row.water !== null ? Number(row.water) : null,
+      state: row.state,
+      county: row.county,
+      daysQ1: row.daysQ1 !== null ? Number(row.daysQ1) : null,
+      daysQ2: row.daysQ2 !== null ? Number(row.daysQ2) : null,
+      daysQ3: row.daysQ3 !== null ? Number(row.daysQ3) : null,
+      daysQ4: row.daysQ4 !== null ? Number(row.daysQ4) : null,
+      daysQ1Pct: row.daysQ1Pct !== null ? Number(row.daysQ1Pct) : null,
+      daysQ2Pct: row.daysQ2Pct !== null ? Number(row.daysQ2Pct) : null,
+      daysQ3Pct: row.daysQ3Pct !== null ? Number(row.daysQ3Pct) : null,
+      daysQ4Pct: row.daysQ4Pct !== null ? Number(row.daysQ4Pct) : null,
+      gasCorr: row.gasCorr !== null ? Number(row.gasCorr) : null,
+      oilCorr: row.oilCorr !== null ? Number(row.oilCorr) : null,
+      waterCorr: row.waterCorr !== null ? Number(row.waterCorr) : null,
+      isMissingFacilityId: row.isMissingFacilityId,
+      isMissingCounty: row.isMissingCounty,
+    }));
+
+    return {
+      success: true,
+      data: {
+        importId,
+        importName: importRecord.name,
+        layoutName: importRecord.layout.name,
+        rows: serializedRows
+      }
+    };
+  } catch (error) {
+    console.error('Error fetching TerraTech facility summary:', error);
+    return { success: false, error: 'Failed to fetch facility summary' };
+  }
 }

+ 201 - 0
app/components/terratech/TerraTechImportsTable.tsx

@@ -0,0 +1,201 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+    ColumnDef,
+    flexRender,
+    getCoreRowModel,
+    getPaginationRowModel,
+    getSortedRowModel,
+    useReactTable,
+    SortingState,
+} from "@tanstack/react-table";
+import { Button } from "@/components/ui/button";
+import {
+    Table,
+    TableBody,
+    TableCell,
+    TableHead,
+    TableHeader,
+    TableRow,
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import { format } from "date-fns";
+import { BarChart3, Calendar, ChevronLeft, ChevronRight } from "lucide-react";
+
+interface Import {
+    id: number;
+    name: string;
+    importDate: string;
+    layoutId: number;
+    layout: {
+        id: number;
+        name: string;
+    };
+}
+
+interface TerraTechImportsTableProps {
+    data: Import[];
+    onViewSummary: (importRecord: Import) => void;
+}
+
+export function TerraTechImportsTable({ data, onViewSummary }: TerraTechImportsTableProps) {
+    const [sorting, setSorting] = useState<SortingState>([
+        { id: "importDate", desc: true },
+    ]);
+
+    const columns: ColumnDef<Import>[] = [
+        {
+            accessorKey: "name",
+            header: "Name",
+            cell: ({ row }) => (
+                <div className="font-medium">{row.getValue("name")}</div>
+            ),
+        },
+        {
+            accessorKey: "layout.name",
+            header: "Layout",
+            cell: ({ row }) => (
+                <Badge variant="outline" className="dark:border-gray-600 dark:text-gray-300">
+                    {row.original.layout.name}
+                </Badge>
+            ),
+        },
+        {
+            accessorKey: "importDate",
+            header: "Import Date",
+            cell: ({ row }) => {
+                const date = new Date(row.getValue("importDate"));
+                return (
+                    <div className="flex items-center text-sm text-muted-foreground">
+                        <Calendar className="mr-2 h-3 w-3" />
+                        {format(date, "MMM d, yyyy HH:mm")}
+                    </div>
+                );
+            },
+        },
+        {
+            id: "actions",
+            header: "Actions",
+            cell: ({ row }) => (
+                <div className="flex gap-2">
+                    <Button
+                        variant="default"
+                        size="sm"
+                        onClick={() => onViewSummary(row.original)}
+                        title="View Summary"
+                        className="bg-amber-500 hover:bg-amber-600 text-white"
+                    >
+                        <BarChart3 className="h-4 w-4 mr-1" />
+                        View Summary
+                    </Button>
+                </div>
+            ),
+            enableSorting: false,
+        },
+    ];
+
+    const table = useReactTable({
+        data,
+        columns,
+        state: {
+            sorting,
+        },
+        onSortingChange: setSorting,
+        getCoreRowModel: getCoreRowModel(),
+        getPaginationRowModel: getPaginationRowModel(),
+        getSortedRowModel: getSortedRowModel(),
+        initialState: {
+            pagination: {
+                pageSize: 10,
+            },
+        },
+    });
+
+    return (
+        <div className="space-y-4">
+            <div className="rounded-md border bg-white dark:bg-gray-800 dark:border-gray-700">
+                <Table>
+                    <TableHeader>
+                        {table.getHeaderGroups().map((headerGroup) => (
+                            <TableRow key={headerGroup.id} className="dark:border-gray-700">
+                                {headerGroup.headers.map((header) => (
+                                    <TableHead
+                                        key={header.id}
+                                        onClick={header.column.getToggleSortingHandler()}
+                                        className={header.column.getCanSort() ? "cursor-pointer select-none" : ""}
+                                    >
+                                        <div className="flex items-center gap-1">
+                                            {flexRender(
+                                                header.column.columnDef.header,
+                                                header.getContext()
+                                            )}
+                                            {header.column.getIsSorted() && (
+                                                <span>
+                                                    {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
+                                                </span>
+                                            )}
+                                        </div>
+                                    </TableHead>
+                                ))}
+                            </TableRow>
+                        ))}
+                    </TableHeader>
+                    <TableBody>
+                        {table.getRowModel().rows?.length ? (
+                            table.getRowModel().rows.map((row) => (
+                                <TableRow
+                                    key={row.id}
+                                    data-state={row.getIsSelected() && "selected"}
+                                    className="dark:border-gray-700"
+                                >
+                                    {row.getVisibleCells().map((cell) => (
+                                        <TableCell key={cell.id}>
+                                            {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                                        </TableCell>
+                                    ))}
+                                </TableRow>
+                            ))
+                        ) : (
+                            <TableRow>
+                                <TableCell colSpan={columns.length} className="h-24 text-center">
+                                    No results.
+                                </TableCell>
+                            </TableRow>
+                        )}
+                    </TableBody>
+                </Table>
+            </div>
+
+            <div className="flex items-center justify-between">
+                <div className="text-sm text-muted-foreground">
+                    Showing {table.getRowModel().rows.length} of {data.length} imports
+                </div>
+                <div className="flex items-center space-x-2">
+                    <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => table.previousPage()}
+                        disabled={!table.getCanPreviousPage()}
+                    >
+                        <ChevronLeft className="h-4 w-4 mr-1" />
+                        Previous
+                    </Button>
+                    <div className="text-sm font-medium">
+                        Page {table.getState().pagination.pageIndex + 1} of{" "}
+                        {table.getPageCount()}
+                    </div>
+                    <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => table.nextPage()}
+                        disabled={!table.getCanNextPage()}
+                    >
+                        Next
+                        <ChevronRight className="h-4 w-4 ml-1" />
+                    </Button>
+                </div>
+            </div>
+        </div>
+    );
+}

+ 200 - 0
app/components/terratech/TerraTechSummaryDialog.tsx

@@ -0,0 +1,200 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+    Dialog,
+    DialogContent,
+    DialogHeader,
+    DialogTitle,
+} from "@/components/ui/dialog";
+import {
+    Table,
+    TableBody,
+    TableCell,
+    TableHead,
+    TableHeader,
+    TableRow,
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import { getTerraTechFacilitySummary, TerraTechSummaryRow } from "@/app/actions/imports";
+import { Fuel, AlertTriangle } from "lucide-react";
+
+interface TerraTechSummaryDialogProps {
+    open: boolean;
+    onOpenChange: (open: boolean) => void;
+    importId: number;
+}
+
+interface SummaryData {
+    importId: number;
+    importName: string;
+    layoutName: string;
+    rows: TerraTechSummaryRow[];
+}
+
+export function TerraTechSummaryDialog({ open, onOpenChange, importId }: TerraTechSummaryDialogProps) {
+    const [loading, setLoading] = useState(false);
+    const [error, setError] = useState<string | null>(null);
+    const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
+
+    useEffect(() => {
+        if (open && importId) {
+            loadSummary();
+        }
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [open, importId]);
+
+    async function loadSummary() {
+        setLoading(true);
+        setError(null);
+        try {
+            const result = await getTerraTechFacilitySummary(importId);
+            if (result.success && result.data) {
+                setSummaryData(result.data);
+            } else {
+                setError(result.error || 'Failed to load summary');
+            }
+        } catch {
+            setError('An unexpected error occurred');
+        } finally {
+            setLoading(false);
+        }
+    }
+
+    function formatNumber(value: number | string | null | undefined, decimals: number = 2): string {
+        if (value === null || value === undefined) return '-';
+        const num = typeof value === 'string' ? parseFloat(value) : value;
+        if (isNaN(num)) return '-';
+        return num.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
+    }
+
+    function formatPercent(value: number | string | null | undefined): string {
+        if (value === null || value === undefined) return '-';
+        const num = typeof value === 'string' ? parseFloat(value) : value;
+        if (isNaN(num)) return '-';
+        return `${num.toFixed(1)}%`;
+    }
+
+    return (
+        <Dialog open={open} onOpenChange={onOpenChange}>
+            <DialogContent className="max-w-[95vw] max-h-[90vh] dark:bg-gray-800 dark:border-gray-700">
+                <DialogHeader>
+                    <DialogTitle className="flex items-center gap-2 text-gray-900 dark:text-white">
+                        <div className="bg-amber-500 w-8 h-8 rounded-full flex items-center justify-center text-white">
+                            <Fuel className="w-4 h-4" />
+                        </div>
+                        Facility Summary
+                        {summaryData && (
+                            <Badge variant="outline" className="ml-2">
+                                {summaryData.importName}
+                            </Badge>
+                        )}
+                    </DialogTitle>
+                </DialogHeader>
+
+                {loading && (
+                    <div className="flex justify-center py-12">
+                        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500"></div>
+                    </div>
+                )}
+
+                {error && (
+                    <div className="flex flex-col items-center justify-center py-12 text-red-500">
+                        <AlertTriangle className="h-8 w-8 mb-2" />
+                        <p>{error}</p>
+                    </div>
+                )}
+
+                {!loading && !error && summaryData && (
+                    <div className="h-[70vh] overflow-auto">
+                        <div className="pr-4">
+                            <div className="mb-4 text-sm text-gray-600 dark:text-gray-300">
+                                Total records: <strong>{summaryData.rows.length}</strong>
+                            </div>
+                            <Table>
+                                <TableHeader>
+                                    <TableRow className="dark:border-gray-700">
+                                        <TableHead className="sticky left-0 bg-white dark:bg-gray-800 z-10">Well Name</TableHead>
+                                        <TableHead>CorpID</TableHead>
+                                        <TableHead>Facility ID</TableHead>
+                                        <TableHead className="text-right">Gas</TableHead>
+                                        <TableHead className="text-right">Oil</TableHead>
+                                        <TableHead className="text-right">Water</TableHead>
+                                        <TableHead>State</TableHead>
+                                        <TableHead>County</TableHead>
+                                        <TableHead className="text-right">Days Q1</TableHead>
+                                        <TableHead className="text-right">Days Q2</TableHead>
+                                        <TableHead className="text-right">Days Q3</TableHead>
+                                        <TableHead className="text-right">Days Q4</TableHead>
+                                        <TableHead className="text-right">Q1 %</TableHead>
+                                        <TableHead className="text-right">Q2 %</TableHead>
+                                        <TableHead className="text-right">Q3 %</TableHead>
+                                        <TableHead className="text-right">Q4 %</TableHead>
+                                        <TableHead className="text-right">Gas-corr</TableHead>
+                                        <TableHead className="text-right">Oil-corr</TableHead>
+                                        <TableHead className="text-right">Water-corr</TableHead>
+                                        <TableHead>Missing Facility ID</TableHead>
+                                        <TableHead>Missing County</TableHead>
+                                    </TableRow>
+                                </TableHeader>
+                                <TableBody>
+                                    {summaryData.rows.length === 0 ? (
+                                        <TableRow>
+                                            <TableCell colSpan={21} className="h-24 text-center">
+                                                No summary data available for this import.
+                                            </TableCell>
+                                        </TableRow>
+                                    ) : (
+                                        summaryData.rows.map((row, index) => (
+                                            <TableRow key={index} className="dark:border-gray-700">
+                                                <TableCell className="sticky left-0 bg-white dark:bg-gray-800 font-medium">
+                                                    {row.wellName || '-'}
+                                                </TableCell>
+                                                <TableCell>{row.corpId || '-'}</TableCell>
+                                                <TableCell>
+                                                    {row.facilityId || (
+                                                        <span className="text-amber-500">-</span>
+                                                    )}
+                                                </TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.gas)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.oil)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.water)}</TableCell>
+                                                <TableCell>{row.state || '-'}</TableCell>
+                                                <TableCell>
+                                                    {row.county || (
+                                                        <span className="text-amber-500">-</span>
+                                                    )}
+                                                </TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.daysQ1, 0)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.daysQ2, 0)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.daysQ3, 0)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.daysQ4, 0)}</TableCell>
+                                                <TableCell className="text-right">{formatPercent(row.daysQ1Pct)}</TableCell>
+                                                <TableCell className="text-right">{formatPercent(row.daysQ2Pct)}</TableCell>
+                                                <TableCell className="text-right">{formatPercent(row.daysQ3Pct)}</TableCell>
+                                                <TableCell className="text-right">{formatPercent(row.daysQ4Pct)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.gasCorr)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.oilCorr)}</TableCell>
+                                                <TableCell className="text-right">{formatNumber(row.waterCorr)}</TableCell>
+                                                <TableCell>
+                                                    {row.isMissingFacilityId && (
+                                                        <Badge variant="destructive" className="text-xs">Yes</Badge>
+                                                    )}
+                                                </TableCell>
+                                                <TableCell>
+                                                    {row.isMissingCounty && (
+                                                        <Badge variant="destructive" className="text-xs">Yes</Badge>
+                                                    )}
+                                                </TableCell>
+                                            </TableRow>
+                                        ))
+                                    )}
+                                </TableBody>
+                            </Table>
+                        </div>
+                    </div>
+                )}
+            </DialogContent>
+        </Dialog>
+    );
+}

+ 14 - 6
app/dashboard/page.tsx

@@ -2,7 +2,7 @@
 
 import { useRouter } from 'next/navigation';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { FileText, Layout, Upload, BookOpen } from 'lucide-react';
+import { FileText, Layout, Upload, BookOpen, Fuel } from 'lucide-react';
 import Image from 'next/image';
 import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
 
@@ -83,18 +83,26 @@ export default function DashboardPage() {
       ),
       href: '/cintas-calendar-summary',
       color: 'bg-blue-600'
+    },
+    {
+      id: 'terratech-facility-summaries',
+      title: 'TerraTech Facility Summaries',
+      description: 'View Gas, Oil, and Water production summaries',
+      icon: <Fuel className="w-8 h-8" />,
+      href: '/terratech-facility-summaries',
+      color: 'bg-amber-500'
     }
   ];
 
   // Filter apps based on authentication and authorization
   const filteredApps = allApps.filter(app => {
-    // All apps except Cintas require admin role
-    if (app.id !== 'cintas-calendar-summary') {
-      return hasAdminRole;
+    // Cintas summary has special access rules
+    if (app.id === 'cintas-calendar-summary') {
+      return canAccessCintasSummary;
     }
 
-    // Cintas summary has special access rules
-    return canAccessCintasSummary;
+    // All other apps require admin role
+    return hasAdminRole;
   });
 
   const handleAppClick = (href: string) => {

+ 22 - 0
app/terratech-facility-summaries/loading.tsx

@@ -0,0 +1,22 @@
+import { Fuel } from 'lucide-react';
+
+export default function Loading() {
+    return (
+        <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
+            <div className="container mx-auto px-4 py-8">
+                <div className="flex items-center gap-4 mb-8">
+                    <div className="bg-amber-500 w-12 h-12 rounded-full flex items-center justify-center text-white">
+                        <Fuel className="w-6 h-6" />
+                    </div>
+                    <div>
+                        <div className="h-8 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
+                        <div className="h-4 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mt-2"></div>
+                    </div>
+                </div>
+                <div className="flex justify-center py-12">
+                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500"></div>
+                </div>
+            </div>
+        </div>
+    );
+}

+ 147 - 0
app/terratech-facility-summaries/page.tsx

@@ -0,0 +1,147 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { FileText, Fuel } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+import { getImportsByLayoutName } from '@/app/actions/imports';
+import { TerraTechImportsTable } from '@/app/components/terratech/TerraTechImportsTable';
+import { TerraTechSummaryDialog } from '@/app/components/terratech/TerraTechSummaryDialog';
+import { useToast } from '@/hooks/use-toast';
+
+interface Import {
+    id: number;
+    name: string;
+    importDate: string;
+    layoutId: number;
+    layout: {
+        id: number;
+        name: string;
+    };
+}
+
+interface RawImportData {
+    id: number;
+    name: string;
+    importDate: Date | string;
+    layoutId: number;
+    layout: {
+        id: number;
+        name: string;
+    };
+}
+
+const LAYOUT_NAME = 'TerraTech - GasOilWater Summary';
+
+export default function TerraTechFacilitySummariesPage() {
+    const [imports, setImports] = useState<Import[]>([]);
+    const [loading, setLoading] = useState(true);
+    const [summaryDialogOpen, setSummaryDialogOpen] = useState(false);
+    const [selectedImportId, setSelectedImportId] = useState<number | null>(null);
+    const { toast } = useToast();
+
+    const loadImports = useCallback(async () => {
+        try {
+            const result = await getImportsByLayoutName(LAYOUT_NAME);
+            if (result.success && result.data) {
+                const transformedImports = result.data.map((item: RawImportData) => ({
+                    id: item.id,
+                    name: item.name,
+                    importDate: item.importDate instanceof Date
+                        ? item.importDate.toISOString()
+                        : String(item.importDate),
+                    layoutId: item.layoutId,
+                    layout: {
+                        id: item.layout.id,
+                        name: item.layout.name,
+                    },
+                }));
+                setImports(transformedImports);
+            } else {
+                toast({
+                    title: 'Error',
+                    description: result.error || 'Failed to load imports',
+                    variant: 'destructive',
+                });
+                setImports([]);
+            }
+        } catch {
+            toast({
+                title: 'Error',
+                description: 'Failed to load imports',
+                variant: 'destructive',
+            });
+        } finally {
+            setLoading(false);
+        }
+    }, [toast]);
+
+    useEffect(() => {
+        loadImports();
+    }, [loadImports]);
+
+    function handleViewSummary(importRecord: Import) {
+        setSelectedImportId(importRecord.id);
+        setSummaryDialogOpen(true);
+    }
+
+    if (loading) {
+        return (
+            <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
+                <div className="container mx-auto px-4 py-8">
+                    <div className="flex justify-center">
+                        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
+                    </div>
+                </div>
+            </div>
+        );
+    }
+
+    return (
+        <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
+            <div className="container mx-auto px-4 py-8">
+                <div className="flex justify-between items-center mb-8">
+                    <div className="flex items-center gap-4">
+                        <div className="bg-amber-500 w-12 h-12 rounded-full flex items-center justify-center text-white">
+                            <Fuel className="w-6 h-6" />
+                        </div>
+                        <div>
+                            <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
+                                TerraTech Facility Summaries
+                            </h1>
+                            <p className="text-gray-600 dark:text-gray-300">
+                                View Gas, Oil, and Water production summaries for imported data
+                            </p>
+                        </div>
+                    </div>
+                </div>
+
+                {imports.length === 0 ? (
+                    <Card className="dark:bg-gray-800 dark:border-gray-700">
+                        <CardContent className="flex flex-col items-center justify-center py-12">
+                            <FileText className="h-12 w-12 text-muted-foreground mb-4" />
+                            <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">No imports found</h3>
+                            <p className="text-muted-foreground mb-4 dark:text-gray-300 text-center">
+                                No imports using the &quot;{LAYOUT_NAME}&quot; layout configuration were found.
+                                <br />
+                                Import data using this layout to see it here.
+                            </p>
+                        </CardContent>
+                    </Card>
+                ) : (
+                    <div className="w-full">
+                        <TerraTechImportsTable
+                            data={imports}
+                            onViewSummary={handleViewSummary}
+                        />
+                    </div>
+                )}
+
+                <TerraTechSummaryDialog
+                    open={summaryDialogOpen}
+                    onOpenChange={setSummaryDialogOpen}
+                    importId={selectedImportId || 0}
+                />
+            </div>
+        </div>
+    );
+}