# Piano: Collaborazione Real-Time nel Report Designer ## Obiettivo Implementare la collaborazione in tempo reale nel Report Designer, simile a Google Docs o Excel Online. Quando un utente modifica un template, tutti gli altri utenti collegati allo stesso template vedono le modifiche istantaneamente. --- ## Architettura Proposta ### Concetti Chiave 1. **Room-based Collaboration**: Ogni template ha una "room" SignalR dedicata 2. **Operational Transformation Semplificata**: Invece di OT completo (complesso), usiamo un modello "last-write-wins" con sync frequente 3. **Presence Awareness**: Gli utenti vedono chi altro sta modificando il template 4. **Cursor/Selection Sharing**: Mostra quale elemento sta selezionando ogni utente ### Flusso Dati ``` User A modifica elemento ↓ Template locale aggiornato (come ora) ↓ SignalR invia delta alla room ↓ Backend riceve e ritrasmette a tutti nella room (eccetto mittente) ↓ User B/C/... ricevono delta ↓ Applicano delta al loro template locale ↓ Canvas si aggiorna automaticamente (già funziona così) ``` --- ## Implementazione Dettagliata ### FASE 1: Backend - ReportCollaborationHub **File:** `/src/Apollinare.API/Hubs/ReportCollaborationHub.cs` Nuovo Hub dedicato per la collaborazione sui report: ```csharp public class ReportCollaborationHub : Hub { // Stato in-memory degli utenti per template private static ConcurrentDictionary> _templateUsers = new(); // Join a template room public async Task JoinTemplate(int templateId, string userName, string userColor) // Leave template room public async Task LeaveTemplate(int templateId) // Broadcast element change to room public async Task ElementChanged(int templateId, ElementChangeDto change) // Broadcast element added public async Task ElementAdded(int templateId, AprtElement element) // Broadcast element deleted public async Task ElementDeleted(int templateId, string elementId) // Broadcast page changes public async Task PageChanged(int templateId, PageChangeDto change) // Broadcast selection change (which element user is editing) public async Task SelectionChanged(int templateId, string? elementId) // Broadcast cursor position (optional, for live cursors) public async Task CursorMoved(int templateId, float x, float y) // Request full template sync (when joining late or after reconnect) public async Task RequestSync(int templateId) // Send full template state (host responds to sync requests) public async Task SendSync(int templateId, string connectionId, AprtTemplate template) } ``` **DTOs:** ```csharp public record CollaboratorInfo(string ConnectionId, string UserName, string Color, string? SelectedElementId); public record ElementChangeDto( string ElementId, string ChangeType, // "position", "style", "content", "visibility", etc. object NewValue ); public record PageChangeDto( string PageId, string ChangeType, // "added", "deleted", "reordered", "renamed", "settings" object? Data ); ``` ### FASE 2: Frontend - Servizio Collaborazione **File:** `/frontend/src/services/reportCollaboration.ts` ```typescript class ReportCollaborationService { private connection: HubConnection | null = null; private templateId: number | null = null; private listeners: Map> = new Map(); // Connessione e gestione room async joinTemplate( templateId: number, userName: string, userColor: string, ): Promise; async leaveTemplate(): Promise; // Invio modifiche (chiamati dal ReportEditorPage) sendElementChange(elementId: string, changeType: string, newValue: any): void; sendElementAdded(element: AprtElement): void; sendElementDeleted(elementId: string): void; sendPageChange(pageId: string, changeType: string, data?: any): void; sendSelectionChange(elementId: string | null): void; // Sottoscrizione eventi (per ricevere modifiche da altri) onElementChanged(callback: (change: ElementChange) => void): () => void; onElementAdded(callback: (element: AprtElement) => void): () => void; onElementDeleted(callback: (elementId: string) => void): () => void; onPageChanged(callback: (change: PageChange) => void): () => void; onCollaboratorsChanged( callback: (collaborators: Collaborator[]) => void, ): () => void; onSelectionChanged( callback: (userId: string, elementId: string | null) => void, ): () => void; onSyncRequested(callback: (requesterId: string) => void): () => void; // Sync requestSync(): void; sendSync(connectionId: string, template: AprtTemplate): void; } ``` ### FASE 3: Frontend - Integrazione in ReportEditorPage **Modifiche a:** `/frontend/src/pages/ReportEditorPage.tsx` 1. **Nuovo State per collaborazione:** ```typescript const [collaborators, setCollaborators] = useState([]); const [remoteSelections, setRemoteSelections] = useState>( new Map(), ); const [isCollaborating, setIsCollaborating] = useState(false); ``` 2. **Join/Leave room al mount/unmount:** ```typescript useEffect(() => { if (!isNew && id) { const userName = getCurrentUserName(); // Da auth context const userColor = generateUserColor(userName); reportCollaborationService .joinTemplate(Number(id), userName, userColor) .then(() => setIsCollaborating(true)); return () => { reportCollaborationService.leaveTemplate(); }; } }, [id, isNew]); ``` 3. **Sottoscrizione eventi remoti:** ```typescript useEffect(() => { if (!isCollaborating) return; const unsubscribers = [ reportCollaborationService.onElementChanged((change) => { // Applica modifica senza creare history entry historyActions.setWithoutHistory((prev) => { // ... applica change.newValue a element con change.elementId }); }), reportCollaborationService.onElementAdded((element) => { historyActions.setWithoutHistory((prev) => ({ ...prev, elements: [...prev.elements, element], })); }), // ... altri handler reportCollaborationService.onCollaboratorsChanged(setCollaborators), ]; return () => unsubscribers.forEach((unsub) => unsub()); }, [isCollaborating]); ``` 4. **Invio modifiche locali:** Modificare `handleUpdateElement` per inviare anche via SignalR: ```typescript const handleUpdateElement = useCallback( (elementId: string, updates: Partial) => { // Aggiorna stato locale (come ora) historyActions.set((prev) => ({...})); // Invia a collaboratori if (isCollaborating) { reportCollaborationService.sendElementChange(elementId, "update", updates); } }, [historyActions, isCollaborating], ); ``` ### FASE 4: UI Collaborazione **File:** `/frontend/src/components/reportEditor/CollaboratorsBar.tsx` Barra che mostra gli utenti connessi: ```typescript interface CollaboratorsBarProps { collaborators: Collaborator[]; remoteSelections: Map; // userId -> elementId } // Mostra: // - Avatar circolari colorati per ogni collaboratore // - Tooltip con nome utente // - Indicatore "sta modificando [elemento]" // - Badge con conteggio totale collaboratori ``` **Modifiche a EditorCanvas:** Evidenziare elementi selezionati da altri utenti con bordo colorato. ### FASE 5: Gestione Conflitti Per semplicità, usiamo strategia **"last-write-wins"** con alcune ottimizzazioni: 1. **Elementi diversi**: Nessun conflitto, modifiche applicate indipendentemente 2. **Stesso elemento, proprietà diverse**: Merge delle proprietà 3. **Stesso elemento, stessa proprietà**: Ultima modifica vince 4. **Lock visivo**: Quando un utente seleziona un elemento, gli altri vedono un indicatore **Opzionale (Fase futura):** Lock pessimistico - solo un utente alla volta può modificare un elemento. --- ## File da Creare/Modificare ### Nuovi File | File | Descrizione | | ----------------------------------------------------------- | ----------------------------------- | | `src/Apollinare.API/Hubs/ReportCollaborationHub.cs` | Hub SignalR per collaborazione | | `src/Apollinare.API/Models/CollaborationDtos.cs` | DTOs per messaggi collaborazione | | `frontend/src/services/reportCollaboration.ts` | Client SignalR per collaborazione | | `frontend/src/components/reportEditor/CollaboratorsBar.tsx` | UI collaboratori connessi | | `frontend/src/types/collaboration.ts` | Types TypeScript per collaborazione | ### File da Modificare | File | Modifiche | | -------------------------------------------------------- | ------------------------------------------------- | | `src/Apollinare.API/Program.cs` | Registrare nuovo hub `/hubs/report-collaboration` | | `frontend/src/pages/ReportEditorPage.tsx` | Integrare collaborazione, stato collaboratori | | `frontend/src/components/reportEditor/EditorCanvas.tsx` | Mostrare selezioni remote | | `frontend/src/components/reportEditor/EditorToolbar.tsx` | Mostrare CollaboratorsBar | --- ## Stima Complessità | Fase | Complessità | Note | | ---------------------- | ----------- | ---------------------------------------- | | 1. Backend Hub | Media | SignalR groups, gestione stato in-memory | | 2. Frontend Service | Media | Gestione connessione, eventi | | 3. Integrazione Editor | Alta | Molti handler da modificare | | 4. UI Collaboratori | Bassa | Componente semplice | | 5. Gestione Conflitti | Media | Merge logic | --- ## Considerazioni Aggiuntive ### Performance - Throttling degli aggiornamenti durante drag (ogni 50-100ms invece di ogni frame) - Debounce per modifiche testo (300ms) - Batch di modifiche multiple in singolo messaggio ### Scalabilità - Per deployment multi-server: usare Redis backplane per SignalR - Considerare Azure SignalR Service per produzione ### Autenticazione - Aggiungere autenticazione al hub (verificare che utente abbia accesso al template) - Usare JWT token per identificare utente ### Edge Cases - Reconnessione dopo disconnessione: richiedere sync completo - Template eliminato mentre utenti connessi: notificare e chiudere - Conflitto salvataggio: merge o "force save" con conferma --- ## Ordine di Implementazione Consigliato 1. **Backend Hub base** - Join/Leave room, broadcast semplice 2. **Frontend service base** - Connessione, invio/ricezione messaggi 3. **Integrazione minima** - Solo sync modifiche elementi (no UI collaboratori) 4. **Test funzionale** - Verificare che modifiche si propaghino 5. **UI Collaboratori** - Mostrare chi è connesso 6. **Selezioni remote** - Evidenziare elementi selezionati da altri 7. **Ottimizzazioni** - Throttling, batching, gestione conflitti --- ## Domande per l'Utente Prima di procedere, confermare: 1. **Autenticazione**: Il sistema ha già autenticazione utenti? Devo usare un sistema mock per ora? 2. **Persistenza stato**: Le modifiche devono essere salvate automaticamente o solo quando l'utente clicca "Salva"? 3. **Lock elementi**: Vuoi che solo un utente alla volta possa modificare un elemento (lock pessimistico)? 4. **Cursori live**: Vuoi vedere il cursore degli altri utenti in tempo reale (come Google Docs)? 5. **Nome utente**: Da dove prendo il nome utente da mostrare? (localStorage, auth context, prompt?)