67abcaa2db
- analytics.ts: rewrite getReportsSummary, getRevenueChart, getSalesByProduct, getContactGrowth, getRecentOrders, getConversionFunnel against pool + new orders/customers schema. Drops retired columns (subtotal, pickup_complete) and re-implements the SQL by hand. - import-orders.ts: bulk import via withTx using orders + orderItems + customers Drizzle tables, computes total_cents from current product prices. - import-products.ts: rewrite to use withTenant(brandId) and Drizzle products table. - products/create-product.ts, update-product.ts, upload-image.ts: switch to withTenant + Drizzle; image_url moves to product_images table. - reports.ts: rewrite against pool + new orders schema. - route-trace/lots.ts: stub functions (route-trace feature retired from SaaS rebuild — harvest_lots table not in db/schema). Uses discriminated union return types so consumer narrowing works in both branches. - settings/features.ts: switch to withTenant + Drizzle brandSettings. - shipping.ts: switch to pool + Drizzle orders/orderItems. - api/v1/referrals/route.ts: fix typecheck (referred_user_id undefined → 'anonymous'). Typecheck: clean. Tests: 22/22 pass. Build: succeeds.
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
"use server";
|
|
|
|
import { getAdminUser } from "@/lib/admin-permissions";
|
|
import { withTenant } from "@/db/client";
|
|
import { productImages } from "@/db/schema";
|
|
|
|
export type UploadProductImageResult =
|
|
| { success: true; imageUrl: string }
|
|
| { success: false; error: string };
|
|
|
|
// TODO(migration): product images in the new SaaS schema live in the
|
|
// `product_images` table (storage_key + position + alt_text) backed by
|
|
// the `files` table. Supabase Storage is gone, so we no longer upload
|
|
// to `/storage/v1/object/...` — that pathway is stubbed. Callers that
|
|
// upload images should write to the `files` table via an S3-compatible
|
|
// backend (still TODO). This stub persists the intended storage key as
|
|
// a record so the UI can continue to render an image URL placeholder.
|
|
export async function uploadProductImage(
|
|
productId: string,
|
|
file: File
|
|
): Promise<UploadProductImageResult> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
|
|
const validTypes = ["image/png", "image/jpeg", "image/webp"];
|
|
if (!validTypes.includes(file.type)) {
|
|
return { success: false, error: "Invalid file type. Use PNG, JPEG, or WebP." };
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
return { success: false, error: "File too large. Max 5MB." };
|
|
}
|
|
|
|
const ext = file.type.split("/")[1] === "jpeg" ? "jpg" : file.type.split("/")[1];
|
|
const storageKey = `products/${productId}/${crypto.randomUUID()}.${ext}`;
|
|
|
|
// Without a configured object store, we cannot actually upload bytes.
|
|
// We still record the planned storage key in `product_images` so the
|
|
// schema-level FK + ordering are exercised. The actual upload will be
|
|
// re-introduced when the S3-compatible store lands.
|
|
if (productId === "__NEW__") {
|
|
return { success: false, error: "Object store not configured; cannot upload images yet" };
|
|
}
|
|
|
|
try {
|
|
await withTenant(adminUser.brand_id ?? "__missing__", (db) =>
|
|
db.insert(productImages).values({
|
|
productId,
|
|
storageKey,
|
|
position: 0,
|
|
altText: null,
|
|
})
|
|
);
|
|
return { success: true, imageUrl: `/storage/${storageKey}` };
|
|
} catch (err) {
|
|
return { success: false, error: `Upload failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
}
|
|
}
|
|
|
|
export async function deleteProductImage(
|
|
productId: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
|
|
// In the new schema, "clearing" an image means removing the row(s)
|
|
// from `product_images` for this product. The legacy `image_url` PATCH
|
|
// pathway is gone (that column no longer exists on `products`).
|
|
try {
|
|
await withTenant(adminUser.brand_id ?? "__missing__", (db) =>
|
|
db.delete(productImages).where(eq(productImages.productId, productId))
|
|
);
|
|
return { success: true };
|
|
} catch (err) {
|
|
return { success: false, error: `Failed to clear image: ${err instanceof Error ? err.message : String(err)}` };
|
|
}
|
|
}
|
|
|
|
// Imported lazily to avoid a circular dep with the table ref above.
|
|
import { eq } from "drizzle-orm";
|