feat(05-02): add offer-queries.ts and server actions for offer catalog CRUD
- Create src/lib/offer-queries.ts with getCatalogWithMicros, getAllOfferServices, getMicroAssignedServiceIds - Create src/app/admin/offers/actions.ts with createMacro, deleteMacro, createMicro, deleteMicro, createOfferService, toggleOfferServiceActive, updateMicroOfferServices - All exported server actions guard with requireAdmin() + revalidatePath(/admin/offers)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
offer_macros,
|
||||
offer_micros,
|
||||
offer_services,
|
||||
offer_micro_services,
|
||||
} from "@/db/schema";
|
||||
import type { OfferMacro, OfferMicro, OfferService } from "@/db/schema";
|
||||
import { eq, asc, inArray, sql } from "drizzle-orm";
|
||||
|
||||
export type MicroWithServices = OfferMicro & {
|
||||
services: Array<{ id: string; name: string; price: string }>;
|
||||
cumulative_price: string;
|
||||
};
|
||||
|
||||
export type MacroWithMicros = OfferMacro & {
|
||||
micros: MicroWithServices[];
|
||||
};
|
||||
|
||||
export async function getCatalogWithMicros(): Promise<MacroWithMicros[]> {
|
||||
const macros = await db
|
||||
.select()
|
||||
.from(offer_macros)
|
||||
.orderBy(asc(offer_macros.sort_order), asc(offer_macros.created_at));
|
||||
|
||||
if (macros.length === 0) return [];
|
||||
|
||||
const macroIds = macros.map((m) => m.id);
|
||||
|
||||
const micros = await db
|
||||
.select()
|
||||
.from(offer_micros)
|
||||
.where(inArray(offer_micros.macro_id, macroIds))
|
||||
.orderBy(asc(offer_micros.sort_order));
|
||||
|
||||
const microIds = micros.map((m) => m.id);
|
||||
|
||||
if (microIds.length === 0) {
|
||||
return macros.map((m) => ({ ...m, micros: [] }));
|
||||
}
|
||||
|
||||
// Fetch junction rows + service data in one query
|
||||
const assignments = await db
|
||||
.select({
|
||||
micro_id: offer_micro_services.micro_id,
|
||||
service_id: offer_services.id,
|
||||
name: offer_services.name,
|
||||
price: offer_services.price,
|
||||
})
|
||||
.from(offer_micro_services)
|
||||
.innerJoin(offer_services, eq(offer_micro_services.service_id, offer_services.id))
|
||||
.where(inArray(offer_micro_services.micro_id, microIds));
|
||||
|
||||
// Cumulative price per micro
|
||||
const cumulRows = await db
|
||||
.select({
|
||||
micro_id: offer_micro_services.micro_id,
|
||||
cumulative_price: sql<string>`coalesce(sum(${offer_services.price}::numeric), 0)`,
|
||||
})
|
||||
.from(offer_micro_services)
|
||||
.innerJoin(offer_services, eq(offer_micro_services.service_id, offer_services.id))
|
||||
.where(inArray(offer_micro_services.micro_id, microIds))
|
||||
.groupBy(offer_micro_services.micro_id);
|
||||
|
||||
const cumulMap = new Map(cumulRows.map((r) => [r.micro_id, r.cumulative_price]));
|
||||
|
||||
const microsWithServices: MicroWithServices[] = micros.map((micro) => ({
|
||||
...micro,
|
||||
services: assignments
|
||||
.filter((a) => a.micro_id === micro.id)
|
||||
.map((a) => ({ id: a.service_id, name: a.name, price: String(a.price) })),
|
||||
cumulative_price: cumulMap.get(micro.id) ?? "0",
|
||||
}));
|
||||
|
||||
return macros.map((macro) => ({
|
||||
...macro,
|
||||
micros: microsWithServices.filter((m) => m.macro_id === macro.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getAllOfferServices(): Promise<OfferService[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(offer_services)
|
||||
.where(eq(offer_services.active, true))
|
||||
.orderBy(asc(offer_services.name));
|
||||
}
|
||||
|
||||
// Returns assigned service IDs for a given micro-offer (used by ServiceCheckboxList)
|
||||
export async function getMicroAssignedServiceIds(microId: string): Promise<string[]> {
|
||||
const rows = await db
|
||||
.select({ service_id: offer_micro_services.service_id })
|
||||
.from(offer_micro_services)
|
||||
.where(eq(offer_micro_services.micro_id, microId));
|
||||
return rows.map((r) => r.service_id);
|
||||
}
|
||||
Reference in New Issue
Block a user