Files
route-commerce/src/app/api/route-trace/fsma-report/route.ts
T
tyler d892b3f64f
Deploy to route.crispygoat.com / deploy (push) Failing after 9s
feat(selfhost): complete Supabase removal migration
Phase 1 — Pattern library
- Add src/lib/api.ts (typed PostgREST client, anon-key)
- Add src/lib/svc-fetch.ts (service-role fetch + PostgREST client)
- Add src/lib/db-types.ts (Database generic, RowOf helper)
- Add src/lib/auth-admin.ts (better-auth admin wrappers: createUser, listUsers, removeUser, requestPasswordReset, validateSession)

Phase 2 — Server action migration
- Migrate all 67 server actions off @supabase/supabase-js to svcApi/svcRpc
- Replace service.auth.admin.* with authAdmin.* (better-auth)
- Replace publicApi.auth.* with authAdmin.* (better-auth)
- Add typed single() null guards + nullable column fallbacks

Phase 3 — Delete mock data + debug routes
- Remove if(useMockData) branches from 7 files (-226 lines)
- Delete src/lib/mock-data.ts (no longer referenced)
- Delete 7 debug API routes (debug-admin-users, debug-auth, debug-cookie, debug-env, debug-get-admin-users, debug-hello, debug-me)

Phase 4 — Hard cut
- Delete src/lib/supabase.ts (orphan — no importers)
- Delete src/app/api/supabase + src/app/api/supabase-test routes
- Remove @supabase/ssr + @supabase/supabase-js from package.json
- Rename env vars: NEXT_PUBLIC_SUPABASE_URL → NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY → NEXT_PUBLIC_API_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY → POSTGREST_SERVICE_KEY

Phase 5 — Verify
- npx tsc --noEmit: 0 errors
- npm run lint: 0 new errors from migration (104 pre-existing errors unrelated)
- npm run build: same failure mode as main worktree (PostgREST-dependent prerender), no regression

Net: 158 files changed, +1640 / -1903 lines (-263 net)
2026-06-05 20:17:02 +00:00

182 lines
6.7 KiB
TypeScript

