-
This commit is contained in:
750
PLAN.md
750
PLAN.md
@@ -1,425 +1,343 @@
|
||||
# Piano: Sistema di Report PDF con Editor Visuale
|
||||
# Piano: Collaborazione Real-Time nel Report Designer
|
||||
|
||||
## Obiettivo
|
||||
Creare un sistema completo di generazione report PDF con:
|
||||
- Editor grafico drag-and-drop (stile Canva)
|
||||
- Potenza di JasperReports (data binding, paginazione, formule)
|
||||
- Metalinguaggio esportabile/importabile (tipo LaTeX)
|
||||
- Salvataggio template riutilizzabili
|
||||
- Supporto immagini e font personalizzati
|
||||
|
||||
## Architettura Proposta
|
||||
|
||||
### 1. Metalinguaggio Template (APRT - Apollinare Report Template)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"meta": {
|
||||
"name": "Scheda Evento",
|
||||
"description": "Template per stampa evento",
|
||||
"author": "admin",
|
||||
"createdAt": "2025-01-15",
|
||||
"pageSize": "A4",
|
||||
"orientation": "portrait",
|
||||
"margins": { "top": 20, "right": 15, "bottom": 20, "left": 15 }
|
||||
},
|
||||
"resources": {
|
||||
"fonts": [
|
||||
{ "id": "font1", "name": "Roboto", "url": "/fonts/roboto.ttf" }
|
||||
],
|
||||
"images": [
|
||||
{ "id": "logo", "name": "Logo Aziendale", "url": "/images/logo.png" }
|
||||
]
|
||||
},
|
||||
"dataSources": {
|
||||
"evento": { "type": "object", "schema": "Evento" },
|
||||
"ospiti": { "type": "array", "schema": "EventoDettaglioOspiti" },
|
||||
"costi": { "type": "array", "schema": "EventoAltroCosto" }
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"type": "header",
|
||||
"height": 80,
|
||||
"repeatOnPages": true,
|
||||
"elements": [...]
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"elements": [...]
|
||||
},
|
||||
{
|
||||
"type": "detail",
|
||||
"dataSource": "ospiti",
|
||||
"elements": [...]
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"height": 40,
|
||||
"repeatOnPages": true,
|
||||
"elements": [...]
|
||||
}
|
||||
],
|
||||
"elements": [
|
||||
{
|
||||
"id": "elem1",
|
||||
"type": "text",
|
||||
"position": { "x": 10, "y": 10, "width": 200, "height": 30 },
|
||||
"style": {
|
||||
"fontFamily": "font1",
|
||||
"fontSize": 24,
|
||||
"fontWeight": "bold",
|
||||
"color": "#333333",
|
||||
"textAlign": "left"
|
||||
},
|
||||
"content": {
|
||||
"type": "static",
|
||||
"value": "SCHEDA EVENTO"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "elem2",
|
||||
"type": "text",
|
||||
"position": { "x": 10, "y": 50, "width": 150, "height": 20 },
|
||||
"content": {
|
||||
"type": "binding",
|
||||
"expression": "{{evento.codice}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "elem3",
|
||||
"type": "image",
|
||||
"position": { "x": 450, "y": 10, "width": 100, "height": 60 },
|
||||
"content": {
|
||||
"type": "resource",
|
||||
"resourceId": "logo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "elem4",
|
||||
"type": "table",
|
||||
"position": { "x": 10, "y": 200, "width": 550, "height": "auto" },
|
||||
"dataSource": "ospiti",
|
||||
"columns": [
|
||||
{ "field": "tipoOspite.descrizione", "header": "Tipo", "width": 150 },
|
||||
{ "field": "numero", "header": "Quantità", "width": 100 },
|
||||
{ "field": "costoUnitario", "header": "Costo Unit.", "width": 100, "format": "currency" },
|
||||
{ "field": "costoTotale", "header": "Totale", "width": 100, "format": "currency" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "elem5",
|
||||
"type": "shape",
|
||||
"position": { "x": 10, "y": 180, "width": 550, "height": 2 },
|
||||
"style": {
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pageNum",
|
||||
"type": "text",
|
||||
"section": "footer",
|
||||
"position": { "x": 250, "y": 10, "width": 100, "height": 20 },
|
||||
"content": {
|
||||
"type": "expression",
|
||||
"value": "Pagina {{$pageNumber}} di {{$totalPages}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Struttura Backend
|
||||
|
||||
#### Nuove Entità
|
||||
```
|
||||
ReportTemplate
|
||||
├── Id
|
||||
├── Nome
|
||||
├── Descrizione
|
||||
├── Categoria (Evento, Cliente, Articoli, etc.)
|
||||
├── TemplateJson (il metalinguaggio APRT)
|
||||
├── Thumbnail (preview del template)
|
||||
├── Attivo
|
||||
├── CreatedAt/By, UpdatedAt/By
|
||||
|
||||
ReportFont
|
||||
├── Id
|
||||
├── Nome
|
||||
├── FontFamily
|
||||
├── FontData (BLOB - file TTF/OTF)
|
||||
├── MimeType
|
||||
|
||||
ReportImage
|
||||
├── Id
|
||||
├── Nome
|
||||
├── Categoria
|
||||
├── ImageData (BLOB)
|
||||
├── MimeType
|
||||
├── Width, Height
|
||||
```
|
||||
|
||||
#### Nuovi Controller
|
||||
```
|
||||
ReportTemplatesController
|
||||
├── GET /api/report-templates # Lista template
|
||||
├── GET /api/report-templates/{id} # Dettaglio
|
||||
├── POST /api/report-templates # Crea
|
||||
├── PUT /api/report-templates/{id} # Aggiorna
|
||||
├── DELETE /api/report-templates/{id} # Elimina
|
||||
├── POST /api/report-templates/{id}/clone # Duplica
|
||||
├── GET /api/report-templates/{id}/export # Esporta .aprt
|
||||
├── POST /api/report-templates/import # Importa .aprt
|
||||
|
||||
ReportResourcesController
|
||||
├── GET /api/report-resources/fonts # Lista font
|
||||
├── POST /api/report-resources/fonts # Upload font
|
||||
├── DELETE /api/report-resources/fonts/{id}
|
||||
├── GET /api/report-resources/images # Lista immagini
|
||||
├── POST /api/report-resources/images # Upload immagine
|
||||
├── DELETE /api/report-resources/images/{id}
|
||||
|
||||
ReportGeneratorController
|
||||
├── POST /api/reports/generate # Genera PDF
|
||||
│ Body: { templateId, dataContext: { eventoId, ... } }
|
||||
├── POST /api/reports/preview # Anteprima (PNG/HTML)
|
||||
```
|
||||
|
||||
#### Servizio Generazione PDF
|
||||
Useremo **QuestPDF** per la generazione:
|
||||
- Supporto nativo .NET
|
||||
- API fluent per layout complessi
|
||||
- Font personalizzati
|
||||
- Immagini
|
||||
- Paginazione automatica
|
||||
- Performance eccellenti
|
||||
|
||||
```csharp
|
||||
public class ReportGeneratorService
|
||||
{
|
||||
public byte[] GeneratePdf(ReportTemplate template, object dataContext)
|
||||
{
|
||||
var parsed = ParseTemplate(template.TemplateJson);
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(parsed.PageSize);
|
||||
page.Margin(parsed.Margins);
|
||||
|
||||
if (parsed.Header != null)
|
||||
page.Header().Element(c => RenderSection(c, parsed.Header, dataContext));
|
||||
|
||||
page.Content().Element(c => RenderContent(c, parsed, dataContext));
|
||||
|
||||
if (parsed.Footer != null)
|
||||
page.Footer().Element(c => RenderSection(c, parsed.Footer, dataContext));
|
||||
});
|
||||
});
|
||||
|
||||
return document.GeneratePdf();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Frontend - Editor Visuale
|
||||
|
||||
#### Componenti Principali
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/
|
||||
│ ├── ReportEditorPage.tsx # Editor principale
|
||||
│ └── ReportTemplatesPage.tsx # Lista template
|
||||
├── components/
|
||||
│ └── reportEditor/
|
||||
│ ├── ReportEditor.tsx # Container principale
|
||||
│ ├── Canvas.tsx # Area di disegno (Fabric.js o Konva)
|
||||
│ ├── Toolbar.tsx # Barra strumenti (text, image, shape, table)
|
||||
│ ├── PropertiesPanel.tsx # Pannello proprietà elemento selezionato
|
||||
│ ├── DataBindingPanel.tsx # Pannello per mappare dati
|
||||
│ ├── LayersPanel.tsx # Gestione livelli/elementi
|
||||
│ ├── ResourcesPanel.tsx # Font e immagini disponibili
|
||||
│ ├── PageSettings.tsx # Impostazioni pagina
|
||||
│ ├── PreviewModal.tsx # Anteprima PDF
|
||||
│ └── elements/
|
||||
│ ├── TextElement.tsx
|
||||
│ ├── ImageElement.tsx
|
||||
│ ├── ShapeElement.tsx
|
||||
│ ├── TableElement.tsx
|
||||
│ └── BarcodeElement.tsx
|
||||
├── services/
|
||||
│ └── reportService.ts
|
||||
└── types/
|
||||
└── report.ts # Tipi TypeScript per APRT
|
||||
```
|
||||
|
||||
#### Libreria Canvas
|
||||
**Fabric.js** è la scelta migliore:
|
||||
- Drag & drop nativo
|
||||
- Selezione multipla
|
||||
- Ridimensionamento con handle
|
||||
- Rotazione elementi
|
||||
- Serializzazione JSON
|
||||
- Supporto testo, immagini, forme
|
||||
- Griglia e snap
|
||||
- Undo/redo
|
||||
|
||||
#### Flusso Editor
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Toolbar: [Text] [Image] [Shape] [Table] [Line] [Barcode] │
|
||||
├─────────────┬───────────────────────────────────┬───────────────┤
|
||||
│ │ │ │
|
||||
│ Layers │ CANVAS │ Properties │
|
||||
│ Panel │ ┌─────────────────┐ │ Panel │
|
||||
│ │ │ HEADER │ │ │
|
||||
│ □ Logo │ │ [Logo] [Titolo]│ │ Position │
|
||||
│ □ Titolo │ ├─────────────────┤ │ x: 10 y: 10 │
|
||||
│ □ Data │ │ │ │ w: 200 h: 30 │
|
||||
│ □ Tabella │ │ BODY │ │ │
|
||||
│ □ Footer │ │ │ │ Style │
|
||||
│ │ │ [Data Evento] │ │ Font: Roboto │
|
||||
│ │ │ [Cliente] │ │ Size: 24 │
|
||||
│ │ │ [Tabella] │ │ Color: #333 │
|
||||
│ │ │ │ │ │
|
||||
│ │ ├─────────────────┤ │ Data Binding │
|
||||
│ │ │ FOOTER │ │ {{evento. │
|
||||
│ │ │ [Pag X di Y] │ │ codice}} │
|
||||
│ │ └─────────────────┘ │ │
|
||||
│ │ │ │
|
||||
├─────────────┴───────────────────────────────────┴───────────────┤
|
||||
│ Data Sources: [evento] [ospiti] [costi] [risorse] │
|
||||
│ Available Fields: codice, dataEvento, cliente.ragioneSociale...│
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4. Implementazione Step-by-Step
|
||||
|
||||
#### Fase 1: Backend Foundation
|
||||
1. Creare entità `ReportTemplate`, `ReportFont`, `ReportImage`
|
||||
2. Aggiornare DbContext e migrare database
|
||||
3. Creare `ReportTemplatesController` con CRUD base
|
||||
4. Creare `ReportResourcesController` per upload font/immagini
|
||||
5. Installare e configurare QuestPDF
|
||||
6. Creare `ReportGeneratorService` base
|
||||
|
||||
#### Fase 2: Metalinguaggio Parser
|
||||
1. Definire classi C# per il metalinguaggio APRT
|
||||
2. Implementare parser JSON → oggetti
|
||||
3. Implementare renderer elementi → QuestPDF
|
||||
4. Gestire binding dati con espressioni {{campo}}
|
||||
5. Implementare paginazione e sezioni ripetute
|
||||
|
||||
#### Fase 3: Frontend Editor Base
|
||||
1. Installare Fabric.js (`fabric`)
|
||||
2. Creare pagina `ReportEditorPage`
|
||||
3. Implementare `Canvas` con Fabric.js
|
||||
4. Implementare `Toolbar` per aggiungere elementi
|
||||
5. Implementare `PropertiesPanel` per editing proprietà
|
||||
6. Implementare serializzazione canvas → APRT
|
||||
|
||||
#### Fase 4: Data Binding
|
||||
1. Creare `DataBindingPanel` con schema dati disponibili
|
||||
2. Implementare drag-drop campi su elementi
|
||||
3. Supportare espressioni {{campo.sottocampo}}
|
||||
4. Implementare formattazione (currency, date, number)
|
||||
5. Supportare espressioni condizionali
|
||||
|
||||
#### Fase 5: Tabelle e Repeater
|
||||
1. Implementare `TableElement` con colonne configurabili
|
||||
2. Supportare data source array per righe ripetute
|
||||
3. Implementare auto-height per tabelle
|
||||
4. Gestire page break automatici
|
||||
|
||||
#### Fase 6: Risorse e Upload
|
||||
1. Implementare upload font custom
|
||||
2. Implementare upload immagini
|
||||
3. Creare libreria risorse condivise
|
||||
4. Preview font e immagini
|
||||
|
||||
#### Fase 7: Preview e Generazione
|
||||
1. Implementare preview real-time (canvas → PNG)
|
||||
2. Implementare generazione PDF finale
|
||||
3. Download PDF
|
||||
4. Stampa diretta
|
||||
|
||||
#### Fase 8: Import/Export
|
||||
1. Implementare export .aprt (JSON + risorse embedded base64)
|
||||
2. Implementare import .aprt
|
||||
3. Validazione template importati
|
||||
|
||||
### 5. Template Esempio: Scheda Evento
|
||||
|
||||
Creeremo un template predefinito per la stampa eventi con:
|
||||
- Header con logo aziendale e titolo
|
||||
- Dati evento (codice, data, cliente, location)
|
||||
- Tabella ospiti con subtotali
|
||||
- Tabella costi aggiuntivi
|
||||
- Riepilogo totali
|
||||
- Note
|
||||
- Footer con paginazione
|
||||
|
||||
### 6. Dipendenze da Aggiungere
|
||||
|
||||
**Backend (NuGet):**
|
||||
```xml
|
||||
<PackageReference Include="QuestPDF" Version="2024.12.0" />
|
||||
```
|
||||
|
||||
**Frontend (npm):**
|
||||
```json
|
||||
{
|
||||
"fabric": "^6.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"@types/fabric": "^5.3.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Routes Frontend
|
||||
|
||||
```typescript
|
||||
// App.tsx - nuove routes
|
||||
<Route path="/report-templates" element={<ReportTemplatesPage />} />
|
||||
<Route path="/report-editor/:id?" element={<ReportEditorPage />} />
|
||||
<Route path="/report-preview/:templateId/:entityId" element={<ReportPreviewPage />} />
|
||||
```
|
||||
|
||||
### 8. Stima Componenti
|
||||
|
||||
| Componente | File | Complessità |
|
||||
|------------|------|-------------|
|
||||
| Entità + DbContext | 3 file | Bassa |
|
||||
| Controllers | 3 file | Media |
|
||||
| ReportGeneratorService | 1 file | Alta |
|
||||
| APRT Parser | 1 file | Media |
|
||||
| ReportEditorPage | 1 file | Alta |
|
||||
| Canvas (Fabric.js) | 1 file | Alta |
|
||||
| Toolbar | 1 file | Bassa |
|
||||
| PropertiesPanel | 1 file | Media |
|
||||
| DataBindingPanel | 1 file | Media |
|
||||
| LayersPanel | 1 file | Bassa |
|
||||
| Elementi (5 tipi) | 5 file | Media |
|
||||
| Services frontend | 1 file | Bassa |
|
||||
| Types | 1 file | Bassa |
|
||||
|
||||
**Totale: ~20 file, complessità alta**
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Decisioni Architetturali
|
||||
## Architettura Proposta
|
||||
|
||||
1. **QuestPDF** invece di iTextSharp (licenza più permissiva, API moderna)
|
||||
2. **Fabric.js** invece di Konva (più features per editing)
|
||||
3. **JSON** come metalinguaggio (leggibile, facile da parsare)
|
||||
4. **Embedded resources** negli export (portabilità completa)
|
||||
5. **Real-time preview** via canvas (no round-trip server)
|
||||
### Concetti Chiave
|
||||
|
||||
## Note Implementative
|
||||
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
|
||||
|
||||
- Il canvas Fabric.js lavora in pixel, convertiremo in mm per la stampa
|
||||
- I font custom vanno registrati in QuestPDF all'avvio
|
||||
- Le immagini BLOB vanno convertite in base64 per Fabric.js
|
||||
- La paginazione è gestita lato server da QuestPDF
|
||||
- L'editor salva solo il JSON, la generazione PDF è on-demand
|
||||
### 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<string, HashSet<CollaboratorInfo>> _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<string, Set<Function>> = new Map();
|
||||
|
||||
// Connessione e gestione room
|
||||
async joinTemplate(
|
||||
templateId: number,
|
||||
userName: string,
|
||||
userColor: string,
|
||||
): Promise<void>;
|
||||
async leaveTemplate(): Promise<void>;
|
||||
|
||||
// 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<Collaborator[]>([]);
|
||||
const [remoteSelections, setRemoteSelections] = useState<Map<string, string>>(
|
||||
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<AprtElement>) => {
|
||||
// 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<string, string>; // 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?)
|
||||
|
||||
Reference in New Issue
Block a user