Vai al contenuto principale
Kpi Dashboard APT

Sistema KPI end-to-end per DVC Apartments: dalla configurazione al reporting AI

29 mag 2026·7 min di lettura
DashboardKPIMongoDBNode.jsExpress.jsTypescriptKPI DashboardClaude APIRole-based Access

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.

services/config.ts
// 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 }
  );
}
services/actual.ts
// 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.

services/prompts.ts
// 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
// 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.

lib/kpi-utils.ts
// 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.

Articoli correlati

Ask Fabio