2daa8fd4b6
The legacy src/lib/supabase.ts shim returned { data: null } for every
query, so 15+ pages were silently rendering empty state — including the
two customer-facing storefronts (tuxedo, indian-river-direct) and
several v1 admin pages. Replace every caller with canonical access:
- New src/actions/storefront.ts server-action module: brand lookup,
public stops, active products, stop-by-slug, wholesale settings,
portal config. Uses the shared pg Pool and SECURITY DEFINER RPCs.
- src/actions/brand-settings.ts: getBrandSettingsPublic inlined the
brands + brand_settings LEFT JOIN wholesale_settings query (the
RPC it called did not exist; try/catch was masking the failure).
- src/actions/ai/preferences.ts: switched get/saveAIPreferences to
pool.query.
- src/actions/square-sync-ui.ts: new getSquareQueueCount action for
SquareSyncWidget (replaces shim count).
- src/app/api/{tuxedo,indian-river-direct}/schedule-pdf/route.ts:
use pool.query.
- next.config.ts: redirects /admin/{products/:id,reports,taxes,
settings/{shipping,integrations,billing}} → their v2 / settings
equivalents, then deleted those pages.
- Deleted src/lib/supabase.ts (no remaining imports).
Tests: 174/175 (unchanged from baseline; pre-existing getAdminUser
mock issue is tracked in Step 5).
190 lines
7.3 KiB
TypeScript
190 lines
7.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
|
|
import { pool } from "@/lib/db";
|
|
|
|
export async function GET() {
|
|
const brandSlug = "tuxedo";
|
|
|
|
const { rows: brandRows } = await pool.query<{ id: string; name: string }>(
|
|
`SELECT id, name FROM brands WHERE slug = $1 LIMIT 1`,
|
|
[brandSlug],
|
|
);
|
|
const brand = brandRows[0];
|
|
if (!brand?.id) {
|
|
return new NextResponse("Brand not found", { status: 404 });
|
|
}
|
|
|
|
const { rows: stops } = await pool.query<{ city: string; state: string; date: string; time: string; location: string }>(
|
|
`SELECT city, state, date::text AS date, time, location
|
|
FROM stops
|
|
WHERE brand_id = $1 AND is_public = true AND status = 'active'
|
|
ORDER BY date ASC, time ASC`,
|
|
[brand.id],
|
|
);
|
|
|
|
const { rows: settingsRows } = await pool.query<{
|
|
logo_url: string | null;
|
|
email: string | null;
|
|
phone: string | null;
|
|
schedule_pdf_notes: string | null;
|
|
}>(
|
|
`SELECT logo_url, email, phone, schedule_pdf_notes
|
|
FROM brand_settings
|
|
WHERE brand_id = $1
|
|
LIMIT 1`,
|
|
[brand.id],
|
|
);
|
|
const settings = settingsRows[0] ?? null;
|
|
|
|
const pdfBytes = await buildProfessionalSchedulePdf({
|
|
brandName: brand.name,
|
|
stops: stops,
|
|
logoUrl: settings?.logo_url ?? null,
|
|
contactEmail: settings?.email ?? null,
|
|
contactPhone: settings?.phone ?? null,
|
|
footerNotes: settings?.schedule_pdf_notes ?? null,
|
|
});
|
|
|
|
return new NextResponse(Buffer.from(pdfBytes), {
|
|
headers: {
|
|
"Content-Type": "application/pdf",
|
|
"Content-Disposition": `attachment; filename="${brand.name.replace(/\s+/g, "-")}-Schedule.pdf"`,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function buildProfessionalSchedulePdf({
|
|
brandName,
|
|
stops,
|
|
logoUrl,
|
|
contactEmail,
|
|
contactPhone,
|
|
footerNotes,
|
|
}: {
|
|
brandName: string;
|
|
stops: Array<{ city: string; state: string; date: string; time: string; location: string }>;
|
|
logoUrl: string | null;
|
|
contactEmail: string | null;
|
|
contactPhone: string | null;
|
|
footerNotes: string | null;
|
|
}) {
|
|
const pdfDoc = await PDFDocument.create();
|
|
const page = pdfDoc.addPage([612, 792]);
|
|
const { width, height } = page.getSize();
|
|
const margin = 50;
|
|
|
|
const helvetica = await page.doc.embedFont(StandardFonts.Helvetica);
|
|
const helveticaBold = await page.doc.embedFont(StandardFonts.HelveticaBold);
|
|
|
|
const colWidths = [130, 80, 80, 210];
|
|
const headers = ["City / State", "Date", "Time", "Location"];
|
|
|
|
let y = height - margin;
|
|
|
|
// ── Header block ─────────────────────────────────────────────────────────
|
|
// Brand name
|
|
page.drawText(brandName, { x: margin, y, font: helveticaBold, size: 18, color: rgb(0.1, 0.1, 0.1) });
|
|
y -= 10;
|
|
|
|
// Tagline
|
|
page.drawText("Stop Schedule", { x: margin, y, font: helvetica, size: 11, color: rgb(0.4, 0.4, 0.4) });
|
|
y -= 8;
|
|
|
|
// Stop count
|
|
const stopLabel = stops.length === 0
|
|
? "No upcoming stops"
|
|
: stops.length === 1
|
|
? "1 upcoming stop"
|
|
: `${stops.length} upcoming stops`;
|
|
page.drawText(stopLabel, { x: margin, y, font: helvetica, size: 9, color: rgb(0.5, 0.5, 0.5) });
|
|
y -= 18;
|
|
|
|
// ── Logo (top-right) ────────────────────────────────────────────────────
|
|
if (logoUrl) {
|
|
try {
|
|
const logoResponse = await fetch(logoUrl);
|
|
if (logoResponse.ok) {
|
|
const logoBuffer = Buffer.from(await logoResponse.arrayBuffer());
|
|
const ext = logoUrl.split(".").pop()?.toLowerCase() ?? "";
|
|
const embedFn = ext === "png" ? pdfDoc.embedPng : pdfDoc.embedJpg;
|
|
const logoImage = await embedFn(logoBuffer);
|
|
const logoH = 36;
|
|
const logoW = Math.min(120, logoImage.width * (logoH / logoImage.height));
|
|
page.drawImage(logoImage, {
|
|
x: width - margin - logoW,
|
|
y: height - margin - logoH,
|
|
width: logoW,
|
|
height: logoH,
|
|
});
|
|
}
|
|
} catch {
|
|
// logo fetch failed — skip embedding
|
|
}
|
|
}
|
|
|
|
// ── Divider ─────────────────────────────────────────────────────────────
|
|
page.drawLine({ start: { x: margin, y }, end: { x: width - margin, y }, thickness: 1, color: rgb(0.2, 0.2, 0.2) });
|
|
y -= 14;
|
|
|
|
// ── Table headers ────────────────────────────────────────────────────────
|
|
let x = margin;
|
|
for (let i = 0; i < headers.length; i++) {
|
|
page.drawText(headers[i], { x, y, font: helveticaBold, size: 9, color: rgb(0.3, 0.3, 0.3) });
|
|
x += colWidths[i];
|
|
}
|
|
y -= 8;
|
|
page.drawLine({ start: { x: margin, y }, end: { x: width - margin, y }, thickness: 0.5, color: rgb(0.75, 0.75, 0.75) });
|
|
y -= 16;
|
|
|
|
// ── Stop rows ───────────────────────────────────────────────────────────
|
|
if (stops.length === 0) {
|
|
page.drawText("No upcoming stops scheduled.", { x: margin, y, font: helvetica, size: 10, color: rgb(0.5, 0.5, 0.5) });
|
|
y -= 20;
|
|
} else {
|
|
for (const stop of stops) {
|
|
x = margin;
|
|
const cols = [stop.city + ", " + stop.state, stop.date, stop.time, stop.location];
|
|
for (let i = 0; i < cols.length; i++) {
|
|
page.drawText(cols[i], { x, y, font: helvetica, size: 9, color: rgb(0.15, 0.15, 0.15) });
|
|
x += colWidths[i];
|
|
}
|
|
y -= 20;
|
|
|
|
if (y < margin + 40) {
|
|
const newPage = pdfDoc.addPage([612, 792]);
|
|
y = newPage.getSize().height - margin;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Bottom divider ───────────────────────────────────────────────────────
|
|
y -= 4;
|
|
page.drawLine({ start: { x: margin, y }, end: { x: width - margin, y }, thickness: 0.5, color: rgb(0.8, 0.8, 0.8) });
|
|
y -= 14;
|
|
|
|
// ── Footer notes ────────────────────────────────────────────────────────
|
|
if (footerNotes) {
|
|
page.drawText(footerNotes, { x: margin, y, font: helvetica, size: 8, color: rgb(0.45, 0.45, 0.45) });
|
|
y -= 14;
|
|
}
|
|
|
|
// ── Contact line ───────────────────────────────────────────────────────
|
|
const contactParts = [];
|
|
if (contactEmail) contactParts.push(contactEmail);
|
|
if (contactPhone) contactParts.push(contactPhone);
|
|
if (contactParts.length > 0) {
|
|
page.drawText(contactParts.join(" | "), { x: margin, y, font: helvetica, size: 8, color: rgb(0.4, 0.4, 0.4) });
|
|
y -= 12;
|
|
}
|
|
|
|
// ── Generated date ──────────────────────────────────────────────────────
|
|
page.drawText(`Generated ${new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}`, {
|
|
x: width - margin - 160,
|
|
y: 16,
|
|
font: helvetica,
|
|
size: 8,
|
|
color: rgb(0.6, 0.6, 0.6),
|
|
});
|
|
|
|
return pdfDoc.save();
|
|
} |