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.
+
+
+
+
+
+
+ |
+ Mese
+ |
+
+ Fatturato previsto
+ |
+
+ Barra
+ |
+
+
+
+ {forecast.map((month) => {
+ const maxTotal = Math.max(...forecast.map((m) => m.total), 1);
+ const pct = Math.round((month.total / maxTotal) * 100);
+ return (
+
+ | {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)}
+
+
+ )}
+
+
+ ))}
+
+ );
+}