
Sistema KPI end-to-end per DVC Apartments: dalla configurazione al reporting AI
DVC Apartments gestisce decine di appartamenti in affitto breve e aveva bisogno di tracciare le performance dei singoli Property Manager rispetto a budget mensili, confrontare i dati con l'anno precedente, e generare report strategici senza lavoro manuale. Abbiamo progettato un sistema KPI end-to-end che integra backend robusto, modelli dati granulari e un'interfaccia backoffice con AI insights.
La struttura dati: da configurazione a metriche reali
Il sistema ruota attorno a tre entità Mongoose principali. KpiYearConfig memorizza la configurazione annuale: budget mensile per ogni metrica (12 valori), dati dell'anno precedente per confronti YoY, e parametri Time-to-Book per 6 canali OTA (Airbnb, Booking.com, Holidu, Italianway, VRBO, dirette). Include anche benchmark interni: target per % dirette, tasso di recensioni, rating Airbnb e Booking.
PmMonthlyActual traccia 9 metriche per ogni PM ogni mese: transato lordo, transato netto pulizie (TNP), occupazione %, numero prenotazioni, ADR (Average Daily Rate), % dirette, numero recensioni, rating Airbnb e rating Booking. L'indice univoco è composto da year, month e pmUserId, garantendo atomicità e query veloci.
ApartmentKpiAssignment assegna ogni appartamento a un PM con un tier di complessità (Economy, Standard, Premium, Luxury). Ogni tier ha un moltiplicatore di peso (1x, 1.5x, 2.5x, 4x) che determina come il budget viene allocato tra i PM: una proprietà Luxury vale 4 volte una Economy a parità di metriche.
Backend: API modularizzate e sincronizzazione intelligente
Il backend Express espone 5 gruppi di endpoint REST con autorizzazione role-based (ADMIN e EDITOR). La configurazione annuale supporta POST con import CSV/JSON, GET per leggere budget e TTB, PATCH per aggiornare benchmark interni. Il servizio config.ts genera anche template CSV scaricabili con header italianizzati, facilitando la compilazione offline.
Il servizio actual.ts implementa UPSERT per i dati mensili: se l'indice (year, month, pmUserId) esiste, aggiorna; altrimenti crea. Importante: implementiamo sincronizzazione automatica del nome PM in caso di rinomina, evitando inconsistenze nei report storici. Le query sono filtrate per role: ADMIN vede tutti i PM, EDITOR vede solo i propri dati.
Il servizio assignments.ts esegue bulk write con upsert per assegnare o riassegnare appartamenti a PM con cambio tier in una sola operazione, scalando a centinaia di aggiornamenti senza timeout.
// config.ts: import CSV con validazione
import { parse } from 'csv-parse/sync';
export async function importBudgetFromCsv(csvBuffer: Buffer, year: number) {
const records = parse(csvBuffer, { columns: true, skip_empty_lines: true });
const budgets = records.map((row) => ({
metric: row.metriche,
jan: parseFloat(row.gennaio),
// ... altri mesi
}));
return KpiYearConfig.findOneAndUpdate(
{ year },
{ budgets, updatedAt: new Date() },
{ upsert: true, new: true }
);
}// actual.ts: upsert dati mensili con sincronizzazione PM
export async function upsertPmMonthlyActual(
year: number,
month: number,
pmUserId: string,
data: PmMonthlyActualInput
) {
const pmUser = await User.findById(pmUserId);
return PmMonthlyActual.findOneAndUpdate(
{ year, month, pmUserId },
{
...data,
pmName: pmUser.name, // sincronizza nome
updatedAt: new Date(),
},
{ upsert: true, new: true }
);
}Prompt engineering per report consuntivi e operativi
Il servizio prompts.ts fornisce due template per Claude. Il monthly report invia al modello i dati strutturati di budget, actual di quest'anno, actual dell'anno precedente e TTB medio, chiedendo un'analisi di performance con confronti YoY, raccomandazioni di revenue management e warning su metriche critiche.
Il weekly report è più operativo: invia lo stato di avanzamento per ogni PM, i TTB per canale, il totale DVC aggregato, e l'avanzamento atteso calcolato in funzione della settimana (week 1 = 25% del target mensile, week 2 = 50%, ecc.). Claude genera un report con semaforo on-track/ritardo per PM e 3 azioni concrete per recuperare o mantenere la traiettoria.
// prompts.ts: monthly report prompt
export const monthlyReportPrompt = (
config: KpiYearConfig,
actual2026: PmMonthlyActual[],
actual2025: PmMonthlyActual[],
month: number
) => `
Analizza performance KPI per ${ITALIAN_MONTHS[month]} 2026.
Budget: ${config.budgets.map(b => b.label + ': ' + formatEUR(b[monthKey])).join(', ')}
Attuale 2026: ${actual2026.map(a => a.metric + ': ' + formatEUR(a.value)).join(', ')}
Attuale 2025: ${actual2025.map(a => a.metric + ': ' + formatEUR(a.value)).join(', ')}
Genera:
1. Scostamento budget vs actual (% e valore assoluto)
2. Confronto YoY con insight
3. Metriche critiche
4. 3 azioni di revenue management
`;Backoffice: dashboard ADMIN e editor self-service
Il frontend Next.js 14 con App Router espone due view principali. La dashboard ADMIN è SSR e carica in parallelo config, actuals, assignments, appartamenti, utenti e anni disponibili. L'interfaccia è organizzata in 5 tab che coprono l'intera gestione KPI.
La vista EDITOR è access-controlled: il PM autenticato accede solo al mese selezionato e compila il form dei propri KPI. Una volta salvati, i dati fluiscono immediatamente in query e report per ADMIN.
Tab 1: Dashboard globale
Mostra KPI card con budget vs actual, varianza % e indicatori visuali. Un bar chart Recharts colora il transato in verde se raggiunge budget, rosso se scende. Line chart per ADR e occupazione nel tempo, bar chart per pickup pulizie mensile. Una tabella riepilogativa mostra scostamenti colorati per velocità di lettura.
Tab 2: KPI per PM
Una PmCard per ogni PM con budget personalizzato calcolato sul peso tier complessivo (somma dei multiplicatori dei suoi appartamenti). Una tabella mostra Budget/Actual/Scostamento con indicatori ▲/▼ per 7 metriche. È integrato un form di compilazione actual in-line per rapidità.
Una sezione WeeklyReport permette di selezionare la settimana (1-4) e visualizza progress bar con % raggiungimento vs atteso (week × 25% del target mensile). Un semaforo on-track/ritardo indica lo stato. Un bottone genera il report settimanale via Claude, renderizzato in markdown.
// components/PmCard.tsx: calcolo budget personalizzato
export function PmCard({ pm, assignments, yearConfig }: Props) {
const pmWeight = assignments
.filter(a => a.pmId === pm.id)
.reduce((sum, a) => sum + TIER_WEIGHTS[a.tier], 0);
const pmBudgetShare = yearConfig.budgets.map(b => ({
metric: b.label,
monthly: (b.monthly * pmWeight) / totalWeight,
}));
return (
<div className="border rounded-lg p-4">
<h3>{pm.name}</h3>
<p className="text-sm text-gray-600">Peso: {pmWeight.toFixed(2)}x</p>
{/* Budget/Actual table */}
</div>
);
}Tab 3: Assegnazioni appartamenti
Tabella di gestione con dropdown per scegliere PM e Tier per ogni appartamento. Una stat card mostra conteggi per PM e peso % calcolato. Cambio bulk con upsert lato server, salvataggio atomico.
Tab 4: Time-to-Book (TTB)
Grafico e tabella del TTB (giorni di prenotazione anticipata) per 6 canali OTA, 12 mesi + media annua. Palette colori dedicata per canale facilita il riconoscimento. Utilità strategica: TTB alto indica mese "consolidato" con poco margine, TTB basso indica opportunità per azioni last-minute (sconti, promozioni dirette).
Tab 5: Monthly Report (AI)
5 stat card riassuntive della performance globale. Un bottone genera il report mensile consuntivo via Claude API con analisi budget vs actual, confronti YoY e raccomandazioni. La risposta viene renderizzata in markdown direttamente nell'interfaccia.
Utility library: formatter e calcoli KPI
kpi-utils.ts centralizza formattatori (EUR, %, numeri interi con locale IT), funzioni di allocazione budget (calcPmWeight, calcPmBudgetShare), aggregazione multi-PM (aggregateActualsByMonth somma transato/prenotazioni, media occupazione/ADR escludendo PM senza dato). Include costanti: mesi italiani, tier weights, canali TTB con label e colori, palette DVC.
// kpi-utils.ts: calcoli core
export const TIER_WEIGHTS = {
Economy: 1,
Standard: 1.5,
Premium: 2.5,
Luxury: 4,
};
export function calcPmWeight(
assignments: ApartmentKpiAssignment[],
pmId: string
): number {
return assignments
.filter(a => a.pmId === pmId)
.reduce((sum, a) => sum + TIER_WEIGHTS[a.tier], 0);
}
export function formatEUR(value: number): string {
return new Intl.NumberFormat('it-IT', {
style: 'currency',
currency: 'EUR',
}).format(value);
}Integrazione Claude API e generazione report
Il sistema integra Anthropic SDK con temperatura 0.3 (deterministica) e modello claude-sonnet-4-6. Gli endpoint API REST POST /api/kpi/reports/monthly e /api/kpi/reports/weekly ricevono i dati strutturati, costruiscono il prompt via prompts.ts, chiamano Claude e renderizzano il markdown in frontend.
Il prompt monthly integra context esteso (budget 12 mesi, actual 2026/2025, TTB medio) per analisi ricca. Il prompt weekly è più snello ma include semaforo per PM (on-track se avanzamento >= atteso, ritardo se < atteso) e 3 azioni concrete basate su TTB basso/alto e metriche critiche.
Decisioni di design e tradeoff
Abbiamo scelto MongoDB e Mongoose per flessibilità su schema e query parallele veloci. Un indice composto su (year, month, pmUserId) garantisce UPSERT atomico senza race condition. Recharts per grafici perché leggera e reattiva su mount, evitando heavy lib come Chart.js.
Per l'autorizzazione ADMIN/EDITOR, implementiamo middleware Express che controlla il role dal JWT e filtra query. ADMIN vede tutto, EDITOR accede solo a pmUserId === req.user.id. Questo pattern scala bene se in futuro aggiungiamo role granulari (es. Finance, Ops).
La sincronizzazione del nome PM in actual.ts evita inconsistenze storiche: se un PM si rinomina, tutti i record da quel PM mantengono il nome aggiornato. Alternativa era duplicare pmName a scopo reporting, ma la sincronizzazione è più pulita.
Generazione di template CSV scaricabili semplifica l'onboarding: PM non esperti di software possono compilare offline in Excel e re-import. csv-parse è leggera e hand; Papaparse era un'opzione, ma Node.js nativo è più adatto al backend.
Risultati e impatto
DVC Apartments ora monitora in tempo reale le performance di ogni PM, confronta automaticamente con budget e anno precedente, e genera report strategici senza compilazione manuale. I PM accedono al self-service editor e vedono il feedback immediato. La dashboard ADMIN aggregata permette decisioni veloci su revenue management e assegnazione appartamenti. L'integrazione Claude automatizza il report consuntivo e operativo, liberando risorse per strategie piuttosto che data entry.
Il sistema è stato sviluppato con TypeScript, test unitari su servizi critico (calcoli budget, UPSERT) e deploy su Vercel (frontend) e server Node.js dedicato (backend). Scalabilità orizzontale del backend è pronta se il numero di PM e appartamenti cresce.


