d892b3f64f
Deploy to route.crispygoat.com / deploy (push) Failing after 9s
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)
182 lines
6.7 KiB
TypeScript
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(",");
|
|
} |