Architettura, API e guida tecnica per sviluppatori del Property Management System di CheckIn Facile.
Naviga rapidamente verso la sezione che ti interessa.
Il PMS di CheckIn Facile segue un'architettura a tre livelli con separazione netta tra presentazione, logica di business e persistenza dati. Il sistema utilizza il Service Layer Pattern con manager specializzati per dominio.
User Action → Component → usePMS Hook → PMSService → Manager → Storage
↓
User Feedback ← Component ← usePMS Hook ← Updated State ←──┘
| Pattern | Utilizzo |
|---|---|
| Singleton | PMSService instance globale |
| Manager Pattern | Separazione logica per dominio |
| Custom Hook | Integrazione React con servizi |
| Observer | Aggiornamenti reattivi dello stato |
| Repository | Astrazione del layer di persistenza |
PMS_LABELS.Il codice sorgente del PMS è organizzato seguendo una struttura modulare all'interno della directory src/.
Nota: Il file pmsService.js contiene circa 1800+ righe di codice ed è il cuore dell'intero sistema PMS.
Il file pmsService.js contiene costanti, enumerazioni, 5 manager specializzati, la classe PMSService principale, il generatore di mock data e l'istanza singleton esportata.
export const RESERVATION_STATUS = { CONFIRMED: 'confirmed', // Prenotazione confermata CHECKED_IN: 'checked_in', // Ospite arrivato CHECKED_OUT: 'checked_out', // Ospite partito CANCELLED: 'cancelled', // Cancellata NO_SHOW: 'no_show', // Non presentato PENDING: 'pending' // In attesa }; export const ROOM_STATUS = { CLEAN: 'clean', // Pulita e pronta DIRTY: 'dirty', // Da pulire CLEANING: 'cleaning', // Pulizia in corso INSPECTED: 'inspected', // Ispezionata OUT_OF_ORDER: 'out_of_order', // Fuori servizio (guasto) OUT_OF_SERVICE: 'out_of_service' // Fuori servizio (manutenzione) }; export const MAINTENANCE_PRIORITY = { EMERGENCY: 'emergency', // Risoluzione immediata HIGH: 'high', // Entro 24h MEDIUM: 'medium', // Entro 48h LOW: 'low' // Programmabile };
Gestisce l'intero ciclo di vita delle prenotazioni: creazione, aggiornamento, check-in, check-out, cancellazione e query avanzate.
class ReservationManager { // CRUD getAll() // Tutte le prenotazioni getById(id) // Per ID create(data) // Crea nuova update(id, updates) // Aggiorna delete(id) // Elimina // Query getByRoom(roomId) // Per camera getByDateRange(start, end) // Per range date getArrivals(date) // Arrivi del giorno getDepartures(date) // Partenze del giorno getInHouse() // Ospiti in casa // Azioni checkIn(id, data) // Esegue check-in checkOut(id, data) // Esegue check-out cancel(id, reason) // Cancella markNoShow(id) // Segna no-show // Utilità isRoomAvailable(roomId, checkIn, checkOut, excludeId) getOccupancyRate(date) }
Gestisce task di pulizia, stato camere, assegnazioni e checklist digitali.
class HousekeepingManager { getTasks() / createTask(data) / completeTask(id, data) getRoomStatus(roomId) / updateRoomStatus(roomId, status) getTasksByAssignee(staffId) / getUrgentTasks() generateDailyTasks(date) / assignTask(taskId, staffId) getTaskChecklist(taskId) / updateChecklistItem(taskId, itemId, completed) }
Gestisce i ticket di manutenzione con workflow di stati, commenti e statistiche.
class MaintenanceManager { getTickets() / createTicket(data) / updateTicket(id, updates) startWork(id, technicianId) / completeWork(id, data) addComment(ticketId, comment) / getComments(ticketId) getOpenTickets() / getUrgentTickets() getStatistics(dateRange) / getAverageResolutionTime() }
Gestisce prodotti, scorte, movimenti di magazzino e generazione liste acquisti.
class InventoryManager { getProducts() / createProduct(data) / updateProduct(id, updates) addStock(productId, qty, notes) / removeStock(productId, qty, notes) getLowStockProducts() / getOutOfStockProducts() getMovements(productId) / getInventoryValue() generateShoppingList() }
Gestisce l'assegnazione dei compiti, notifiche e monitoraggio task per lo staff.
class StaffTaskManager { getTasks() / createTask(data) / completeTask(id, data) assignTask(taskId, staffId) / reassignTask(taskId, newStaffId) getTasksByStaff(staffId) / getOverdueTasks() sendTaskNotification(taskId) / sendReminder(taskId) }
Oltre al core service, il sistema include servizi specializzati modulari e indipendenti che estendono le funzionalità del PMS.
Fornisce metriche, KPI e analisi delle performance della struttura ricettiva.
import { analyticsService } from '../services/pmsAnalyticsService'; // Metriche principali analyticsService.calculateOccupancyRate({ startDate, endDate, reservations, totalRooms }); analyticsService.calculateADR({ totalRevenue, roomNightsSold }); analyticsService.calculateRevPAR({ totalRevenue, availableRoomNights }); analyticsService.generatePerformanceReport({ startDate, endDate, ... });
| Metrica | Formula | Utilizzo |
|---|---|---|
| Occupancy Rate | (Notti vendute / Notti disponibili) x 100 | Domanda e pricing |
| ADR | Revenue totale / Notti vendute | Valore medio prenotazione |
| RevPAR | Revenue totale / Notti disponibili | Performance complessiva |
| Cancellation Rate | Cancellazioni / Totale prenotazioni | Rischio revenue |
Gestisce notifiche e alert tramite canali multipli: in-app, email, SMS, push e WhatsApp.
import { notificationService, NOTIFICATION_TYPES } from '../services/pmsNotificationService'; notificationService.send({ type: NOTIFICATION_TYPES.HOUSEKEEPING_ASSIGNED, recipient: 'staff-123', channel: 'in_app', data: { taskId: 'hk-456', roomName: 'Camera 101', priority: 'urgent' } }); notificationService.getUnread('user-123'); notificationService.markAsRead('notification-123');
Gestisce staff, turni, competenze, carichi di lavoro e assegnazione automatica intelligente.
import { staffService } from '../services/pmsStaffManagementService'; staffService.getAvailableStaff({ date, department, skill }); staffService.calculateWorkload({ staffId, date }); staffService.autoAssign({ task, availableStaff, considerWorkload: true }); staffService.getPerformanceReport({ staffId, startDate, endDate });
Gestisce tariffe dinamiche con aggiustamenti stagionali, per giorno della settimana, sconti per durata soggiorno e pricing basato sull'occupazione.
import { ratesService } from '../services/pmsRatesService'; const pricing = ratesService.calculateStayPrice({ roomType: 'double', checkIn: '2025-07-15', checkOut: '2025-07-22', guests: 2, currentOccupancy: 75, promoCode: 'SUMMER10' }); // Result: { basePrice, seasonalAdj, losDiscount, finalPrice, breakdown[] }
Profilo ospiti completo con storico soggiorni, preferenze, loyalty, segmentazione e campagne mirate.
import { guestCRMService } from '../services/pmsGuestCRMService'; guestCRMService.searchGuests({ query: 'Rossi', tags: ['high_value'] }); guestCRMService.getRecommendations('guest-001'); guestCRMService.segmentGuests({ criteria: 'total_spent' }); guestCRMService.sendCampaign({ segment: 'gold', template: 'loyalty_offer' });
I servizi lavorano insieme in workflow completi. Esempio: checkout automatico.
async function handleCheckout(reservationId) { // 1. Aggiornare stato prenotazione const reservation = pmsService.reservations.checkOut(reservationId); // 2. Creare task pulizia automatico const task = pmsService.housekeeping.createTask({ roomId: reservation.roomId, type: 'checkout_clean' }); // 3. Notificare housekeeping notificationService.send({ type: 'housekeeping_assigned', recipient: task.assignedTo }); // 4. Aggiornare analytics e CRM analyticsService.recordCheckout({ reservationId }); guestCRMService.updateStayHistory({ guestId: reservation.guestId }); }
L'hook usePMS è il bridge tra il servizio PMS e i componenti React. Gestisce inizializzazione, persistenza su localStorage, aggiornamenti reattivi e integrazione con il DataContext esistente.
import { useState, useEffect, useCallback } from 'react'; import { useData } from '../context/DataContext'; import { pmsService } from '../services/pmsService'; export const usePMS = () => { const { property, currentPropertyId } = useData(); const [loading, setLoading] = useState(true); const [reservations, setReservations] = useState([]); const [housekeepingTasks, setHousekeepingTasks] = useState([]); const [maintenanceTickets, setMaintenanceTickets] = useState([]); const [inventory, setInventory] = useState([]); // ... inizializzazione e auto-save su localStorage return { loading, reservations, housekeepingTasks, maintenanceTickets, inventory, addReservation, updateReservation, checkIn, checkOut, addHousekeepingTask, completeHousekeepingTask, addMaintenanceTicket, addInventoryProduct, getDashboardOverview, getArrivals, getDepartures, // ... }; };
import { usePMS } from '../hooks/usePMS'; const MyComponent = () => { const { loading, reservations, getArrivals, addReservation } = usePMS(); if (loading) return <Spinner />; const todayArrivals = getArrivals(new Date()); return <div>Arrivi Oggi: {todayArrivals.length}</div>; };
Ogni pagina PMS è un componente React autonomo che utilizza l'hook usePMS per accedere a dati e metodi.
/pmsDashboard principale con panoramica di tutti i moduli: Quick Stats (KPI), arrivi e partenze del giorno, griglia stato camere, task housekeeping urgenti, alert manutenzione, prodotti sotto scorta.
/pms/housekeepingGestione completa pulizie: lista task con filtri (stato, priorità, assegnatario), vista griglia camere, creazione/modifica task, assegnazione staff, completamento con checklist, timeline attività.
/pms/maintenanceGestione ticket manutenzione: lista con filtri multipli, creazione con categoria e priorità, workflow stati (open → in_progress → completed), sistema commenti, storico interventi, statistiche risoluzione.
/pms/inventoryGestione inventario: catalogo prodotti con categorie, gestione stock (carico/scarico), alert scorte basse, storico movimenti, generazione lista acquisti, report valore inventario.
/pms/calendarCalendario interattivo con drag & drop per spostare prenotazioni, resize per estendere soggiorni, color coding per stato, quick create su cella vuota, viste settimana/mese.
// In App.jsx <Route path="/pms" element={<ProtectedRoute><PMSDashboardPage /></ProtectedRoute>} /> <Route path="/pms/housekeeping" element={<ProtectedRoute><HousekeepingPage /></ProtectedRoute>} /> <Route path="/pms/maintenance" element={<ProtectedRoute><MaintenancePage /></ProtectedRoute>} /> <Route path="/pms/inventory" element={<ProtectedRoute><InventoryPage /></ProtectedRoute>} /> <Route path="/pms/calendar" element={<ProtectedRoute><ReservationsCalendarPage /></ProtectedRoute>} />
Le interfacce TypeScript-style che descrivono la struttura dei dati principali del sistema.
interface Reservation { id: string; propertyId: string; roomId: string; guestName: string; guestEmail?: string; guestPhone?: string; checkIn: string; // ISO date checkOut: string; // ISO date adults: number; children: number; totalPrice: number; status: ReservationStatus; source: ReservationSource; notes?: string; specialRequests?: string[]; createdAt: string; updatedAt: string; }
interface HousekeepingTask { id: string; roomId: string; roomName: string; type: HousekeepingTaskType; // checkout_clean | stay_over | deep_clean | ... priority: HousekeepingPriority; // urgent | high | normal | low status: TaskStatus; assignedTo?: string; scheduledDate: string; estimatedDuration: number; // minuti checklist: ChecklistItem[]; startedAt?: string; completedAt?: string; } interface ChecklistItem { id: string; task: string; completed: boolean; }
interface MaintenanceTicket { id: string; title: string; description: string; roomId?: string; category: MaintenanceCategory; // plumbing | electrical | hvac | ... priority: MaintenancePriority; // emergency | high | medium | low status: MaintenanceStatus; reportedBy: string; assignedTo?: string; estimatedCost?: number; actualCost?: number; comments: Comment[]; resolutionNotes?: string; }
interface InventoryProduct { id: string; name: string; sku?: string; category: InventoryCategory; // linens | toiletries | cleaning | ... unit: string; currentStock: number; minimumStock: number; reorderPoint: number; unitCost: number; supplier?: string; movements: StockMovement[]; } interface StockMovement { id: string; type: 'in' | 'out' | 'adjustment'; quantity: number; previousStock: number; newStock: number; performedBy: string; }
interface Room { id: string; name: string; number: string; floor: number; type: RoomType; // single | double | twin | suite | ... capacity: number; basePrice: number; amenities: string[]; status: RoomStatus; }
Riferimento completo dei metodi esposti dall'hook usePMS.
| Property | Tipo | Descrizione |
|---|---|---|
loading | boolean | True durante il caricamento iniziale |
initialized | boolean | True quando il PMS è inizializzato |
reservations | Reservation[] | Lista prenotazioni |
housekeepingTasks | HousekeepingTask[] | Lista task pulizie |
roomStatuses | Record<string, RoomStatus> | Mappa stato camere |
maintenanceTickets | MaintenanceTicket[] | Lista ticket manutenzione |
inventory | InventoryProduct[] | Lista prodotti inventario |
rooms | Room[] | Lista camere |
| Metodo | Parametri | Descrizione |
|---|---|---|
addReservation | data: Partial<Reservation> | Crea nuova prenotazione |
updateReservation | id, updates | Aggiorna prenotazione |
deleteReservation | id: string | Elimina prenotazione |
checkIn | id, data? | Esegue check-in |
checkOut | id, data? | Esegue check-out |
getArrivals | date: Date | Arrivi del giorno |
getDepartures | date: Date | Partenze del giorno |
| Metodo | Parametri | Descrizione |
|---|---|---|
addHousekeepingTask | data | Crea task pulizia |
completeHousekeepingTask | id, data? | Completa task |
updateRoomStatus | roomId, status | Aggiorna stato camera |
// Reservations GET /api/pms/reservations POST /api/pms/reservations GET /api/pms/reservations/:id PUT /api/pms/reservations/:id DELETE /api/pms/reservations/:id POST /api/pms/reservations/:id/checkin POST /api/pms/reservations/:id/checkout // Housekeeping GET /api/pms/housekeeping/tasks POST /api/pms/housekeeping/tasks PUT /api/pms/housekeeping/tasks/:id POST /api/pms/housekeeping/tasks/:id/complete // Maintenance GET /api/pms/maintenance/tickets POST /api/pms/maintenance/tickets PUT /api/pms/maintenance/tickets/:id POST /api/pms/maintenance/tickets/:id/comments // Inventory GET /api/pms/inventory/products POST /api/pms/inventory/products PUT /api/pms/inventory/products/:id POST /api/pms/inventory/products/:id/stock
Il PMS utilizza un pattern Flux-like con React Context + Custom Hooks per la gestione dello stato.
1. User Action (es. click "Completa Task") ↓ 2. Component chiama metodo hook (completeHousekeepingTask) ↓ 3. Hook aggiorna stato locale (setHousekeepingTasks) ↓ 4. Hook chiama servizio (pmsService.housekeeping.complete) ↓ 5. Servizio aggiorna dati interni ↓ 6. Hook triggera salvataggio (saveToStorage) ↓ 7. React re-renderizza componenti interessati
// Ogni cambio di stato triggera il salvataggio automatico useEffect(() => { if (initialized) { const dataToSave = { version: PMS_VERSION, propertyId: currentPropertyId, reservations, housekeepingTasks, roomStatuses, maintenanceTickets, inventory, staffTasks, rooms, savedAt: new Date().toISOString() }; localStorage.setItem(PMS_STORAGE_KEY, JSON.stringify(dataToSave)); } }, [initialized, reservations, housekeepingTasks, maintenanceTickets, inventory]);
I dati sono filtrati per propertyId per supportare la gestione multi-proprietà:
const getPropertyReservations = useCallback(() => { return reservations.filter(r => r.propertyId === currentPropertyId); }, [reservations, currentPropertyId]);
// Prima (localStorage - demo) const loadData = () => { const saved = localStorage.getItem(PMS_STORAGE_KEY); return saved ? JSON.parse(saved) : null; }; // Dopo (API REST - produzione) const loadData = async () => { const response = await fetch('/api/pms/data'); return response.json(); };
Strategie implementate per mantenere il PMS reattivo e fluido anche con grandi quantità di dati.
// Memoizza calcoli costosi const occupancyRate = useMemo(() => { const occupied = reservations.filter(r => r.status === RESERVATION_STATUS.CHECKED_IN ).length; return (occupied / rooms.length) * 100; }, [reservations, rooms.length]); // Memoizza callback passati come props const handleClick = useCallback((id) => { setSelectedReservation(id); }, []);
Per liste con oltre 100 elementi, viene utilizzata la virtualizzazione con react-window:
import { FixedSizeList } from 'react-window'; <FixedSizeList height={600} itemCount={reservations.length} itemSize={80}> {({ index, style }) => ( <ReservationRow reservation={reservations[index]} style={style} /> )} </FixedSizeList>
const PMSDashboardPage = lazy(() => import('./pages/PMSDashboardPage')); const CalendarPage = lazy(() => import('./pages/ReservationsCalendarPage')); <Suspense fallback={<LoadingSpinner />}> <Routes>...</Routes> </Suspense>
import { useDebouncedCallback } from 'use-debounce'; const debouncedSearch = useDebouncedCallback((query) => { setSearchResults(filterReservations(query)); }, 300); // Salvataggio localStorage differito (max 1 volta/secondo) const saveToStorage = useDebouncedCallback(() => { localStorage.setItem(PMS_STORAGE_KEY, JSON.stringify(state)); }, 1000);
| Metrica | Target | Azione se superato |
|---|---|---|
| First Paint | < 1.5s | Ottimizzare bundle |
| Time to Interactive | < 3s | Lazy loading |
| Largest Contentful Paint | < 2.5s | Ottimizzare immagini |
| Re-renders per interazione | < 3 | Memoizzazione |
La sicurezza nel PMS segue il principio della Defense in Depth: layer multipli di protezione.
const validateReservation = (data) => { const errors = []; // Nome ospite: richiesto, no HTML if (!data.guestName || data.guestName.trim() === '') errors.push('Nome ospite richiesto'); if (/<[^>]*>/.test(data.guestName)) errors.push('Nome non valido'); // Date: check-in prima di check-out if (new Date(data.checkOut) <= new Date(data.checkIn)) errors.push('Check-out deve essere dopo check-in'); // Email: formato valido se presente if (data.guestEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.guestEmail)) errors.push('Email non valida'); return { valid: errors.length === 0, errors }; };
import DOMPurify from 'dompurify'; const SafeText = ({ text }) => { const clean = DOMPurify.sanitize(text, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }); return <span>{clean}</span>; };
const ROLES = { ADMIN: 'admin', MANAGER: 'manager', RECEPTIONIST: 'receptionist', HOUSEKEEPER: 'housekeeper' }; const PERMISSIONS = { [ROLES.ADMIN]: ['*'], [ROLES.MANAGER]: ['reservations.*', 'housekeeping.*', 'maintenance.*'], [ROLES.RECEPTIONIST]: ['reservations.*', 'housekeeping.read'], [ROLES.HOUSEKEEPER]: ['housekeeping.own', 'maintenance.create'] }; const hasPermission = (user, permission) => { const perms = PERMISSIONS[user.role] || []; return perms.includes('*') || perms.includes(permission); };
Attenzione: Non salvare mai in localStorage numeri di documenti completi, password, token di accesso, numeri di carte di credito o dati biometrici. Salvare solo riferimenti parziali (es. ultime 4 cifre del documento).
const auditLog = (action, details) => { const entry = { timestamp: new Date().toISOString(), userId: currentUser.id, action, details }; // Demo: localStorage / Produzione: API server const logs = JSON.parse(localStorage.getItem('pms_audit_log') || '[]'); logs.push(entry); localStorage.setItem('pms_audit_log', JSON.stringify(logs.slice(-1000))); };
Problemi comuni e soluzioni per lo sviluppo e il debug del PMS.
Causa: localStorage corrotto o versione incompatibile.
Soluzione:
// In console browser localStorage.removeItem('checkin_pms_data'); // Ricarica la pagina
Causa: Il propertyId delle prenotazioni non corrisponde a currentPropertyId.
Soluzione: Verificare che i filtri per proprietà siano allineati nel DataContext.
Causa: Browser non supporta HTML5 Drag and Drop.
Soluzione: Utilizzare un browser moderno (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+) oppure implementare un fallback touch.
// In pmsService.js const DEBUG = true; const log = (...args) => { if (DEBUG) console.log('[PMS]', ...args); };
Per ripristinare il PMS allo stato iniziale con dati demo freschi:
// In console browser localStorage.removeItem('checkin_pms_data'); localStorage.removeItem('checkin_pms_settings'); window.location.reload();
Clona il repository, avvia il dev server e inizia a contribuire al PMS di CheckIn Facile.