916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
146 lines
4.0 KiB
TypeScript
146 lines
4.0 KiB
TypeScript
// Data service for direct PostgreSQL queries via pg Pool
|
|
// This replaces the Supabase client for production use
|
|
|
|
import { pool } from "./db";
|
|
import type { QueryResultRow } from "pg";
|
|
|
|
// Re-export pool for convenience
|
|
export { pool };
|
|
|
|
// Query helpers for common operations
|
|
export async function query<T extends QueryResultRow = Record<string, unknown>>(
|
|
sql: string,
|
|
params: unknown[] = []
|
|
): Promise<T[]> {
|
|
const result = await pool.query<T>(sql, params);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function queryOne<T extends QueryResultRow = Record<string, unknown>>(
|
|
sql: string,
|
|
params: unknown[] = []
|
|
): Promise<T | null> {
|
|
const result = await pool.query<T>(sql, params);
|
|
return result.rows[0] ?? null;
|
|
}
|
|
|
|
// Brand operations
|
|
export async function getBrands() {
|
|
return query<{ id: string; name: string; slug: string; accent_color: string | null; active: boolean }>(
|
|
"SELECT id, name, slug, accent_color, active FROM brands ORDER BY name"
|
|
);
|
|
}
|
|
|
|
export async function getBrandById(brandId: string) {
|
|
return queryOne<{ id: string; name: string; slug: string }>(
|
|
"SELECT id, name, slug FROM brands WHERE id = $1",
|
|
[brandId]
|
|
);
|
|
}
|
|
|
|
// Product operations
|
|
export async function getProducts(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM products WHERE brand_id = $1 AND active = true AND deleted_at IS NULL ORDER BY name",
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM products WHERE active = true AND deleted_at IS NULL ORDER BY name"
|
|
);
|
|
}
|
|
|
|
export async function getProductById(productId: string) {
|
|
return queryOne<Record<string, unknown>>(
|
|
"SELECT * FROM products WHERE id = $1 AND deleted_at IS NULL",
|
|
[productId]
|
|
);
|
|
}
|
|
|
|
// Stop operations
|
|
export async function getStops(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM stops WHERE brand_id = $1 AND active = true ORDER BY date DESC",
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM stops WHERE active = true ORDER BY date DESC"
|
|
);
|
|
}
|
|
|
|
export async function getStopById(stopId: string) {
|
|
return queryOne<Record<string, unknown>>(
|
|
"SELECT * FROM stops WHERE id = $1",
|
|
[stopId]
|
|
);
|
|
}
|
|
|
|
// Order operations
|
|
export async function getOrders(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
`SELECT o.*, s.city, s.state, s.date, s.location, s.slug
|
|
FROM orders o
|
|
LEFT JOIN stops s ON o.stop_id = s.id
|
|
WHERE o.brand_id = $1
|
|
ORDER BY o.created_at DESC`,
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
`SELECT o.*, s.city, s.state, s.date, s.location, s.slug
|
|
FROM orders o
|
|
LEFT JOIN stops s ON o.stop_id = s.id
|
|
ORDER BY o.created_at DESC`
|
|
);
|
|
}
|
|
|
|
// Worker operations (for time tracking)
|
|
export async function getWorkers(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM workers WHERE brand_id = $1 AND is_active = true ORDER BY name",
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM workers WHERE is_active = true ORDER BY name"
|
|
);
|
|
}
|
|
|
|
// Task operations (for time tracking)
|
|
export async function getTasks(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM tasks WHERE brand_id = $1 ORDER BY sort_order",
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM tasks ORDER BY sort_order"
|
|
);
|
|
}
|
|
|
|
// Customer operations
|
|
export async function getCustomers(brandId?: string | null) {
|
|
if (brandId) {
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM customers WHERE brand_id = $1 ORDER BY name",
|
|
[brandId]
|
|
);
|
|
}
|
|
return query<Record<string, unknown>>(
|
|
"SELECT * FROM customers ORDER BY name"
|
|
);
|
|
}
|
|
|
|
// Brand settings
|
|
export async function getBrandSettings(brandId: string) {
|
|
return queryOne<Record<string, unknown>>(
|
|
"SELECT * FROM brand_settings WHERE brand_id = $1",
|
|
[brandId]
|
|
);
|
|
} |