import { svcHeaders } from "@/lib/svc-headers";
const SUPABASE_URL = process.env.NEXT_PUBLIC_API_URL!;
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
type LotRow = {
lot_id: string;
lot_number: string;
crop_type: string;
variety: string | null;
harvest_date: string;
field_location: string | null;
field_block: string | null;
worker_name: string | null;
packer_name: string | null;
quantity_lbs: number | null;
quantity_used_lbs: number | null;
yield_estimate_lbs: number | null;
yield_unit: string | null;
bin_id: string | null;
status: string;
};
type EventRow = {
lot_id: string;
event_type: string;
event_time: string;
location: string | null;
notes: string | null;
bin_id: string | null;
created_by_name: string | null;
};
async function adminFetchJson(endpoint: string, body: unknown) {
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/${endpoint}`, {
method: "POST",
headers: {
...svcHeaders(SUPABASE_PAT),
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) return null;
return res.json();
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const brandId = searchParams.get("brandId");
const startDate = searchParams.get("startDate");
const endDate = searchParams.get("endDate");
if (!brandId || !startDate || !endDate) {
return new Response("Missing brandId, startDate, or endDate", { status: 400 });
}
// Use existing get_harvest_lots RPC and filter by date in JavaScript
const allLots = await adminFetchJson("get_harvest_lots", {
p_brand_id: brandId,
p_status: null,
});
const lots: LotRow[] = (allLots ?? []) as LotRow[];
// Filter by date range
const filteredLots = lots.filter(lot => {
const harvestDate = lot.harvest_date;
if (!harvestDate) return false;
return harvestDate >= startDate && harvestDate <= endDate;
});
if (filteredLots.length === 0) {
return new Response("No lots found for this period", { status: 404 });
}
const lotIds = filteredLots.map((l) => l.lot_id);
const eventsRaw = await adminFetchJson("get_events_for_lots", { p_lot_ids: lotIds });
const allEvents: EventRow[] = (eventsRaw ?? []) as EventRow[];
const eventsByLot: Record<string, EventRow[]> = {};
for (const evt of allEvents) {
if (!eventsByLot[evt.lot_id]) eventsByLot[evt.lot_id] = [];
eventsByLot[evt.lot_id].push(evt);
}
const lines: string[] = [];
lines.push("FSMA FOOD SAFETY MODERNIZATION ACT — Produce Traceability Report");
lines.push(`Brand ID,${brandId}`);
lines.push(`Report Period,${startDate} to ${endDate}`);
lines.push(`Generated,${new Date().toLocaleString("en-US")}`);
lines.push(`Total Lots,${filteredLots.length}`);
lines.push("");
const totalQty = filteredLots.reduce((s, l) => s + (l.quantity_lbs ?? 0), 0);
const totalUsed = filteredLots.reduce((s, l) => s + (l.quantity_used_lbs ?? 0), 0);
const crops = [...new Set(filteredLots.map((l) => l.crop_type))];
const units = [...new Set(filteredLots.map((l) => l.yield_unit ?? "lbs"))];
const primaryUnit = units.length === 1 ? units[0] : "lbs";
lines.push("SUMMARY");
lines.push(`Total Lots Harvested,${filteredLots.length}`);
lines.push(`Total Weight (${primaryUnit}),${totalQty.toLocaleString()}`);
lines.push(`Total Used (${primaryUnit}),${totalUsed.toLocaleString()}`);
lines.push(`Remaining (${primaryUnit}),${(totalQty - totalUsed).toLocaleString()}`);
lines.push(`Crop Types,"${crops.join(", ")}"`);
lines.push(`Yield Unit,"${units.join(", ")}"`);
lines.push(`Date Range,${startDate} to ${endDate}`);
lines.push("");
lines.push("ONE-UP / ONE-DOWN TRACEABILITY");
lines.push("Lot Number,Crop,Variety,Harvest Date,Field,Block,Worker,Packer,Qty (lbs),Used (lbs),Remaining (lbs),Yield Est. (lbs),Yield Unit,Yield Variance %,Bin ID,Status,Event,Event Time,Location,Bin ID,Notes,Recorded By");
for (const lot of filteredLots) {
const evts = eventsByLot[lot.lot_id] ?? [];
const remaining = (lot.quantity_lbs ?? 0) - (lot.quantity_used_lbs ?? 0);
if (evts.length === 0) {
const yieldVariance = (lot.yield_estimate_lbs && lot.yield_estimate_lbs > 0)
? (((lot.quantity_lbs ?? 0) - lot.yield_estimate_lbs) / lot.yield_estimate_lbs * 100).toFixed(1)
: "";
lines.push(esc([
lot.lot_number, lot.crop_type, lot.variety ?? "", lot.harvest_date ?? "",
lot.field_location ?? "", lot.field_block ?? "", lot.worker_name ?? "",
lot.packer_name ?? "", String(lot.quantity_lbs ?? ""),
String(lot.quantity_used_lbs ?? ""), String(Math.max(0, remaining)),
String(lot.yield_estimate_lbs ?? ""), lot.yield_unit ?? "",
yieldVariance,
lot.bin_id ?? "", lot.status ?? "",
"—", "—", "—", "—", "—", "—",
]));
} else {
for (let i = 0; i < evts.length; i++) {
const evt = evts[i];
const evtTime = new Date(evt.event_time).toLocaleString("en-US", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit",
});
const yieldVariance = (lot.yield_estimate_lbs && lot.yield_estimate_lbs > 0)
? (((lot.quantity_lbs ?? 0) - lot.yield_estimate_lbs) / lot.yield_estimate_lbs * 100).toFixed(1)
: "";
lines.push(esc([
i === 0 ? lot.lot_number : "",
i === 0 ? lot.crop_type : "",
i === 0 ? (lot.variety ?? "") : "",
i === 0 ? (lot.harvest_date ?? "") : "",
i === 0 ? (lot.field_location ?? "") : "",
i === 0 ? (lot.field_block ?? "") : "",
i === 0 ? (lot.worker_name ?? "") : "",
i === 0 ? (lot.packer_name ?? "") : "",
i === 0 ? String(lot.quantity_lbs ?? "") : "",
i === 0 ? String(lot.quantity_used_lbs ?? "") : "",
i === 0 ? String(Math.max(0, remaining)) : "",
i === 0 ? String(lot.yield_estimate_lbs ?? "") : "",
i === 0 ? (lot.yield_unit ?? "") : "",
i === 0 ? yieldVariance : "",
i === 0 ? (lot.bin_id ?? "") : "",
i === 0 ? (lot.status ?? "") : "",
evt.event_type ?? "",
evtTime,
evt.location ?? "",
evt.bin_id ?? "",
(evt.notes ?? "").replace(/"/g, '""'),
evt.created_by_name ?? "",
]));
}
}
}
lines.push("");
lines.push("END OF REPORT");
lines.push("Powered by Route Commerce — routecommerce.com");
const csv = lines.join("\n");
return new Response(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="fsma-report-${startDate}-to-${endDate}.csv"`,
},
});
}
function esc(values: string[]): string {
return values.map((v) => `"${v.replace(/"/g, '""')}"`).join(",");
}