"use server"; import { and, desc, eq, isNotNull } from "drizzle-orm"; import { getAdminUser } from "@/lib/admin-permissions"; import { withBrand } from "@/db/client"; import { customers, orders, campaigns } from "@/db/schema"; import { getSession } from "@/lib/auth"; /** * Server-side data loader for the per-stop "Message customers" panel. * The new schema has no `orders.stop_id` column or * `orders.pickup_complete` column, so the legacy "fetch orders for * this stop" lookup cannot be reproduced exactly. The new approach: * - "Orders" = the tenant's recent orders with a known customer * (acts as a generic "people we can message" list). * - "Messages" = the tenant's most recent campaigns (used as the * recent-message history placeholder). * The `MessageCustomersSection` UI degrades gracefully when these * lists are empty. */ export type StopOrder = { id: string; customer_name: string; customer_email: string | null; customer_phone: string | null; pickup_complete: boolean; }; export type StopBlastMessage = { id: string; type: string; subject: string | null; body: string; created_at: string; message_recipients: { id: string }[]; }; export type GetStopMessagingDataResult = { success: true; orders: StopOrder[]; messages: StopBlastMessage[]; } | { success: false; error: string }; export async function getStopMessagingData(params: { stopId: string; brandId?: string; }): Promise { await getSession(); const adminUser = await getAdminUser(); if (!adminUser) return { success: false, error: "Not authenticated" }; // We don't filter by `stopId` because the new `orders` table has no // `stop_id` column. Instead, fall back to "any recent order in the // tenant". The caller is responsible for picking the right brand. const brandId = params.brandId ?? adminUser.brand_id; if (!brandId) { return { success: false, error: "Brand context required" }; } try { const [orderRows, campaignRows] = await withBrand(brandId, async (db) => { // Independent reads — fire in parallel. const [o, c] = await Promise.all([ db .select({ id: orders.id, status: orders.status, placedAt: orders.placedAt, customerName: customers.fullName, customerEmail: customers.email, customerPhone: customers.phone, }) .from(orders) .innerJoin(customers, eq(customers.id, orders.customerId)) .where( and( eq(orders.brandId, brandId), isNotNull(customers.email), ), ) .orderBy(desc(orders.placedAt)) .limit(50), db .select({ id: campaigns.id, name: campaigns.name, sentAt: campaigns.sentAt, updatedAt: campaigns.updatedAt, }) .from(campaigns) .where(eq(campaigns.brandId, brandId)) .orderBy(desc(campaigns.sentAt), desc(campaigns.updatedAt)) .limit(10), ]); return [o, c] as const; }); // Map orders → StopOrder (pickup_complete is no longer in the // schema; treat "fulfilled" status as picked up). const mappedOrders: StopOrder[] = orderRows.map((o) => ({ id: o.id, customer_name: o.customerName ?? "", customer_email: o.customerEmail, customer_phone: o.customerPhone, pickup_complete: o.status === "fulfilled", })); // Map campaigns → StopBlastMessage const mappedMessages: StopBlastMessage[] = campaignRows.map((c) => ({ id: c.id, type: "campaign", subject: c.name, body: "", created_at: (c.sentAt ?? c.updatedAt).toISOString(), message_recipients: [], })); void params.stopId; // not used in the new schema return { success: true, orders: mappedOrders, messages: mappedMessages }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : "Failed to load stop data" }; } }