feat(portale): barra full-width, card offerta senza accordion, valore override
Barra di avanzamento a tutta larghezza (via il max-w-[1200px]). Card offerta: rimosso l'accordion "Cosa è compreso". La lista servizi non arriva più al client — client-view.ts legge dai servizi i soli prezzi — e OffersSection perde il suo unico useState, quindi anche il "use client". "Valore incluso" diventa "Valore dell'offerta" e accetta un override manuale (migration 0020, project_offers.offer_value_override). Prima era sempre la somma dei prezzi di catalogo del tier: in produzione mostrava €20.250 su offerte vendute a 7.000 e 5.500. NULL torna al calcolo, 0 nasconde la riga. Si gestisce dal tab Offerte del progetto, che affianca la cifra calcolata per far vedere cosa si sta sostituendo. La somma calcolata vive ora in src/lib/offer-value.ts, letta sia dal portale sia dall'admin: duplicarla avrebbe fatto divergere le due viste. "Prezzo finale" diventa "Investimento finale" (il ricorrente resta "Canone mensile"). Migration 0020 applicata a prod prima del push. Override tutti NULL, quindi il comportamento resta invariato finché non se ne imposta uno. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { inArray, eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
offer_tier_services,
|
||||
services,
|
||||
offer_micro_services,
|
||||
offer_services,
|
||||
} from "@/db/schema";
|
||||
|
||||
/**
|
||||
* "Valore dell'offerta" calcolato: la somma dei prezzi dei servizi collegati a
|
||||
* un tier (micro).
|
||||
*
|
||||
* Due sorgenti, nell'ordine: `offer_tier_services` → `services` (catalogo
|
||||
* Phase 12) e, per i tier che non hanno righe lì, il legacy
|
||||
* `offer_micro_services` → `offer_services`.
|
||||
*
|
||||
* Vive in un modulo neutro perché lo leggono **due** lati: il portale cliente
|
||||
* (`client-view.ts`, dove è il fallback quando l'admin non ha messo un
|
||||
* override) e l'area admin (`admin-queries.ts`, dove serve a mostrare quale
|
||||
* cifra si sta sostituendo). Duplicarlo significherebbe farli divergere.
|
||||
*
|
||||
* Legge solo prezzi: nomi e descrizioni dei servizi non servono a nessuno dei
|
||||
* due chiamanti, e client-view non deve proiettare più di quanto renderizza.
|
||||
*
|
||||
* @returns Map micro_id → totale con 2 decimali. I micro senza servizi non
|
||||
* compaiono nella mappa (il chiamante decide se è "0" o "assente").
|
||||
*/
|
||||
export async function getComputedOfferValues(
|
||||
microIds: string[]
|
||||
): Promise<Map<string, string>> {
|
||||
const out = new Map<string, string>();
|
||||
if (microIds.length === 0) return out;
|
||||
|
||||
const sum = (prices: Array<string | null>) =>
|
||||
prices.reduce((acc, p) => acc + (parseFloat(p ?? "0") || 0), 0).toFixed(2);
|
||||
|
||||
// Sorgente primaria: catalogo unificato.
|
||||
const tierRows = await db
|
||||
.select({
|
||||
tier_id: offer_tier_services.tier_id,
|
||||
unit_price: services.unit_price,
|
||||
})
|
||||
.from(offer_tier_services)
|
||||
.innerJoin(services, eq(offer_tier_services.service_id, services.id))
|
||||
.where(inArray(offer_tier_services.tier_id, microIds));
|
||||
|
||||
const byTier = new Map<string, Array<string | null>>();
|
||||
for (const row of tierRows) {
|
||||
const existing = byTier.get(row.tier_id) ?? [];
|
||||
existing.push(row.unit_price);
|
||||
byTier.set(row.tier_id, existing);
|
||||
}
|
||||
for (const [tierId, prices] of byTier) out.set(tierId, sum(prices));
|
||||
|
||||
// Fallback legacy solo per i micro rimasti scoperti.
|
||||
const needLegacy = microIds.filter((id) => !out.has(id));
|
||||
if (needLegacy.length === 0) return out;
|
||||
|
||||
const legacyRows = await db
|
||||
.select({
|
||||
micro_id: offer_micro_services.micro_id,
|
||||
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, needLegacy));
|
||||
|
||||
const byMicro = new Map<string, Array<string | null>>();
|
||||
for (const row of legacyRows) {
|
||||
const existing = byMicro.get(row.micro_id) ?? [];
|
||||
existing.push(row.price);
|
||||
byMicro.set(row.micro_id, existing);
|
||||
}
|
||||
for (const [microId, prices] of byMicro) out.set(microId, sum(prices));
|
||||
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user