Files
clienthub/src/lib/offer-value.ts
T
simone 44be190631 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>
2026-08-21 07:54:55 +02:00

79 lines
2.8 KiB
TypeScript

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;
}