diff --git a/src/app/(portal)/dashboard/loading.tsx b/src/app/(portal)/dashboard/loading.tsx deleted file mode 100644 index 804a6f4..0000000 --- a/src/app/(portal)/dashboard/loading.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Card, CardContent, CardHeader } from '@/components/ui/card'; - -export default function DashboardLoading() { - return ( -
-

Dashboard

-
-
- {[...Array(4)].map((_, i) => ( - - -
- - -
- - - ))} -
-
- {[...Array(2)].map((_, i) => ( - - -
- - -
- {[...Array(5)].map((_, j) => ( -
- ))} -
- - - ))} -
-
-
- ); -} diff --git a/src/app/(portal)/inventory/[category]/detail/loading.tsx b/src/app/(portal)/inventory/[category]/detail/loading.tsx deleted file mode 100644 index c3cb890..0000000 --- a/src/app/(portal)/inventory/[category]/detail/loading.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Card, CardContent } from '@/components/ui/card'; - -export default function InventoryDetailLoading() { - return ( -
-
-
-
-
-
-
-
- - -
- {[...Array(8)].map((_, i) => ( -
- ))} -
- - -
- ); -} diff --git a/src/app/(portal)/inventory/[category]/detail/page.tsx b/src/app/(portal)/inventory/[category]/detail/page.tsx index 7c46536..cd03587 100644 --- a/src/app/(portal)/inventory/[category]/detail/page.tsx +++ b/src/app/(portal)/inventory/[category]/detail/page.tsx @@ -52,11 +52,9 @@ async function InventoryDetailData({ plant?: string; warehouse?: string; }) { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); diff --git a/src/app/(portal)/inventory/finished-goods/page.tsx b/src/app/(portal)/inventory/finished-goods/page.tsx index 4548f51..8c8fc96 100644 --- a/src/app/(portal)/inventory/finished-goods/page.tsx +++ b/src/app/(portal)/inventory/finished-goods/page.tsx @@ -9,29 +9,26 @@ import { import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function FinishedGoodsInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; const sub = userIsSubUser ? 1 : 0; const [inventory, canShip] = await Promise.all([ - cachedQuery( - { key: `inv:${custId}:finished-goods:summary:${sub}`, ttlSeconds: 300 }, - () => getFinishedGoodsSummary(custId, dbName, sub) + getFinishedGoodsSummary( + activeCompany.epicor_cust_id, + dbName, + sub ).catch(() => []), hasPermission('create_shipment_request'), ]); diff --git a/src/app/(portal)/inventory/processed-other/page.tsx b/src/app/(portal)/inventory/processed-other/page.tsx index 8c5af7b..6572b0a 100644 --- a/src/app/(portal)/inventory/processed-other/page.tsx +++ b/src/app/(portal)/inventory/processed-other/page.tsx @@ -8,28 +8,25 @@ import { import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function ProcessedOtherInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; const sub = userIsSubUser ? 1 : 0; - const inventory = await cachedQuery( - { key: `inv:${custId}:processed-other:summary:${sub}`, ttlSeconds: 300 }, - () => getProcessedOtherSummary(custId, dbName, sub) + const inventory = await getProcessedOtherSummary( + activeCompany.epicor_cust_id, + dbName, + sub ).catch(() => []); return ; diff --git a/src/app/(portal)/inventory/processed-rr/page.tsx b/src/app/(portal)/inventory/processed-rr/page.tsx index 174866c..8bef9a7 100644 --- a/src/app/(portal)/inventory/processed-rr/page.tsx +++ b/src/app/(portal)/inventory/processed-rr/page.tsx @@ -8,16 +8,13 @@ import { import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function ProcessedRRInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); @@ -28,12 +25,12 @@ async function ProcessedRRInventoryData() { redirect('/inventory'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; - const inventory = await cachedQuery( - { key: `inv:${custId}:processed-rr:summary`, ttlSeconds: 300 }, - () => getProcessedRRSummary(custId, dbName, userIsSubUser) + const inventory = await getProcessedRRSummary( + activeCompany.epicor_cust_id, + dbName, + userIsSubUser ).catch(() => []); return ; diff --git a/src/app/(portal)/inventory/unprocessed-rr/page.tsx b/src/app/(portal)/inventory/unprocessed-rr/page.tsx index f6d1520..f732bff 100644 --- a/src/app/(portal)/inventory/unprocessed-rr/page.tsx +++ b/src/app/(portal)/inventory/unprocessed-rr/page.tsx @@ -8,16 +8,13 @@ import { import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function UnprocessedRRInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); @@ -28,12 +25,12 @@ async function UnprocessedRRInventoryData() { redirect('/inventory'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; - const inventory = await cachedQuery( - { key: `inv:${custId}:unprocessed-rr:summary`, ttlSeconds: 300 }, - () => getUnprocessedRRSummary(custId, dbName, userIsSubUser) + const inventory = await getUnprocessedRRSummary( + activeCompany.epicor_cust_id, + dbName, + userIsSubUser ).catch(() => []); return ; diff --git a/src/app/(portal)/inventory/unprocessed/page.tsx b/src/app/(portal)/inventory/unprocessed/page.tsx index 4cb1fe2..7f4523c 100644 --- a/src/app/(portal)/inventory/unprocessed/page.tsx +++ b/src/app/(portal)/inventory/unprocessed/page.tsx @@ -8,16 +8,13 @@ import { import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function UnprocessedInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); @@ -28,12 +25,12 @@ async function UnprocessedInventoryData() { redirect('/inventory'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; - const inventory = await cachedQuery( - { key: `inv:${custId}:unprocessed:summary`, ttlSeconds: 300 }, - () => getUnprocessedSummary(custId, dbName, userIsSubUser) + const inventory = await getUnprocessedSummary( + activeCompany.epicor_cust_id, + dbName, + userIsSubUser ).catch(() => []); return ; diff --git a/src/app/(portal)/inventory/wip/page.tsx b/src/app/(portal)/inventory/wip/page.tsx index 92ff228..c9755de 100644 --- a/src/app/(portal)/inventory/wip/page.tsx +++ b/src/app/(portal)/inventory/wip/page.tsx @@ -6,26 +6,23 @@ import { getQuestSession, getActiveCompany, isSubUser } from '@/lib/permissions' import { redirect } from 'next/navigation'; import { InventorySummaryTable } from '@/components/inventory/inventory-summary-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; async function WIPInventoryData() { - const [session, activeCompany, userIsSubUser] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - isSubUser(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + const userIsSubUser = await isSubUser(); if (!session || !activeCompany) { redirect('/select-company'); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; const sub = userIsSubUser ? 1 : 0; - const inventory = await cachedQuery( - { key: `inv:${custId}:wip:summary:${sub}`, ttlSeconds: 300 }, - () => getWorkInProgressSummary(custId, dbName, sub) + const inventory = await getWorkInProgressSummary( + activeCompany.epicor_cust_id, + dbName, + sub ).catch(() => []); return ; diff --git a/src/app/(portal)/orders/loading.tsx b/src/app/(portal)/orders/loading.tsx deleted file mode 100644 index d7d0501..0000000 --- a/src/app/(portal)/orders/loading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Card, CardContent } from '@/components/ui/card'; - -export default function OrdersLoading() { - return ( -
-

Orders

-

- View your most recent orders and order acknowledgements -

- - -
- {[...Array(10)].map((_, i) => ( -
- ))} -
- - -
- ); -} diff --git a/src/app/(portal)/orders/page.tsx b/src/app/(portal)/orders/page.tsx index 9d33495..e191cc8 100644 --- a/src/app/(portal)/orders/page.tsx +++ b/src/app/(portal)/orders/page.tsx @@ -4,29 +4,23 @@ import { getTop100Orders } from '@/services/orders'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; import { OrdersTable } from '@/components/orders/orders-table'; import { Card, CardContent } from '@/components/ui/card'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; async function OrdersData() { - const [session, activeCompany] = await Promise.all([ - getQuestSession(), - getActiveCompany(), - ]); + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); if (!session || !activeCompany) { redirect('/select-company'); } - const custId = activeCompany.epicor_cust_id; - - const orders = await cachedQuery( - { key: `orders:${custId}:top100`, ttlSeconds: 300 }, - () => getTop100Orders(custId) - ).catch((err) => { - console.error('Failed to fetch orders:', err); - return []; - }); + const orders = await getTop100Orders(activeCompany.epicor_cust_id).catch( + (err) => { + console.error('Failed to fetch orders:', err); + return []; + } + ); return ; } diff --git a/src/app/(portal)/shipments/loading.tsx b/src/app/(portal)/shipments/loading.tsx deleted file mode 100644 index be00c7a..0000000 --- a/src/app/(portal)/shipments/loading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Card, CardContent } from '@/components/ui/card'; - -export default function ShipmentsLoading() { - return ( -
-

Shipments

-

- View your most recent shipments and BOL details -

- - -
- {[...Array(10)].map((_, i) => ( -
- ))} -
- - -
- ); -} diff --git a/src/app/api/available-coils/route.ts b/src/app/api/available-coils/route.ts index 6420c1f..451f24a 100644 --- a/src/app/api/available-coils/route.ts +++ b/src/app/api/available-coils/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getQuestSession, getActiveCompany, requirePermission } from '@/lib/permissions'; import { getAvailableCoils } from '@/services/available-coils'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -29,12 +28,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'jobNumber is required' }, { status: 400 }); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; - const coils = await cachedQuery( - { key: `avail-coils:${custId}:${jobNumber}`, ttlSeconds: 120 }, - () => getAvailableCoils(custId, dbName, jobNumber) + const coils = await getAvailableCoils( + activeCompany.epicor_cust_id, + dbName, + jobNumber ); return NextResponse.json(coils); diff --git a/src/app/api/coil-activity/coil-by-coil/route.ts b/src/app/api/coil-activity/coil-by-coil/route.ts index d671a16..bbcb385 100644 --- a/src/app/api/coil-activity/coil-by-coil/route.ts +++ b/src/app/api/coil-activity/coil-by-coil/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; import { getCoilByCoil } from '@/services/coil-activity'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -20,13 +19,8 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'jobNum is required' }, { status: 400 }); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `coil:${custId}:cbc:${jobNum}`, ttlSeconds: 300 }, - () => getCoilByCoil(jobNum, custId) - ); + const data = await getCoilByCoil(jobNum, activeCompany.epicor_cust_id); return NextResponse.json(data); } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/src/app/api/coil-activity/receipts/route.ts b/src/app/api/coil-activity/receipts/route.ts index 300be5b..4a3e49c 100644 --- a/src/app/api/coil-activity/receipts/route.ts +++ b/src/app/api/coil-activity/receipts/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getCoilReceipts } from '@/services/coil-activity'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -45,12 +44,11 @@ export async function GET(request: NextRequest) { ); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `coil:${custId}:receipts:${startDate}:${endDate}`, ttlSeconds: 300 }, - () => getCoilReceipts(custId, startDate, endDate) + const data = await getCoilReceipts( + activeCompany.epicor_cust_id, + startDate, + endDate ); return NextResponse.json(data); } catch (err) { diff --git a/src/app/api/coil-activity/usage/route.ts b/src/app/api/coil-activity/usage/route.ts index 08838f8..d98e1e0 100644 --- a/src/app/api/coil-activity/usage/route.ts +++ b/src/app/api/coil-activity/usage/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getCoilUsage } from '@/services/coil-activity'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -47,12 +46,11 @@ export async function GET(request: NextRequest) { ); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `coil:${custId}:usage:${startDate}:${endDate}`, ttlSeconds: 300 }, - () => getCoilUsage(custId, startDate, endDate) + const data = await getCoilUsage( + activeCompany.epicor_cust_id, + startDate, + endDate ); return NextResponse.json(data); } catch (err) { diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts index a521c2a..876330c 100644 --- a/src/app/api/dashboard/route.ts +++ b/src/app/api/dashboard/route.ts @@ -6,7 +6,6 @@ import { } from '@/services/dashboard'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; import { getUnreadAlertCount } from '@/services/notifications'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -18,23 +17,12 @@ export async function GET() { return NextResponse.json({ error: 'No active company' }, { status: 401 }); } - const custId = activeCompany.epicor_cust_id; - try { const [orders, shipments, inventorySummary, unreadNotifications] = await Promise.all([ - cachedQuery( - { key: `dash:${custId}:orders`, ttlSeconds: 180 }, - () => getRecentOrders(custId) - ).catch(() => []), - cachedQuery( - { key: `dash:${custId}:shipments`, ttlSeconds: 180 }, - () => getRecentShipments(custId) - ).catch(() => []), - cachedQuery( - { key: `dash:${custId}:inv-summary`, ttlSeconds: 180 }, - () => getInventorySummary(custId) - ).catch(() => ({ + getRecentOrders(activeCompany.epicor_cust_id).catch(() => []), + getRecentShipments(activeCompany.epicor_cust_id).catch(() => []), + getInventorySummary(activeCompany.epicor_cust_id).catch(() => ({ wip_count: 0, finished_goods_count: 0, unprocessed_count: 0, diff --git a/src/app/api/inventory/details/route.ts b/src/app/api/inventory/details/route.ts index 8950846..fc25732 100644 --- a/src/app/api/inventory/details/route.ts +++ b/src/app/api/inventory/details/route.ts @@ -8,7 +8,6 @@ import { getInventoryDetails, type InventoryCategory, } from '@/services/inventory'; -import { cachedQuery } from '@/lib/cache'; const VALID_CATEGORIES: InventoryCategory[] = [ 'wip', @@ -61,14 +60,15 @@ export async function GET(request: NextRequest) { return NextResponse.json({ data: [] }); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; const sub = userIsSubUser ? 1 : 0; - const filterKey = [part, plant, warehouse].filter(Boolean).join(':') || 'all'; - const details = await cachedQuery( - { key: `inv:${custId}:${category}:detail:${sub}:${filterKey}`, ttlSeconds: 300 }, - () => getInventoryDetails(category, custId, dbName, sub, { partNum: part, plant, warehouse }) + const details = await getInventoryDetails( + category, + activeCompany.epicor_cust_id, + dbName, + sub, + { partNum: part, plant, warehouse } ); return NextResponse.json({ data: details }); diff --git a/src/app/api/jobs/[jobNum]/traveler/route.ts b/src/app/api/jobs/[jobNum]/traveler/route.ts index 77f1634..810f8b2 100644 --- a/src/app/api/jobs/[jobNum]/traveler/route.ts +++ b/src/app/api/jobs/[jobNum]/traveler/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; import { getJobTraveler } from '@/services/jobs'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -17,13 +16,9 @@ export async function GET( } const { jobNum } = await params; - const custId = activeCompany.epicor_cust_id; try { - const data = await cachedQuery( - { key: `jobs:${custId}:traveler:${jobNum}`, ttlSeconds: 600 }, - () => getJobTraveler(jobNum) - ); + const data = await getJobTraveler(jobNum); if (data === null) { return NextResponse.json({ error: 'Job not found' }, { status: 404 }); diff --git a/src/app/api/jobs/status/route.ts b/src/app/api/jobs/status/route.ts index 59bd83c..d17562f 100644 --- a/src/app/api/jobs/status/route.ts +++ b/src/app/api/jobs/status/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; import { getJobStatusByPlant } from '@/services/jobs'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -13,13 +12,8 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'No active company' }, { status: 401 }); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `jobs:${custId}:status`, ttlSeconds: 300 }, - () => getJobStatusByPlant(custId) - ); + const data = await getJobStatusByPlant(activeCompany.epicor_cust_id); return NextResponse.json(data); } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/src/app/api/orders/[orderNum]/route.ts b/src/app/api/orders/[orderNum]/route.ts index 0114e43..6727a22 100644 --- a/src/app/api/orders/[orderNum]/route.ts +++ b/src/app/api/orders/[orderNum]/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getOrderAcknowledgement } from '@/services/orders'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -26,12 +25,10 @@ export async function GET( ); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `order:${custId}:ack:${orderNumInt}`, ttlSeconds: 600 }, - () => getOrderAcknowledgement(orderNumInt, custId) + const data = await getOrderAcknowledgement( + orderNumInt, + activeCompany.epicor_cust_id ); if (!data) { diff --git a/src/app/api/ship-to-addresses/route.ts b/src/app/api/ship-to-addresses/route.ts index d76ea2c..06d02b0 100644 --- a/src/app/api/ship-to-addresses/route.ts +++ b/src/app/api/ship-to-addresses/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from 'next/server'; import { getQuestSession, getActiveCompany, isSubUser } from '@/lib/permissions'; import { getShipToAddresses } from '@/services/ship-to-addresses'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -17,14 +16,13 @@ export async function GET() { return NextResponse.json({ error: 'No active company' }, { status: 401 }); } - const custId = activeCompany.epicor_cust_id; const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`; const sub = await isSubUser(); - const subKey = sub ? 1 : 0; - const addresses = await cachedQuery( - { key: `shipto:${custId}:${subKey}`, ttlSeconds: 900 }, - () => getShipToAddresses(custId, dbName, sub) + const addresses = await getShipToAddresses( + activeCompany.epicor_cust_id, + dbName, + sub ); return NextResponse.json(addresses); diff --git a/src/app/api/shipments/[bol]/route.ts b/src/app/api/shipments/[bol]/route.ts index f5b01f5..03f436a 100644 --- a/src/app/api/shipments/[bol]/route.ts +++ b/src/app/api/shipments/[bol]/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from 'next/server'; import { getBOLDetail } from '@/services/shipments'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -22,13 +21,8 @@ export async function GET( return NextResponse.json({ error: 'Invalid BOL number' }, { status: 400 }); } - const custId = activeCompany.epicor_cust_id; - try { - const data = await cachedQuery( - { key: `bol:${custId}:${bolNum}`, ttlSeconds: 600 }, - () => getBOLDetail(bolNum, custId) - ); + const data = await getBOLDetail(bolNum, activeCompany.epicor_cust_id); if (!data) { return NextResponse.json({ error: 'BOL not found' }, { status: 404 }); diff --git a/src/app/api/shipments/route.ts b/src/app/api/shipments/route.ts index c138637..435176b 100644 --- a/src/app/api/shipments/route.ts +++ b/src/app/api/shipments/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from 'next/server'; import { getTop100Shipments } from '@/services/shipments'; import { getQuestSession, getActiveCompany } from '@/lib/permissions'; -import { cachedQuery } from '@/lib/cache'; export const dynamic = 'force-dynamic'; @@ -13,13 +12,8 @@ export async function GET() { return NextResponse.json({ error: 'No active company' }, { status: 401 }); } - const custId = activeCompany.epicor_cust_id; - try { - const shipments = await cachedQuery( - { key: `ship:${custId}:top100`, ttlSeconds: 300 }, - () => getTop100Shipments(custId) - ); + const shipments = await getTop100Shipments(activeCompany.epicor_cust_id); return NextResponse.json(shipments); } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/src/lib/cache.ts b/src/lib/cache.ts deleted file mode 100644 index 9663edc..0000000 --- a/src/lib/cache.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ensureRedisConnected } from '@/lib/redis'; - -type CachedQueryOptions = { - key: string; - ttlSeconds: number; -}; - -/** - * Wraps any async data fetch with Redis caching. - * - * - On cache hit: returns parsed JSON from Redis - * - On cache miss: calls fetchFn, stores result in Redis with TTL, returns result - * - On any Redis error: falls through to fetchFn (graceful degradation) - */ -export async function cachedQuery( - options: CachedQueryOptions, - fetchFn: () => Promise -): Promise { - const { key, ttlSeconds } = options; - - try { - const redis = await ensureRedisConnected(); - const cached = await redis.get(key); - - if (cached !== null) { - return JSON.parse(cached) as T; - } - } catch { - // Redis unavailable — fall through to fetchFn - } - - const result = await fetchFn(); - - try { - const redis = await ensureRedisConnected(); - await redis.set(key, JSON.stringify(result), { EX: ttlSeconds }); - } catch { - // Redis unavailable — result is still returned - } - - return result; -} - -/** - * Delete all keys matching a pattern (e.g. `inv:ACM:*`). - * Uses SCAN to avoid blocking Redis. - */ -export async function invalidatePattern(pattern: string): Promise { - try { - const redis = await ensureRedisConnected(); - let deleted = 0; - - for await (const key of redis.scanIterator({ MATCH: pattern })) { - await redis.del(key); - deleted++; - } - - return deleted; - } catch { - return 0; - } -} - -/** - * Delete a single cache key. - */ -export async function invalidateKey(key: string): Promise { - try { - const redis = await ensureRedisConnected(); - const result = await redis.del(key); - return result > 0; - } catch { - return false; - } -} diff --git a/src/lib/epicor.ts b/src/lib/epicor.ts index 09c9cda..e3b53df 100644 --- a/src/lib/epicor.ts +++ b/src/lib/epicor.ts @@ -93,15 +93,7 @@ async function getPool(): Promise { } // Create new connection - connectionPromise = new sql.ConnectionPool({ - ...config, - pool: { - max: 15, - min: 2, - idleTimeoutMillis: 60000, - acquireTimeoutMillis: 30000, - }, - }) + connectionPromise = new sql.ConnectionPool(config) .connect() .then((connectedPool) => { pool = connectedPool; diff --git a/src/lib/redis.ts b/src/lib/redis.ts deleted file mode 100644 index b70a195..0000000 --- a/src/lib/redis.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createClient, type RedisClientType } from 'redis'; - -const globalForRedis = globalThis as unknown as { - redis: RedisClientType | undefined; -}; - -function createRedisClient(): RedisClientType { - const client = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379', - }); - - client.on('error', (err) => { - console.error('Redis client error:', err); - }); - - return client as RedisClientType; -} - -export const redis: RedisClientType = - globalForRedis.redis ?? createRedisClient(); - -if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis; - -export async function ensureRedisConnected(): Promise { - if (!redis.isOpen) { - await redis.connect(); - } - return redis; -} diff --git a/src/services/dashboard.ts b/src/services/dashboard.ts index 29b61a7..84be6be 100644 --- a/src/services/dashboard.ts +++ b/src/services/dashboard.ts @@ -9,7 +9,8 @@ * we need into plain objects. */ -import { execQuery, execStoredProc, getPortalDbName } from '@/lib/epicor'; +import sql from 'mssql'; +import { execQuery, getPortalDbName } from '@/lib/epicor'; import { getTop100Shipments } from '@/services/shipments'; export type DashboardOrder = { @@ -106,7 +107,7 @@ export async function getRecentShipments( /** * Get inventory summary counts for dashboard. * - * Calls the real portal inventory stored procedures via the shared connection pool: + * Calls the real portal inventory stored procedures: * - PortalWorkInProgressInventorySummaryV6 (@Customer, @DBNAME, @SUBUSER) * - PortalFinishedGoodsInventorySummaryV6 (@Customer, @DBNAME, @SUBUSER) * - PortalUnprocessedInventorySummary (@CUSTID, @DBNAME) @@ -119,54 +120,89 @@ export async function getInventorySummary( ): Promise { const dbName = getPortalDbName(); - const [wipRows, fgRows, unprocessedRows] = await Promise.all([ - execStoredProc[]>( - 'PortalWorkInProgressInventorySummaryV6', - { Customer: custId, DBNAME: dbName, SUBUSER: 0 } - ).catch(() => []), - execStoredProc[]>( - 'PortalFinishedGoodsInventorySummaryV6', - { Customer: custId, DBNAME: dbName, SUBUSER: 0 } - ).catch(() => []), - execStoredProc[]>( - 'PortalUnprocessedInventorySummary', - { CUSTID: custId, DBNAME: dbName } - ).catch(() => []), - ]); - - let wipCount = 0; - let fgCount = 0; - let unprocessedCount = 0; - let totalWeight = 0; - - for (const r of wipRows) { - wipCount += Number(r.Rows ?? 0); - totalWeight += Number(r.OnHandQty ?? 0); - } - - for (const r of fgRows) { - fgCount += Number(r.Rows ?? 0); - totalWeight += Number(r.OnHandQty ?? 0); - } - - for (const r of unprocessedRows) { - unprocessedCount += Number(r.Rows ?? 0); - totalWeight += Number(r.OnHandQty ?? 0); - } - - return { - wip_count: wipCount, - finished_goods_count: fgCount, - unprocessed_count: unprocessedCount, - total_weight: totalWeight, + const config = { + server: process.env.MSSQL_HOST || '', + database: process.env.MSSQL_DATABASE || '', + user: process.env.MSSQL_USER || '', + password: process.env.MSSQL_PASSWORD || '', + port: parseInt(process.env.MSSQL_PORT || '1433', 10), + options: { + encrypt: false, + trustServerCertificate: true, + connectTimeout: 30000, + requestTimeout: 120000, + }, }; + + const pool = await sql.connect(config); + try { + // Run all three inventory SPs in parallel + const [wipResult, fgResult, unprocessedResult] = await Promise.all([ + pool + .request() + .input('Customer', custId) + .input('DBNAME', dbName) + .input('SUBUSER', 0) + .execute('PortalWorkInProgressInventorySummaryV6') + .catch(() => null), + pool + .request() + .input('Customer', custId) + .input('DBNAME', dbName) + .input('SUBUSER', 0) + .execute('PortalFinishedGoodsInventorySummaryV6') + .catch(() => null), + pool + .request() + .input('CUSTID', custId) + .input('DBNAME', dbName) + .execute('PortalUnprocessedInventorySummary') + .catch(() => null), + ]); + + // Aggregate: sum Rows for counts, sum OnHandQty for total weight + let wipCount = 0; + let fgCount = 0; + let unprocessedCount = 0; + let totalWeight = 0; + + if (wipResult) { + for (const r of wipResult.recordset) { + wipCount += Number(r.Rows ?? 0); + totalWeight += Number(r.OnHandQty ?? 0); + } + } + + if (fgResult) { + for (const r of fgResult.recordset) { + fgCount += Number(r.Rows ?? 0); + totalWeight += Number(r.OnHandQty ?? 0); + } + } + + if (unprocessedResult) { + for (const r of unprocessedResult.recordset) { + unprocessedCount += Number(r.Rows ?? 0); + totalWeight += Number(r.OnHandQty ?? 0); + } + } + + return { + wip_count: wipCount, + finished_goods_count: fgCount, + unprocessed_count: unprocessedCount, + total_weight: totalWeight, + }; + } finally { + // Don't close the pool — mssql reuses it globally + } } /** * Get unread notification count */ export async function getUnreadNotificationCount( - _userId: string + userId: string ): Promise { // This would query the quest_user_notification_alert_read table // For now, return 0 as placeholder diff --git a/src/services/shipments.ts b/src/services/shipments.ts index 1d41345..f65aec9 100644 --- a/src/services/shipments.ts +++ b/src/services/shipments.ts @@ -5,7 +5,8 @@ * SP columns are PascalCase; we map them to snake_case for the UI. */ -import { execQuery, execStoredProc, getPortalDbName } from '@/lib/epicor'; +import sql from 'mssql'; +import { execQuery, getPortalDbName } from '@/lib/epicor'; import type { BOLData } from '@/types/shipments'; export type ShipmentRow = { @@ -19,32 +20,56 @@ export type ShipmentRow = { /** * Get top 100 shipments for a customer. * - * Uses the shared connection pool via execStoredProc, then immediately extracts - * only the fields we need into plain objects (RSC-safe). + * Bypasses execStoredProc to avoid holding 7k+ mssql row objects in scope + * (which causes Next.js RSC serialization to blow the stack for large customers). + * Instead, we query directly and immediately extract only the fields we need. */ export async function getTop100Shipments( custId: string ): Promise { const dbName = getPortalDbName(); - const result = await execStoredProc[]>( - 'portal_GetShipmentsV1', - { CustID: custId, DBNAME: dbName } - ); + const config = { + server: process.env.MSSQL_HOST || '', + database: process.env.MSSQL_DATABASE || '', + user: process.env.MSSQL_USER || '', + password: process.env.MSSQL_PASSWORD || '', + port: parseInt(process.env.MSSQL_PORT || '1433', 10), + options: { + encrypt: false, + trustServerCertificate: true, + connectTimeout: 30000, + requestTimeout: 120000, + }, + }; - // Immediately extract only the fields we need into plain objects. - // This prevents mssql row metadata from leaking into RSC serialization. - const rows: ShipmentRow[] = result.map((r) => ({ - bol_num: String(r.BOLNum ?? ''), - ship_date: r.ShipDate ? new Date(r.ShipDate as string).toISOString() : '', - ship_to: String(r.ShipToLoc ?? '').replace(/, /g, '\n'), - plant: String(r.PlantName ?? ''), - weight: Number(r.Pounds ?? 0), - })); + const pool = await sql.connect(config); + try { + const result = await pool.request() + .input('CustID', custId) + .input('DBNAME', dbName) + .execute('portal_GetShipmentsV1'); - // Sort by ship date descending and take top 100 - rows.sort((a, b) => b.ship_date.localeCompare(a.ship_date)); - return rows.slice(0, 100); + // Immediately extract only the fields we need into plain objects. + // This prevents mssql row metadata from leaking into RSC serialization. + const rows: ShipmentRow[] = []; + for (let i = 0; i < result.recordset.length; i++) { + const r = result.recordset[i]; + rows.push({ + bol_num: String(r.BOLNum ?? ''), + ship_date: r.ShipDate ? new Date(r.ShipDate as string).toISOString() : '', + ship_to: String(r.ShipToLoc ?? '').replace(/, /g, '\n'), + plant: String(r.PlantName ?? ''), + weight: Number(r.Pounds ?? 0), + }); + } + + // Sort by ship date descending and take top 100 + rows.sort((a, b) => b.ship_date.localeCompare(a.ship_date)); + return rows.slice(0, 100); + } finally { + // Don't close the pool — mssql reuses it globally + } } /**