Fix shipping + dashboard + analytics queries against new schema
Deploy to route.crispygoat.com / deploy (push) Successful in 4m48s

The /admin/shipping page was 500-ing with 'column c.name does not
exist' because the getShippingOrders query in src/actions/shipping.ts
was still using legacy column names on the new-schema customers
table (name, email, phone) — the actual columns are first_name,
last_name, primary_email, primary_phone.

The dashboard and analytics pages were logging the same kind of
errors but catching them, so the UI just showed zeroed stats instead
of crashing. Same fix:

- customers: name → first_name || ' ' || last_name
- customers: email → primary_email
- customers: phone → primary_phone
- orders: subtotal → total_cents / 100
- orders: created_at → placed_at
- orders: customer_name → join customers and concat first/last

These are the same kind of fixes that landed in migration 0041 for
the command-center RPCs. The application-layer queries just hadn't
been updated.
This commit is contained in:
Tyler
2026-06-17 11:00:34 -06:00
parent 4b781d3c76
commit a6df4c2dd9
3 changed files with 46 additions and 36 deletions
+1 -1
View File
@@ -284,7 +284,7 @@ export async function getRecentOrders(limit: number = 10): Promise<RecentOrder[]
}>( }>(
`SELECT `SELECT
o.id::text AS id, o.id::text AS id,
c.name AS customer_name, TRIM(COALESCE(c.first_name, '') || ' ' || COALESCE(c.last_name, '')) AS customer_name,
o.total_cents::float / 100.0 AS subtotal, o.total_cents::float / 100.0 AS subtotal,
o.status, o.status,
o.placed_at::text AS created_at, o.placed_at::text AS created_at,
+42 -32
View File
@@ -42,24 +42,22 @@ export async function getDashboardStats(): Promise<DashboardStats> {
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000); const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000);
// Fetch today's orders. `orders` and `stops` use the legacy schema // Fetch today's orders. New schema: `total_cents` (int, divide by 100
// (column names like `subtotal`, `brand_id`, `date`); the new-schema // for dollars) and `placed_at` (not legacy `subtotal` / `created_at`).
// Drizzle `orders` table doesn't have these. Raw SQL via the shared
// pg pool.
const todayOrdersRes = brandId const todayOrdersRes = brandId
? await pool.query<{ subtotal: number; status: string }>( ? await pool.query<{ total_cents: number; status: string }>(
`SELECT subtotal, status `SELECT total_cents, status
FROM orders FROM orders
WHERE created_at >= $1 WHERE placed_at >= $1
AND created_at < $2 AND placed_at < $2
AND brand_id = $3`, AND brand_id = $3`,
[startOfDay.toISOString(), endOfDay.toISOString(), brandId], [startOfDay.toISOString(), endOfDay.toISOString(), brandId],
) )
: await pool.query<{ subtotal: number; status: string }>( : await pool.query<{ total_cents: number; status: string }>(
`SELECT subtotal, status `SELECT total_cents, status
FROM orders FROM orders
WHERE created_at >= $1 WHERE placed_at >= $1
AND created_at < $2`, AND placed_at < $2`,
[startOfDay.toISOString(), endOfDay.toISOString()], [startOfDay.toISOString(), endOfDay.toISOString()],
); );
const todayOrders = todayOrdersRes.rows; const todayOrders = todayOrdersRes.rows;
@@ -67,7 +65,7 @@ export async function getDashboardStats(): Promise<DashboardStats> {
// Calculate today's revenue and orders // Calculate today's revenue and orders
const validOrders = todayOrders.filter((o) => o.status !== "cancelled"); const validOrders = todayOrders.filter((o) => o.status !== "cancelled");
const todayRevenue = validOrders.reduce( const todayRevenue = validOrders.reduce(
(sum, o) => sum + (o.subtotal || 0), (sum, o) => sum + ((o.total_cents || 0) / 100),
0, 0,
); );
const todayOrderCount = validOrders.length; const todayOrderCount = validOrders.length;
@@ -106,7 +104,7 @@ export async function getDashboardStats(): Promise<DashboardStats> {
); );
const activeProducts = productsRes.rows.length; const activeProducts = productsRes.rows.length;
// Fetch weekly orders for chart (last 7 days) // Fetch weekly orders for chart (last 7 days). New schema uses `placed_at`.
const weeklyOrders: number[] = []; const weeklyOrders: number[] = [];
for (let i = 6; i >= 0; i--) { for (let i = 6; i >= 0; i--) {
const dayStart = new Date(startOfDay); const dayStart = new Date(startOfDay);
@@ -116,48 +114,60 @@ export async function getDashboardStats(): Promise<DashboardStats> {
const dayRes = brandId const dayRes = brandId
? await pool.query<{ id: string }>( ? await pool.query<{ id: string }>(
`SELECT id FROM orders `SELECT id FROM orders
WHERE created_at >= $1 WHERE placed_at >= $1
AND created_at < $2 AND placed_at < $2
AND brand_id = $3 AND brand_id = $3
LIMIT 1`, LIMIT 1`,
[dayStart.toISOString(), dayEnd.toISOString(), brandId], [dayStart.toISOString(), dayEnd.toISOString(), brandId],
) )
: await pool.query<{ id: string }>( : await pool.query<{ id: string }>(
`SELECT id FROM orders `SELECT id FROM orders
WHERE created_at >= $1 WHERE placed_at >= $1
AND created_at < $2 AND placed_at < $2
LIMIT 1`, LIMIT 1`,
[dayStart.toISOString(), dayEnd.toISOString()], [dayStart.toISOString(), dayEnd.toISOString()],
); );
weeklyOrders.push(dayRes.rows.length > 0 ? 1 : 0); weeklyOrders.push(dayRes.rows.length > 0 ? 1 : 0);
} }
// Fetch recent orders (last 10) // Fetch recent orders (last 10). New schema: `total_cents`/`placed_at`
// on `orders`; customer name comes from a `customers` join (no
// `customer_name` column on `orders`).
const recentRes = brandId const recentRes = brandId
? await pool.query<{ ? await pool.query<{
id: string; id: string;
customer_name: string; customer_name: string;
subtotal: number; total_cents: number;
status: string; status: string;
created_at: string; placed_at: string;
}>( }>(
`SELECT id, customer_name, subtotal, status, created_at `SELECT o.id::text AS id,
FROM orders TRIM(COALESCE(c.first_name, '') || ' ' || COALESCE(c.last_name, '')) AS customer_name,
WHERE brand_id = $1 o.total_cents,
ORDER BY created_at DESC o.status,
o.placed_at::text AS placed_at
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.brand_id = $1
ORDER BY o.placed_at DESC
LIMIT 10`, LIMIT 10`,
[brandId], [brandId],
) )
: await pool.query<{ : await pool.query<{
id: string; id: string;
customer_name: string; customer_name: string;
subtotal: number; total_cents: number;
status: string; status: string;
created_at: string; placed_at: string;
}>( }>(
`SELECT id, customer_name, subtotal, status, created_at `SELECT o.id::text AS id,
FROM orders TRIM(COALESCE(c.first_name, '') || ' ' || COALESCE(c.last_name, '')) AS customer_name,
ORDER BY created_at DESC o.total_cents,
o.status,
o.placed_at::text AS placed_at
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
ORDER BY o.placed_at DESC
LIMIT 10`, LIMIT 10`,
); );
@@ -166,9 +176,9 @@ export async function getDashboardStats(): Promise<DashboardStats> {
.map((o) => ({ .map((o) => ({
id: o.id || "", id: o.id || "",
customer_name: o.customer_name || "Guest", customer_name: o.customer_name || "Guest",
total: o.subtotal || 0, total: (o.total_cents || 0) / 100,
status: o.status || "unknown", status: o.status || "unknown",
created_at: formatTimeAgo(o.created_at), created_at: formatTimeAgo(o.placed_at),
})); }));
return { return {
+3 -3
View File
@@ -73,9 +73,9 @@ export async function getShippingOrders(): Promise<GetShippingOrdersResult> {
}>( }>(
`SELECT `SELECT
o.id::text AS id, o.id::text AS id,
c.name AS customer_name, TRIM(COALESCE(c.first_name, '') || ' ' || COALESCE(c.last_name, '')) AS customer_name,
c.email AS customer_email, c.primary_email AS customer_email,
c.phone AS customer_phone, c.primary_phone AS customer_phone,
o.status, o.status,
o.total_cents::float / 100.0 AS subtotal, o.total_cents::float / 100.0 AS subtotal,
o.placed_at::text AS created_at, o.placed_at::text AS created_at,