Files
route-commerce/src/actions/orders.ts
T

229 lines
5.8 KiB
TypeScript

"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { pool } from "@/lib/db";
import { mockOrders, mockStops } from "@/lib/mock-data";
type AdminOrder = {
id: string;
customer_name: string;
customer_email: string | null;
customer_phone: string | null;
stop_id: string | null;
status: string;
subtotal: number;
pickup_complete: boolean;
pickup_completed_at: string | null;
pickup_completed_by: string | null;
created_at: string;
payment_processor: string | null;
// For AdminOrdersPanel: stops is { city, state, date }
// For DriverPickupPanel: stops is { id, city, state, date, brand_id }
// Both share city/state/date so we use the broader shape
stops: {
id?: string;
city: string;
state: string;
date: string;
brand_id?: string;
} | null;
order_items?: Array<{
id: string;
product_id: string;
quantity: number;
price: number;
fulfillment?: string;
products: { name: string } | null;
}>;
};
type AdminStop = {
id: string;
city: string;
state: string;
date: string;
location: string;
brand_id: string;
};
type AdminOrdersResult = {
orders: AdminOrder[];
stops: AdminStop[];
};
type AdminOrdersResponse =
| { success: true; orders: AdminOrder[]; stops: AdminStop[]; error: null }
| { success: false; orders: []; stops: []; error: string };
type AdminOrderDetail = {
id: string;
customer_name: string;
customer_email: string | null;
customer_phone: string | null;
stop_id: string | null;
status: string;
subtotal: number;
pickup_complete: boolean;
pickup_completed_at: string | null;
pickup_completed_by: string | null;
created_at: string;
discount_amount: number | null;
tax_amount: number | null;
tax_rate: number | null;
tax_location: string | null;
discount_reason: string | null;
internal_notes: string | null;
payment_processor: string | null;
payment_status: string | null;
payment_transaction_id: string | null;
refunded_amount: number | null;
refund_reason: string | null;
stops: {
id?: string;
city: string;
state: string;
date: string;
brand_id?: string;
location?: string;
} | null;
order_items?: Array<{
id: string;
product_id: string;
quantity: number;
price: number;
fulfillment?: string;
products: { name: string } | null;
}>;
refunds?: Array<{
id: string;
order_id: string;
amount: number;
reason: string | null;
processor: string | null;
processor_refund_id: string | null;
status: string;
created_at: string;
}>;
};
// Mock orders for UI review
const mockAdminOrders: AdminOrder[] = mockOrders.map((o: any) => ({
id: o.id,
customer_name: o.customer_name,
customer_email: o.customer_email,
customer_phone: o.customer_phone,
stop_id: o.stop_id,
status: o.status,
subtotal: o.subtotal,
pickup_complete: o.pickup_complete,
pickup_completed_at: o.pickup_completed_at,
pickup_completed_by: null,
created_at: o.created_at,
payment_processor: o.payment_processor,
stops: o.stops,
order_items: o.order_items,
}));
const mockAdminStops: AdminStop[] = mockStops.map((s: any) => ({
id: s.id,
city: s.city,
state: s.state,
date: s.date,
location: s.location,
brand_id: s.brand_id,
}));
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
export async function getAdminOrders(): Promise<AdminOrdersResponse> {
// Return mock data in mock mode
if (useMockData) {
return {
success: true,
orders: mockAdminOrders,
stops: mockAdminStops,
error: null,
};
}
const adminUser = await getAdminUser();
const brandId = adminUser?.brand_id ?? null;
// The legacy `get_admin_orders` RPC is a SECURITY DEFINER function that
// returns orders + stops joined with order_items. Call it directly via
// the shared pg pool so we don't go through Supabase REST.
try {
const { rows } = await pool.query<AdminOrdersResult>(
"SELECT * FROM get_admin_orders($1)",
[brandId],
);
const data = rows[0] ?? { orders: [], stops: [] };
return {
success: true,
orders: data.orders ?? [],
stops: data.stops ?? [],
error: null,
};
} catch (err) {
return {
success: false,
orders: [],
stops: [],
error: err instanceof Error ? err.message : "Failed to fetch orders",
};
}
}
export async function getAdminStops(): Promise<AdminStop[]> {
const result = await getAdminOrders();
if (!result.success) return [];
return result.stops;
}
export async function getAdminPendingOrders(): Promise<AdminOrder[]> {
const result = await getAdminOrders();
if (!result.success) return [];
return result.orders.filter((o) => !o.pickup_complete);
}
export async function getAdminPickedUpOrders(): Promise<AdminOrder[]> {
const result = await getAdminOrders();
if (!result.success) return [];
return result.orders.filter((o) => o.pickup_complete);
}
export async function getAdminOrderDetail(orderId: string): Promise<AdminOrderDetail | null> {
// Return mock order detail in mock mode
if (useMockData) {
const order = mockAdminOrders.find((o) => o.id === orderId);
if (!order) return null;
return {
...order,
discount_amount: null,
tax_amount: order.subtotal * 0.08,
tax_rate: 0.08,
tax_location: "Colorado",
discount_reason: null,
internal_notes: null,
payment_status: "paid",
payment_transaction_id: `txn_mock_${orderId}`,
refunded_amount: 0,
refund_reason: null,
refunds: [],
};
}
const adminUser = await getAdminUser();
const brandId = adminUser?.brand_id ?? null;
try {
const { rows } = await pool.query<AdminOrderDetail>(
"SELECT * FROM get_admin_order_detail($1, $2)",
[orderId, brandId],
);
return rows[0] ?? null;
} catch {
return null;
}
}