diff --git a/src/app/admin/forecast/page.tsx b/src/app/admin/forecast/page.tsx new file mode 100644 index 0000000..d974191 --- /dev/null +++ b/src/app/admin/forecast/page.tsx @@ -0,0 +1,75 @@ +import { getRevenueForecast12Months } from "@/lib/forecast-queries"; + +export const revalidate = 0; + +export default async function ForecastPage() { + const forecast = await getRevenueForecast12Months(); + const totalForecast = forecast.reduce((sum, m) => sum + m.total, 0); + + return ( +
+

Revenue Forecast

+

+ Proiezione 12 mesi basata sulle offerte attive, i loro accepted_total e le durate in mesi. + Ogni offerta distribuisce il suo totale equamente per i mesi di durata a partire dalla data di inizio. +

+ +
+ + + + + + + + + + {forecast.map((month) => { + const maxTotal = Math.max(...forecast.map((m) => m.total), 1); + const pct = Math.round((month.total / maxTotal) * 100); + return ( + + + + + + ); + })} + + + + + + + +
+ Mese + + Fatturato previsto + + Barra +
{month.label} + {month.total > 0 ? `€${month.total.toFixed(2)}` : } + + {month.total > 0 && ( +
+
+
+ )} +
Totale 12 mesi + €{totalForecast.toFixed(2)} + +
+
+ + {forecast.every((m) => m.total === 0) && ( +

+ Nessuna offerta con accepted_total trovata. Assegna micro-offerte ai progetti e imposta il totale accettato. +

+ )} +
+ ); +} diff --git a/src/app/client/[token]/page.tsx b/src/app/client/[token]/page.tsx index 6bd483c..5db6a06 100644 --- a/src/app/client/[token]/page.tsx +++ b/src/app/client/[token]/page.tsx @@ -66,6 +66,7 @@ function projectViewToClientView( created_at: n.created_at instanceof Date ? n.created_at.toISOString() : String(n.created_at), })), global_progress_pct: view.global_progress_pct, + activeOffers: view.activeOffers, }; } diff --git a/src/components/client-dashboard.tsx b/src/components/client-dashboard.tsx index 51846a0..b6a8220 100644 --- a/src/components/client-dashboard.tsx +++ b/src/components/client-dashboard.tsx @@ -7,6 +7,7 @@ import { DocumentsSection } from './documents-section'; import { NotesSection } from './notes-section'; import { PhaseViewToggle } from './client/kanban/PhaseViewToggle'; import { ChatSection } from './client/ChatSection'; +import { OffersSection } from './client/OffersSection'; interface ClientDashboardProps { view: ClientView; @@ -54,6 +55,12 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)

Pagamenti

+ {view.activeOffers && view.activeOffers.length > 0 && ( +
+

Offerte Attive

+ +
+ )}

Documenti & File

diff --git a/src/components/client/OffersSection.tsx b/src/components/client/OffersSection.tsx new file mode 100644 index 0000000..db5f9e2 --- /dev/null +++ b/src/components/client/OffersSection.tsx @@ -0,0 +1,40 @@ +interface ActiveOffer { + id: string; + public_name: string; // micro offer public name only (security: see T-05-10) + cumulative_price: string; // sum of service prices + accepted_total: string | null; +} + +interface OffersSectionProps { + offers: ActiveOffer[]; +} + +export function OffersSection({ offers }: OffersSectionProps) { + if (offers.length === 0) return null; + + return ( +
+ {offers.map((offer) => ( +
+

{offer.public_name}

+
+
+ Valore incluso + + €{parseFloat(offer.cumulative_price).toFixed(2)} + +
+ {offer.accepted_total && ( +
+ Prezzo finale + + €{parseFloat(offer.accepted_total).toFixed(2)} + +
+ )} +
+
+ ))} +
+ ); +}