feat: tier names + per-tier prices on offer cards, unified admin UI
- offer editor: per-tier name input (mirrors public_name/internal_name) - offer list cards: show 3-tier services total + manual public price - shared PageHeader component, full-width layout across all admin pages - UI-RULES.md design conventions doc Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,16 +105,23 @@ export async function getMicroAssignedServiceIds(microId: string): Promise<strin
|
||||
const OFFER_TIPO_ENTITY = "offer_macros.tipo";
|
||||
const OFFER_OBIETTIVO_ENTITY = "offer_macros.obiettivo";
|
||||
|
||||
export type OfferListTier = {
|
||||
tier_letter: string | null;
|
||||
public_name: string;
|
||||
public_price: string | null;
|
||||
servicesTotal: string;
|
||||
};
|
||||
|
||||
export type OfferListCard = Pick<
|
||||
OfferMacro,
|
||||
"id" | "internal_name" | "description" | "category" | "is_archived"
|
||||
>;
|
||||
> & { tiers: OfferListTier[] };
|
||||
|
||||
// List-page cards (Plan 04): one row per offer_macros, with category + archive
|
||||
// status for client-side filtering. Archived offers ARE included — the
|
||||
// "Mostra offerte archiviate" toggle filters client-side per UI-SPEC.
|
||||
export async function getOfferListCards(): Promise<OfferListCard[]> {
|
||||
const rows = await db
|
||||
const macros = await db
|
||||
.select({
|
||||
id: offer_macros.id,
|
||||
internal_name: offer_macros.internal_name,
|
||||
@@ -124,7 +131,57 @@ export async function getOfferListCards(): Promise<OfferListCard[]> {
|
||||
})
|
||||
.from(offer_macros)
|
||||
.orderBy(asc(offer_macros.sort_order), asc(offer_macros.created_at));
|
||||
return rows;
|
||||
|
||||
if (macros.length === 0) return [];
|
||||
|
||||
const macroIds = macros.map((m) => m.id);
|
||||
|
||||
// Fetch all tiers for these macros, ordered A -> B -> C
|
||||
const tierOrder = sql`CASE ${offer_micros.tier_letter} WHEN 'A' THEN 1 WHEN 'B' THEN 2 WHEN 'C' THEN 3 ELSE 4 END`;
|
||||
const tierRows = await db
|
||||
.select({
|
||||
id: offer_micros.id,
|
||||
macro_id: offer_micros.macro_id,
|
||||
tier_letter: offer_micros.tier_letter,
|
||||
public_name: offer_micros.public_name,
|
||||
public_price: offer_micros.public_price,
|
||||
})
|
||||
.from(offer_micros)
|
||||
.where(inArray(offer_micros.macro_id, macroIds))
|
||||
.orderBy(asc(tierOrder));
|
||||
|
||||
const allTierIds = tierRows.map((t) => t.id);
|
||||
|
||||
// Compute servicesTotal per tier in batch
|
||||
let totalsByTierId = new Map<string, string>();
|
||||
if (allTierIds.length > 0) {
|
||||
const totalRows = await db
|
||||
.select({
|
||||
tier_id: offer_tier_services.tier_id,
|
||||
servicesTotal: sql<string>`coalesce(sum(${services.unit_price}::numeric), 0)`,
|
||||
})
|
||||
.from(offer_tier_services)
|
||||
.innerJoin(services, eq(offer_tier_services.service_id, services.id))
|
||||
.where(inArray(offer_tier_services.tier_id, allTierIds))
|
||||
.groupBy(offer_tier_services.tier_id);
|
||||
|
||||
totalsByTierId = new Map(totalRows.map((r) => [r.tier_id, r.servicesTotal]));
|
||||
}
|
||||
|
||||
// Group tiers under each macro
|
||||
const tiersByMacroId = new Map<string, OfferListTier[]>();
|
||||
for (const t of tierRows) {
|
||||
const list = tiersByMacroId.get(t.macro_id) ?? [];
|
||||
list.push({
|
||||
tier_letter: t.tier_letter,
|
||||
public_name: t.public_name,
|
||||
public_price: t.public_price,
|
||||
servicesTotal: totalsByTierId.get(t.id) ?? "0",
|
||||
});
|
||||
tiersByMacroId.set(t.macro_id, list);
|
||||
}
|
||||
|
||||
return macros.map((m) => ({ ...m, tiers: tiersByMacroId.get(m.id) ?? [] }));
|
||||
}
|
||||
|
||||
export type OfferTierData = {
|
||||
|
||||
Reference in New Issue
Block a user