diff --git a/CLAUDE.md b/CLAUDE.md index a1d43e4..e417976 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,12 +46,56 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve ## Quick Start - Session Recovery -**Ultima sessione:** 28 Novembre 2025 (sera) +**Ultima sessione:** 28 Novembre 2025 (tarda notte) **Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso **Lavoro completato nell'ultima sessione:** +- **NUOVA FEATURE: Toolbar Report Designer Migliorata Drasticamente** - COMPLETATO + - Design moderno stile Canva/Figma con gradient buttons e animazioni fluide + - **Sezioni etichettate** su desktop (INSERISCI, MODIFICA, CRONOLOGIA, VISTA, ZOOM, PAGINA) + - **Toolbar contestuale** dinamica che appare quando un elemento è selezionato: + - Per testo: formattazione (grassetto, corsivo, sottolineato), allineamento, color picker + - Per forme/linee: color picker riempimento/bordo, spessore bordo + - Per immagini: indicatore mantieni proporzioni + - Mostra tipo elemento, nome e dimensioni in tempo reale + - **Color Picker integrato** con palette di 20 colori predefiniti + - **Indicatore stato salvataggio** visivo: Salvato (verde), Non salvato (arancione), Salvataggio... (spinner) + - **Badge** sul pulsante Snap che mostra quante opzioni sono attive + - **Zoom esteso** fino al 300% con pulsante "Adatta alla finestra" + - Pannello scorciatoie tastiera ampliato + - Aggiunto `textDecoration` a `AprtStyle` per supportare sottolineato + +- **FIX: Indicatore "Non Salvato" errato** - RISOLTO + - Prima usava `canUndo` che indicava solo presenza di history + - Ora usa `undoCount` confrontato con `lastSavedUndoCount` per tracking accurato + +- **NUOVA FEATURE: Auto-Save con Toggle** - COMPLETATO + - Salvataggio automatico dopo 1 secondo di inattività (debounce) + - **Abilitato di default** + - Toggle nella toolbar (icona AutoMode) per attivare/disattivare + - Quando auto-save è attivo, il pulsante "Salva" manuale è nascosto + - Quando auto-save è disattivo, il pulsante "Salva" appare + - Non si attiva per template nuovi (richiede primo salvataggio manuale) + - Non si attiva durante salvataggio in corso + +**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 notte):** + +- **NUOVA FEATURE: Responsive Design Completo** - COMPLETATO + - Tutta l'applicazione è ora responsive per mobile, tablet e desktop + - Breakpoints: mobile (<600px), tablet (600-900px), desktop (>900px) + - **Layout.tsx**: Sidebar collassata con icone su tablet, drawer mobile + - **ReportTemplatesPage**: Header stackato su mobile, FAB per nuovo template, dialog fullScreen + - **ReportEditorPage**: BottomNavigation + SwipeableDrawer (70vh) per pannelli su mobile, auto-zoom + - **EditorToolbar**: 3 varianti (mobile compatta con riga collassabile, tablet media, desktop completa) + - **Pannelli laterali** (DataBindingPanel, PropertiesPanel, PageNavigator): larghezza piena su mobile + - **DatasetSelector**: Header collassabile su mobile + - **PreviewDialog**: fullScreen su mobile con navigazione step-by-step (dataset → entity) + - **ImageUploadDialog**: fullScreen su mobile, area drag-drop ottimizzata, bottoni stacked + +**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 sera):** + - **FIX: Variabili Globali Report ({{$pageNumber}}, {{$totalPages}}, ecc.)** - RISOLTO - Le variabili speciali ora vengono correttamente risolte nel PDF finale - Aggiunta classe `PageContext` per passare numero pagina e totale pagine durante il rendering @@ -625,9 +669,9 @@ Formato JSON esportabile/importabile per portabilità template: - [x] Editor visuale drag-and-drop con Fabric.js - [x] Supporto elementi: testo, forme, linee, tabelle, immagini (placeholder) -- [x] Gestione zoom (25% - 200%) +- [x] Gestione zoom (25% - 300%) - [x] Griglia e snap to grid -- [x] Undo/Redo (max 20 stati) +- [x] Undo/Redo (max 100 stati) - [x] Shortcuts tastiera (Ctrl+Z, Ctrl+Y, Ctrl+S, Delete) - [x] Pannello proprietà con posizione, stile, contenuto - [x] Data binding con browser campi disponibili @@ -638,6 +682,12 @@ Formato JSON esportabile/importabile per portabilità template: - [x] Clone template - [x] Generazione PDF default per eventi - [x] Formattazione campi (valuta, data, numero, percentuale) +- [x] **Responsive design completo** (mobile, tablet, desktop) +- [x] **Toolbar professionale** stile Canva/Figma con sezioni etichettate +- [x] **Toolbar contestuale** per formattazione rapida (testo, forme, immagini) +- [x] **Color picker integrato** con palette preset +- [x] **Auto-save** con toggle (abilitato di default, 1s debounce) +- [x] **Indicatore stato salvataggio** accurato (Salvato/Non salvato/Salvataggio...) ### Cosa Manca per Completare @@ -700,6 +750,7 @@ Formato JSON esportabile/importabile per portabilità template: - [x] Keyboard shortcuts - [x] PageNavigator (gestione multi-pagina) - [x] Navigazione pagine in toolbar +- [x] **Responsive design** (mobile/tablet/desktop) - [ ] Upload e gestione immagini nell'editor - [ ] Editor tabelle avanzato (colonne, binding dati) - [ ] UI relazioni tra dataset @@ -943,6 +994,62 @@ frontend/src/ ``` - **File:** `ReportGeneratorService.cs` - Metodi `GeneratePdfAsync()`, `RenderContentToBitmap()`, `RenderElementToCanvas()`, `RenderTextToCanvas()`, `ResolveContent()`, `ResolveBindingWithFormat()`, `ResolveBinding()`, `ResolveExpression()`, `ResolveBindingPath()` +16. **Responsive Design Completo (IMPLEMENTATO 28/11/2025 notte):** + - **Obiettivo:** Rendere tutta l'applicazione responsive per mobile, tablet e desktop + - **Breakpoints MUI utilizzati:** + - Mobile: `theme.breakpoints.down("sm")` → < 600px + - Tablet: `theme.breakpoints.between("sm", "md")` → 600-900px + - Desktop: `theme.breakpoints.up("md")` → > 900px + - **Pattern principale per Report Editor su mobile:** + - `BottomNavigation` per switch tra pannelli (Pagine, Dati, Proprietà) + - `SwipeableDrawer` con `anchor="bottom"` e altezza 70vh per contenuto pannelli + - Auto-zoom canvas: 0.5 mobile, 0.75 tablet, 1 desktop + - **Pattern per toolbar mobile:** + - Riga primaria con azioni essenziali sempre visibili + - Riga secondaria collassabile con `` per azioni secondarie + - **Pattern per dialog mobile:** + - `fullScreen` prop su Dialog + - AppBar con pulsante back invece di DialogTitle + - Navigazione step-by-step invece di layout side-by-side + - **File modificati:** + - `Layout.tsx` - Sidebar collassata su tablet + - `ReportTemplatesPage.tsx` - FAB, fullScreen dialogs + - `ReportEditorPage.tsx` - BottomNavigation + SwipeableDrawer + - `EditorToolbar.tsx` - 3 varianti layout (mobile/tablet/desktop) + - `DataBindingPanel.tsx`, `PropertiesPanel.tsx`, `PageNavigator.tsx` - Width responsive + - `DatasetSelector.tsx` - Header collapsible + - `PreviewDialog.tsx`, `ImageUploadDialog.tsx` - fullScreen + step navigation + +17. **Indicatore "Non Salvato" Errato (FIX 28/11/2025 tarda notte):** + - **Problema:** Dopo il salvataggio, l'indicatore continuava a mostrare "Non salvato" + - **Causa:** `hasUnsavedChanges` era basato su `templateHistory.canUndo` che indica solo se c'è history disponibile, non se ci sono modifiche non salvate + - **Soluzione:** Introdotto `lastSavedUndoCount` che viene aggiornato dopo ogni salvataggio riuscito. `hasUnsavedChanges` ora confronta `templateHistory.undoCount !== lastSavedUndoCount` + - **File:** `ReportEditorPage.tsx` + +18. **Auto-Save Feature (IMPLEMENTATO 28/11/2025 tarda notte):** + - **Funzionalità:** Salvataggio automatico dopo 1 secondo di inattività + - **Implementazione:** + - Stato `autoSaveEnabled` (default: true) in `ReportEditorPage.tsx` + - `useEffect` con debounce di 1000ms che triggera `saveMutation.mutate()` + - Non si attiva se: `isNew`, `!hasUnsavedChanges`, `saveMutation.isPending` + - Toggle nella toolbar con icona `AutoSaveIcon` (da @mui/icons-material) + - Pulsante "Salva" nascosto quando auto-save è attivo + - **Props toolbar:** `autoSaveEnabled`, `onAutoSaveToggle` + - **File:** `ReportEditorPage.tsx`, `EditorToolbar.tsx` + +19. **Toolbar Migliorata Stile Canva/Figma (IMPLEMENTATO 28/11/2025 tarda notte):** + - **Miglioramenti:** + - Design moderno con gradient buttons e animazioni fluide + - Sezioni etichettate su desktop (INSERISCI, MODIFICA, CRONOLOGIA, VISTA, ZOOM, PAGINA) + - Toolbar contestuale dinamica basata su tipo elemento selezionato + - Color picker integrato con 20 colori preset + - Indicatore stato salvataggio visivo + - Badge su pulsante Snap + - Zoom esteso fino a 300% + - **Componenti aggiunti:** `ToolbarSection`, `StyledIconButton`, `ColorPickerButton` + - **Type aggiunto:** `textDecoration` in `AprtStyle` + - **File:** `EditorToolbar.tsx`, `types/report.ts` + ### Schema Database Report System Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`): diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 0e3b353..9638816 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -13,6 +13,8 @@ import { ListItemButton, ListItemIcon, ListItemText, + useMediaQuery, + useTheme, } from "@mui/material"; import { Menu as MenuIcon, @@ -24,9 +26,11 @@ import { Person as PersonIcon, CalendarMonth as CalendarIcon, Print as PrintIcon, + Close as CloseIcon, } from "@mui/icons-material"; -const drawerWidth = 240; +const DRAWER_WIDTH = 240; +const DRAWER_WIDTH_COLLAPSED = 64; const menuItems = [ { text: "Dashboard", icon: , path: "/" }, @@ -43,53 +47,97 @@ export default function Layout() { const [mobileOpen, setMobileOpen] = useState(false); const navigate = useNavigate(); const location = useLocation(); + const theme = useTheme(); + + // Breakpoints + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px + const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md")); // 600-900px + + // Drawer width based on screen size + const drawerWidth = isTablet ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH; const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); }; const drawer = ( -
- - - Apollinare - + + + {!isTablet && ( + + Apollinare + + )} + {isMobile && ( + + + + )} - + {menuItems.map((item) => ( - + { navigate(item.path); - setMobileOpen(false); + if (isMobile) setMobileOpen(false); + }} + sx={{ + borderRadius: 1, + minHeight: 48, + justifyContent: isTablet ? "center" : "flex-start", + px: isTablet ? 1 : 2, }} > - {item.icon} - + + {item.icon} + + {!isTablet && } ))} -
+ {!isTablet && ( + + + © 2025 Apollinare + + + )} + ); return ( - + - + - - Catering & Banqueting Management + + {isMobile ? "Apollinare" : "Catering & Banqueting Management"} + + {/* Mobile Drawer */} {drawer} + + {/* Desktop/Tablet Drawer */} + + {/* Main Content */} - + {/* Toolbar spacer */} + + + {/* Content */} + + + ); diff --git a/frontend/src/components/reportEditor/DataBindingPanel.tsx b/frontend/src/components/reportEditor/DataBindingPanel.tsx index 385a770..cc9413b 100644 --- a/frontend/src/components/reportEditor/DataBindingPanel.tsx +++ b/frontend/src/components/reportEditor/DataBindingPanel.tsx @@ -15,6 +15,8 @@ import { Tooltip, Divider, alpha, + useMediaQuery, + useTheme, } from "@mui/material"; import { ExpandLess, @@ -59,6 +61,9 @@ export default function DataBindingPanel({ onInsertBinding, onRemoveDataset, }: DataBindingPanelProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [expanded, setExpanded] = useState(["special"]); const [search, setSearch] = useState(""); const [copiedField, setCopiedField] = useState(null); @@ -200,12 +205,16 @@ export default function DataBindingPanel({ return count; }, [search, schemas]); + // Panel width based on context (full width in mobile drawer) + const panelWidth = isMobile ? "100%" : 300; + if (selectedDatasets.length === 0) { return ( @@ -231,24 +241,28 @@ export default function DataBindingPanel({ return ( {/* Header con ricerca */} - - Campi Disponibili - + {!isMobile && ( + + Campi Disponibili + + )} - Clicca su un campo per inserirlo nell'elemento selezionato, oppure usa - l'icona copia per copiare il binding negli appunti. + {isMobile + ? "Tocca un campo per inserirlo" + : "Clicca su un campo per inserirlo nell'elemento selezionato, oppure usa l'icona copia per copiare il binding negli appunti."} diff --git a/frontend/src/components/reportEditor/DatasetSelector.tsx b/frontend/src/components/reportEditor/DatasetSelector.tsx index fc57938..52a591d 100644 --- a/frontend/src/components/reportEditor/DatasetSelector.tsx +++ b/frontend/src/components/reportEditor/DatasetSelector.tsx @@ -13,6 +13,9 @@ import { ListSubheader, Badge, Button, + useMediaQuery, + useTheme, + Collapse, } from "@mui/material"; import { Add as AddIcon, @@ -30,6 +33,8 @@ import { Info as InfoIcon, Settings as SettingsIcon, Dataset as DatasetIcon, + ExpandMore as ExpandMoreIcon, + ExpandLess as ExpandLessIcon, } from "@mui/icons-material"; import type { DatasetTypeDto } from "../../types/report"; @@ -48,10 +53,15 @@ export default function DatasetSelector({ onRemoveDataset, onOpenDatasetManager, }: DatasetSelectorProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md")); + const [anchorEl, setAnchorEl] = useState(null); const [hoveredDataset, setHoveredDataset] = useState( null, ); + const [expanded, setExpanded] = useState(!isMobile); const handleOpenMenu = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -152,18 +162,169 @@ export default function DatasetSelector({ return [...baseCategories, ...additionalCategories]; }, [groupedDatasets]); + // Compact mobile view + if (isMobile) { + return ( + + {/* Header - always visible */} + setExpanded(!expanded)} + > + + + + Dataset ({selectedDatasets.length}) + + + + {expanded ? : } + + + + {/* Expandable content */} + + + {selectedDatasets.map((dataset) => ( + onRemoveDataset(dataset.id)} + sx={{ fontWeight: 500 }} + /> + ))} + + {selectedDatasets.length === 0 && ( + + Nessun dataset selezionato + + )} + + {unselectedDatasets.length > 0 && ( + + + + )} + + + + {/* Menu - shared */} + + + + Seleziona Dataset + + + + + {categoryOrder.map((category) => { + const datasets = groupedDatasets[category]; + if (!datasets || datasets.length === 0) return null; + + return ( + + + {category} + + {datasets.map((dataset) => ( + handleSelectDataset(dataset)} + sx={{ py: 1 }} + > + + {getDatasetIcon(dataset.icon)} + + + + ))} + + ); + })} + + {onOpenDatasetManager && [ + , + { + handleCloseMenu(); + onOpenDatasetManager(); + }} + > + + + + + , + ]} + + + ); + } + + // Tablet/Desktop view return ( - - - setAddMenuAnchor(null)} - anchorOrigin={{ vertical: "bottom", horizontal: "left" }} - transformOrigin={{ vertical: "top", horizontal: "left" }} - TransitionComponent={Fade} - slotProps={{ - paper: { - sx: { mt: 0.5, borderRadius: 2, minWidth: 200 }, - }, - }} - > - - {ELEMENT_TYPES.map(({ type, icon: Icon, label, shortcut, color }) => ( - handleAddElement(type)} - sx={{ - py: 1, - "&:hover": { - bgcolor: `${color}10`, - }, - }} + {/* Text formatting */} + + handleTextFormat("bold")} > - - - - - - - ))} - - + + + handleTextFormat("italic")} + > + + + handleTextFormat("underline")} + > + + + - + - {/* Quick Add Buttons */} - - {ELEMENT_TYPES.map(({ type, icon: Icon, label, color }) => ( - - onAddElement(type)} + {/* Text alignment */} + value && handleTextAlign(value)} + sx={{ "& .MuiToggleButton-root": { border: 0, borderRadius: 1 } }} + > + + + + + + + + + + + + + + {/* Colors */} + + onUpdateSelectedElement({ style: { ...currentStyle, color } }) + } + label="Colore testo" + icon={TextColorIcon} + /> + + onUpdateSelectedElement({ + style: { ...currentStyle, backgroundColor: color }, + }) + } + label="Sfondo" + icon={FillColorIcon} + /> + + + + {/* Data binding indicator */} + {selectedElement.content?.type === "binding" && ( + } + label="Data Bound" size="small" + color="info" + variant="outlined" + sx={{ height: 24 }} + /> + )} + + ); + } + + // Shape/Line toolbar + if (selectedElement.type === "shape" || selectedElement.type === "line") { + return ( + + + onUpdateSelectedElement({ + style: { ...currentStyle, backgroundColor: color }, + }) + } + label="Riempimento" + icon={FillColorIcon} + /> + + onUpdateSelectedElement({ + style: { ...currentStyle, borderColor: color }, + }) + } + label="Bordo" + icon={StrokeColorIcon} + /> + + + + {/* Border width */} + + + + Bordo: + + + onUpdateSelectedElement({ + style: { + ...currentStyle, + borderWidth: Number(e.target.value), + }, + }) + } + inputProps={{ min: 0, max: 20, step: 0.5 }} + sx={{ width: 60, "& input": { py: 0.5, fontSize: "0.75rem" } }} + /> + + + + ); + } + + // Image toolbar + if (selectedElement.type === "image") { + return ( + + } + label={selectedElement.name || "Immagine"} + size="small" + variant="outlined" + sx={{ height: 24, maxWidth: 150 }} + /> + + + + + + + + ); + } + + return null; + }; + + // Save status indicator + const renderSaveStatus = () => { + return ( + + {/* Auto-save toggle */} + {onAutoSaveToggle && ( + + onAutoSaveToggle(!autoSaveEnabled)} sx={{ borderRadius: 1.5, + bgcolor: autoSaveEnabled + ? alpha(theme.palette.success.main, 0.1) + : alpha(theme.palette.grey[500], 0.1), + color: autoSaveEnabled ? "success.main" : "text.disabled", "&:hover": { - bgcolor: `${color}15`, - color: color, + bgcolor: autoSaveEnabled + ? alpha(theme.palette.success.main, 0.2) + : alpha(theme.palette.grey[500], 0.2), }, }} > - + - ))} + )} + + {/* Save status */} + {isSaving ? ( + + + + + Salvo... + + + + ) : hasUnsavedChanges ? ( + + + + + Non salvato + + + + ) : ( + + + + + {formatTimeAgo(lastSavedAt) || "Salvato"} + + + + )} + ); + }; - + // Mobile: Compact toolbar with essential actions + if (isMobile) { + return ( + + {/* Primary Row */} + + {/* Add Element */} + - {/* Selection Actions */} - - - - + + {/* Undo/Redo */} + + + + + + + + + + {/* Delete */} + + + + + + + {/* Page Navigation - Compact */} + + + + + {/* Save/Preview */} + + + + {!autoSaveEnabled && ( + - - - - - - - + ) : ( + + )} + + )} + + {/* Expand/Collapse */} + setShowSecondRow(!showSecondRow)} + > + {showSecondRow ? : } + + + + {/* Secondary Row - Collapsible */} + + + {/* Zoom */} + onZoomChange(Math.max(0.25, zoom - 0.25))} + > + + + + + + + {/* Quick Add Buttons */} + {ELEMENT_TYPES.slice(0, 3).map( + ({ type, icon: Icon, label, color }) => ( + onAddElement(type)} + color={color} + > + + + ), + )} + + + + {/* Selection Actions */} + + + + + + + + {isLocked ? ( + + ) : ( + + )} + + + + + {/* Undo/Redo */} + + + + + + + + + + {/* View Controls */} + {showGrid ? ( ) : ( )} - - + + setSnapMenuAnchor(e.currentTarget)} + active={activeSnapCount > 0} + badge={activeSnapCount || undefined} + > + + - + + + {/* Zoom */} + onZoomChange(Math.max(0.25, zoom - 0.25))} + > + + - + onZoomChange(Math.min(3, zoom + 0.25))} + > + + + + + + {/* Page Navigation */} + + + + + + + = totalPages - 1} + > + + + + + + {/* Actions */} + + {!autoSaveEnabled && ( + + )} + + + {/* Contextual toolbar for text/shape */} + {selectedElement && onUpdateSelectedElement && ( + + {renderContextualToolbar()} + + )} + + {/* Shared Popovers */} + setAddMenuAnchor(null)} + PaperProps={{ sx: { borderRadius: 2, minWidth: 220 } }} + > + {ELEMENT_TYPES.map( + ({ type, icon: Icon, label, color, description, shortcut }) => ( + handleAddElement(type)} + sx={{ py: 1.5 }} + > + + + + + + + + + ), + )} + setSnapMenuAnchor(null)} anchorOrigin={{ vertical: "bottom", horizontal: "left" }} - transformOrigin={{ vertical: "top", horizontal: "left" }} - slotProps={{ - paper: { - sx: { mt: 0.5, borderRadius: 2, minWidth: 280, p: 1 }, - }, - }} + PaperProps={{ sx: { borderRadius: 2 } }} > - + } - label={ - - Tutti - - } + label={Tutti} labelPlacement="start" sx={{ mr: 0 }} /> - - {SNAP_OPTIONS_CONFIG.map( - ({ key, icon: Icon, label, description }) => ( - handleToggleSnapOption(key)} - sx={{ - py: 0.75, - borderRadius: 1, - mb: 0.5, - bgcolor: snapOptions[key] ? "primary.50" : "transparent", - "&:hover": { - bgcolor: snapOptions[key] - ? "primary.100" - : "action.hover", - }, + {SNAP_OPTIONS_CONFIG.map( + ({ key, icon: Icon, label, description }) => ( + handleToggleSnapOption(key)} + sx={{ + py: 0.5, + borderRadius: 1, + bgcolor: snapOptions[key] + ? alpha(theme.palette.primary.main, 0.08) + : "transparent", + }} + > + + + + - - - - - e.stopPropagation()} - onChange={() => handleToggleSnapOption(key)} - /> - - ), - )} - + secondaryTypographyProps={{ variant: "caption" }} + /> + + + ), + )} - - - - - {/* Zoom Controls */} - - - onZoomChange(Math.max(0.25, zoom - 0.25))} - size="small" - sx={{ borderRadius: 1.5 }} - > - - - - - - - setZoomMenuAnchor(null)} anchorOrigin={{ vertical: "bottom", horizontal: "center" }} - transformOrigin={{ vertical: "top", horizontal: "center" }} - slotProps={{ - paper: { - sx: { mt: 0.5, borderRadius: 2, p: 1 }, - }, - }} + PaperProps={{ sx: { borderRadius: 2 } }} > - - - Livello zoom - + onZoomChange(value as number)} size="small" - sx={{ mt: 1, mx: 1, width: "calc(100% - 16px)" }} + valueLabelDisplay="auto" + valueLabelFormat={(v) => `${Math.round(v * 100)}%`} /> - + {ZOOM_PRESETS.map(({ value, label }) => ( ))} - - - - { - onZoomChange(0.75); - setZoomMenuAnchor(null); - }} - > - - - - - - - - - - - onZoomChange(Math.min(2, zoom + 0.25))} - size="small" - sx={{ borderRadius: 1.5 }} - > - - - - - - - - {/* Page Navigation */} - - - - - - - - - - - - - - {currentPageIndex + 1} / {totalPages} + setPageMenuAnchor(null)} + > + + + {currentPageName} - + + { + onPrevPage(); + setPageMenuAnchor(null); + }} + disabled={currentPageIndex <= 0} + > + + + + Pagina precedente + + { + onNextPage(); + setPageMenuAnchor(null); + }} + disabled={currentPageIndex >= totalPages - 1} + > + + + + Pagina successiva + + + + ); + } - - - = totalPages - 1} - sx={{ borderRadius: 1.5 }} - > - - - - - - - - - {/* Keyboard Shortcuts Help */} - - setShortcutsAnchor(e.currentTarget)} - sx={{ borderRadius: 1.5, mr: 1 }} - > - - - - - setShortcutsAnchor(null)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - transformOrigin={{ vertical: "top", horizontal: "right" }} - slotProps={{ - paper: { - sx: { mt: 0.5, borderRadius: 2, p: 2, minWidth: 280 }, - }, + // Desktop: Full professional toolbar + return ( + + {/* Main Toolbar Row */} + - - Scorciatoie Tastiera - - - - {[ - ["Ctrl + Z", "Annulla"], - ["Ctrl + Y", "Ripeti"], - ["Ctrl + S", "Salva"], - ["Ctrl + D", "Duplica"], - ["Canc / Backspace", "Elimina"], - ["Frecce", "Sposta (1px)"], - ["Shift + Frecce", "Sposta (10px)"], - ["G", "Mostra/nascondi griglia"], - ["+ / -", "Zoom"], - ].map(([key, action]) => ( - - - - - - - {action} - - - - ))} - - - + {/* Add Element - Primary Action */} + - {/* Actions */} - + {/* Add Menu with rich descriptions */} + setAddMenuAnchor(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} + PaperProps={{ + sx: { mt: 1, borderRadius: 3, minWidth: 280, overflow: "hidden" }, + }} + > + + + Inserisci elemento + + + {ELEMENT_TYPES.map( + ({ type, icon: Icon, label, shortcut, color, description }) => ( + handleAddElement(type)} + sx={{ + py: 1.5, + px: 1.5, + borderRadius: 2, + mb: 0.5, + transition: "all 0.15s ease", + "&:hover": { + bgcolor: alpha(color, 0.08), + transform: "translateX(4px)", + }, + }} + > + + + + + + + ), + )} + + + + + + + {/* Quick Add Toolbar */} + + {ELEMENT_TYPES.map(({ type, icon: Icon, label, color }) => ( + e.type === type)?.shortcut})`} + onClick={() => onAddElement(type)} + color={color} + > + + + ))} + + + + + {/* Selection Actions */} + + + + + + + + + {isLocked ? ( + + ) : ( + + )} + + + + + + {/* History */} + + + + + + + + {onOpenHistory && ( + + + + )} + + + + + {/* View Controls */} + + + {showGrid ? ( + + ) : ( + + )} + + + + + + {/* Snap Popover */} + setSnapMenuAnchor(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} + PaperProps={{ sx: { mt: 1, borderRadius: 2, minWidth: 300 } }} + > + + + + Allineamento automatico + + + } + label={ + + Tutti + + } + labelPlacement="start" + sx={{ mr: 0 }} + /> + + + + {SNAP_OPTIONS_CONFIG.map( + ({ key, icon: Icon, label, description }) => ( + handleToggleSnapOption(key)} + sx={{ + py: 1, + px: 1.5, + borderRadius: 2, + bgcolor: snapOptions[key] + ? alpha(theme.palette.primary.main, 0.08) + : "transparent", + transition: "all 0.15s ease", + "&:hover": { + bgcolor: snapOptions[key] + ? alpha(theme.palette.primary.main, 0.15) + : "action.hover", + }, + }} + > + + + + + e.stopPropagation()} + /> + + ), + )} + + + + + + + {/* Zoom Controls */} + + onZoomChange(Math.max(0.25, zoom - 0.25))} + > + + + + + + onZoomChange(Math.min(3, zoom + 0.25))} + > + + + + onZoomChange(0.75)} + > + + + + + {/* Zoom Popover */} + setZoomMenuAnchor(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + PaperProps={{ sx: { mt: 1, borderRadius: 2, p: 2, width: 220 } }} + > + + Livello zoom: {Math.round(zoom * 100)}% + + onZoomChange(value as number)} + valueLabelDisplay="auto" + valueLabelFormat={(v) => `${Math.round(v * 100)}%`} + sx={{ mt: 1, mb: 2 }} + /> + + Preset + + + {ZOOM_PRESETS.map(({ value, label }) => ( + { + onZoomChange(value); + setZoomMenuAnchor(null); + }} + sx={{ cursor: "pointer" }} + /> + ))} + + + + + + {/* Page Navigation */} + + + + + + + + = totalPages - 1} + > + + + + + {/* Page Menu */} + setPageMenuAnchor(null)} + PaperProps={{ sx: { borderRadius: 2, minWidth: 200 } }} + > + + + Pagina corrente + + + {currentPageName} + + + + { + onPrevPage(); + setPageMenuAnchor(null); + }} + disabled={currentPageIndex <= 0} + > + + + + + + { + onNextPage(); + setPageMenuAnchor(null); + }} + disabled={currentPageIndex >= totalPages - 1} + > + + + + + + + + + + {/* Save Status */} + + {renderSaveStatus()} + + + {/* Command Palette / Search */} + {onOpenCommandPalette && ( + + + + )} + + {/* Keyboard Shortcuts */} + setShortcutsAnchor(e.currentTarget)} + > + + + + {/* Shortcuts Popover */} + setShortcutsAnchor(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + transformOrigin={{ vertical: "top", horizontal: "right" }} + PaperProps={{ sx: { mt: 1, borderRadius: 2, p: 2, minWidth: 300 } }} + > + + Scorciatoie Tastiera + + + + + {[ + ["Ctrl + Z", "Annulla"], + ["Ctrl + Y", "Ripeti"], + ["Ctrl + S", "Salva"], + ["Ctrl + D", "Duplica"], + ["Ctrl + K", "Cerca comando"], + ["Canc / Backspace", "Elimina"], + ["Frecce", "Sposta (1px)"], + ["Shift + Frecce", "Sposta (10px)"], + ["G", "Mostra/nascondi griglia"], + ["+ / -", "Zoom in/out"], + ["PgUp / PgDn", "Cambia pagina"], + ].map(([key, action]) => ( + + + + + + + {action} + + + + ))} + + + + + + + {/* Primary Actions */} - + )} + + + {/* Contextual Toolbar Row - appears when element is selected */} + {selectedElement && onUpdateSelectedElement && ( + - {isSaving ? "Salvo..." : "Salva"} - - + {/* Element type indicator */} + e.type === selectedElement.type)?.icon + ? (() => { + const Icon = ELEMENT_TYPES.find( + (e) => e.type === selectedElement.type, + )!.icon; + return ; + })() + : undefined + } + label={ + ELEMENT_TYPES.find((e) => e.type === selectedElement.type) + ?.label || selectedElement.type + } + size="small" + variant="outlined" + sx={{ + borderColor: ELEMENT_TYPES.find( + (e) => e.type === selectedElement.type, + )?.color, + color: ELEMENT_TYPES.find((e) => e.type === selectedElement.type) + ?.color, + }} + /> + + + + {/* Contextual formatting options */} + {renderContextualToolbar()} + + + + {/* Element name */} + {selectedElement.name && ( + + {selectedElement.name} + + )} + + {/* Position info */} + + {Math.round(selectedElement.position.x)}× + {Math.round(selectedElement.position.y)} |{" "} + {Math.round(selectedElement.position.width)}× + {Math.round(selectedElement.position.height)}mm + + + )} ); } diff --git a/frontend/src/components/reportEditor/ImageUploadDialog.tsx b/frontend/src/components/reportEditor/ImageUploadDialog.tsx index 0fbcace..18326a6 100644 --- a/frontend/src/components/reportEditor/ImageUploadDialog.tsx +++ b/frontend/src/components/reportEditor/ImageUploadDialog.tsx @@ -13,12 +13,17 @@ import { CircularProgress, Alert, IconButton, + useMediaQuery, + useTheme, + AppBar, + Toolbar, } from "@mui/material"; import { CloudUpload as UploadIcon, Link as LinkIcon, Close as CloseIcon, Image as ImageIcon, + ArrowBack as ArrowBackIcon, } from "@mui/icons-material"; interface ImageUploadDialogProps { @@ -54,6 +59,9 @@ export default function ImageUploadDialog({ onClose, onImageSelected, }: ImageUploadDialogProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [tab, setTab] = useState(0); const [url, setUrl] = useState(""); const [loading, setLoading] = useState(false); @@ -132,7 +140,7 @@ export default function ImageUploadDialog({ setLoading(false); } }, - [processFile] + [processFile], ); const handleDragOver = useCallback((e: React.DragEvent) => { @@ -151,7 +159,7 @@ export default function ImageUploadDialog({ setIsDragging(false); handleFileSelect(e.dataTransfer.files); }, - [handleFileSelect] + [handleFileSelect], ); const handleUrlLoad = useCallback(async () => { @@ -220,7 +228,7 @@ export default function ImageUploadDialog({ setError( innerErr instanceof Error ? innerErr.message - : "Errore nel caricamento dell'immagine" + : "Errore nel caricamento dell'immagine", ); } } finally { @@ -236,20 +244,46 @@ export default function ImageUploadDialog({ }, [preview, onImageSelected, handleClose]); return ( - - - - - - Inserisci Immagine + + {isMobile ? ( + // Mobile: AppBar header + + + + + + + + Inserisci Immagine + + + + ) : ( + // Desktop: Standard DialogTitle + + + + + Inserisci Immagine + + + + - - - - - + + )} - + { @@ -258,9 +292,20 @@ export default function ImageUploadDialog({ setPreview(null); }} sx={{ borderBottom: 1, borderColor: "divider" }} + variant={isMobile ? "fullWidth" : "standard"} > - } label="Carica File" iconPosition="start" /> - } label="Da URL" iconPosition="start" /> + } + label={isMobile ? "Carica" : "Carica File"} + iconPosition="start" + sx={{ minHeight: isMobile ? 56 : 48 }} + /> + } + label={isMobile ? "URL" : "Da URL"} + iconPosition="start" + sx={{ minHeight: isMobile ? 56 : 48 }} + /> {error && ( @@ -277,11 +322,16 @@ export default function ImageUploadDialog({ borderStyle: "dashed", borderColor: isDragging ? "primary.main" : "divider", borderRadius: 2, - p: 4, + p: isMobile ? 3 : 4, textAlign: "center", bgcolor: isDragging ? "primary.50" : "grey.50", cursor: "pointer", transition: "all 0.2s", + minHeight: isMobile ? 150 : "auto", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", "&:hover": { borderColor: "primary.light", bgcolor: "primary.50", @@ -304,16 +354,30 @@ export default function ImageUploadDialog({ ) : ( <> - Trascina un'immagine qui + {isMobile + ? "Tocca per selezionare" + : "Trascina un'immagine qui"} - - oppure clicca per selezionare un file - - - Formati supportati: JPG, PNG, GIF, WebP, SVG (max 10MB) + {!isMobile && ( + + oppure clicca per selezionare un file + + )} + + {isMobile + ? "JPG, PNG, GIF, WebP (max 10MB)" + : "Formati supportati: JPG, PNG, GIF, WebP, SVG (max 10MB)"} )} @@ -330,6 +394,7 @@ export default function ImageUploadDialog({ onChange={(e) => setUrl(e.target.value)} fullWidth disabled={loading} + size={isMobile ? "medium" : "small"} onKeyDown={(e) => { if (e.key === "Enter") { handleUrlLoad(); @@ -340,7 +405,11 @@ export default function ImageUploadDialog({ variant="outlined" onClick={handleUrlLoad} disabled={loading || !url.trim()} - startIcon={loading ? : } + startIcon={ + loading ? : + } + size={isMobile ? "large" : "medium"} + fullWidth={isMobile} > {loading ? "Caricamento..." : "Carica da URL"} @@ -378,17 +447,32 @@ export default function ImageUploadDialog({ alt="Preview" style={{ maxWidth: "100%", - maxHeight: 200, + maxHeight: isMobile ? 150 : 200, objectFit: "contain", }} /> - + {preview.originalWidth} × {preview.originalHeight} px {preview.fileName && ( - + {preview.fileName} )} @@ -397,16 +481,46 @@ export default function ImageUploadDialog({ )} - - - + + {isMobile ? ( + // Mobile: Stacked full-width buttons + <> + + + + ) : ( + // Desktop: Standard layout + <> + + + + )} ); diff --git a/frontend/src/components/reportEditor/PageNavigator.tsx b/frontend/src/components/reportEditor/PageNavigator.tsx index 8fea4c9..d92ec14 100644 --- a/frontend/src/components/reportEditor/PageNavigator.tsx +++ b/frontend/src/components/reportEditor/PageNavigator.tsx @@ -19,6 +19,8 @@ import { DialogContent, DialogActions, TextField, + useMediaQuery, + useTheme, } from "@mui/material"; import { Add as AddIcon, @@ -54,6 +56,9 @@ export default function PageNavigator({ onRenamePage, onMovePage, }: PageNavigatorProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [menuAnchor, setMenuAnchor] = useState<{ element: HTMLElement; pageId: string; @@ -153,15 +158,20 @@ export default function PageNavigator({ const currentMenuIndex = menuAnchor ? getPageIndex(menuAnchor.pageId) : -1; + // Width based on context + const panelWidth = isMobile ? "100%" : 220; + return ( {/* Header */} @@ -212,7 +222,7 @@ export default function PageNavigator({ onClick={() => onSelectPage(page.id)} sx={{ borderRadius: 1, - py: 1, + py: isMobile ? 1.5 : 1, "&.Mui-selected": { bgcolor: "primary.light", color: "primary.contrastText", @@ -230,8 +240,8 @@ export default function PageNavigator({ > {index + 1} @@ -286,7 +297,7 @@ export default function PageNavigator({ @@ -392,6 +405,7 @@ export default function PageNavigator({ variant="contained" onClick={handleRenameConfirm} disabled={!renameDialog.currentName.trim()} + fullWidth={isMobile} > Rinomina @@ -405,6 +419,7 @@ export default function PageNavigator({ setDeleteConfirm({ open: false, pageId: "", pageName: "" }) } maxWidth="xs" + fullScreen={isMobile} > Elimina Pagina @@ -415,11 +430,12 @@ export default function PageNavigator({ Tutti gli elementi sulla pagina verranno eliminati. - + @@ -427,6 +443,7 @@ export default function PageNavigator({ variant="contained" color="error" onClick={handleDeleteConfirm} + fullWidth={isMobile} > Elimina diff --git a/frontend/src/components/reportEditor/PreviewDialog.tsx b/frontend/src/components/reportEditor/PreviewDialog.tsx index 5cb0163..3cbb737 100644 --- a/frontend/src/components/reportEditor/PreviewDialog.tsx +++ b/frontend/src/components/reportEditor/PreviewDialog.tsx @@ -21,6 +21,10 @@ import { IconButton, Tooltip, alpha, + useMediaQuery, + useTheme, + AppBar, + Toolbar, } from "@mui/material"; import { Search as SearchIcon, @@ -36,6 +40,8 @@ import { TableChart as TableIcon, Check as CheckIcon, Clear as ClearIcon, + Close as CloseIcon, + ArrowBack as BackIcon, } from "@mui/icons-material"; import { useQueries } from "@tanstack/react-query"; import { reportGeneratorService } from "../../services/reportService"; @@ -60,11 +66,15 @@ export default function PreviewDialog({ onGeneratePreview, isGenerating, }: PreviewDialogProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [selections, setSelections] = useState>( {}, ); const [searchTerms, setSearchTerms] = useState>({}); const [activeDataset, setActiveDataset] = useState(null); + const [mobileShowList, setMobileShowList] = useState(true); // Mobile: show dataset list or entity list // Fetch entities for each selected dataset const entityQueries = useQueries({ @@ -90,6 +100,7 @@ export default function PreviewDialog({ setActiveDataset( selectedDatasets.length > 0 ? selectedDatasets[0].id : null, ); + setMobileShowList(true); } }, [open, selectedDatasets]); @@ -99,6 +110,11 @@ export default function PreviewDialog({ [datasetId]: entityId, })); + // On mobile, go back to list after selection + if (isMobile) { + setMobileShowList(true); + } + // Passa al prossimo dataset non selezionato const currentIndex = selectedDatasets.findIndex( (ds) => ds.id === datasetId, @@ -108,6 +124,9 @@ export default function PreviewDialog({ ); if (nextUnselected) { setActiveDataset(nextUnselected.id); + if (isMobile) { + setTimeout(() => setMobileShowList(false), 300); + } } }; @@ -142,6 +161,13 @@ export default function PreviewDialog({ onGeneratePreview(dataSources); }; + const handleSelectDataset = (datasetId: string) => { + setActiveDataset(datasetId); + if (isMobile) { + setMobileShowList(false); + } + }; + const getDatasetIcon = (icon: string) => { switch (icon) { case "event": @@ -205,31 +231,318 @@ export default function PreviewDialog({ searchTerms[activeDataset || ""] || "", ); + // Render dataset list (sidebar on desktop, main view on mobile) + const renderDatasetList = () => ( + + {selectedDatasets.map((dataset, index) => { + const isSelected = selections[dataset.id] !== null; + const isActive = activeDataset === dataset.id; + const query = entityQueries[index]; + const selectedEntity = isSelected + ? ((query.data as EntityListItemDto[]) || []).find( + (e) => e.id === selections[dataset.id], + ) + : null; + + return ( + handleSelectDataset(dataset.id)} + sx={{ + borderBottom: 1, + borderColor: "divider", + bgcolor: isSelected + ? (theme) => alpha(theme.palette.success.main, 0.08) + : "inherit", + py: isMobile ? 1.5 : 1, + }} + > + + {isSelected ? ( + + ) : ( + getDatasetIcon(dataset.icon) + )} + + + {isSelected && ( + + { + e.stopPropagation(); + handleClearSelection(dataset.id); + }} + > + + + + )} + + ); + })} + + ); + + // Render entity list for active dataset + const renderEntityList = () => { + if (!activeDatasetObj) return null; + + return ( + + {/* Header dataset attivo */} + + {isMobile && ( + + setMobileShowList(true)}> + + + Seleziona + + )} + + {getDatasetIcon(activeDatasetObj.icon)} + + {activeDatasetObj.name} + + {!isMobile && ( + + )} + + {!isMobile && ( + + {activeDatasetObj.description} + + )} + + {/* Ricerca */} + handleSearchChange(activeDataset!, e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchTerms[activeDataset || ""] && ( + + handleSearchChange(activeDataset!, "")} + > + + + + ), + }} + /> + + + {/* Lista entità */} + + {activeQuery?.isLoading ? ( + + + + ) : filteredEntities.length === 0 ? ( + + + {searchTerms[activeDataset || ""] + ? "Nessun risultato trovato" + : "Nessuna entità disponibile"} + + + ) : ( + + {filteredEntities.map((entity) => { + const isEntitySelected = + selections[activeDataset!] === entity.id; + + return ( + + handleSelectionChange(activeDataset!, entity.id) + } + selected={isEntitySelected} + sx={{ + borderBottom: 1, + borderColor: "divider", + bgcolor: isEntitySelected + ? (theme) => alpha(theme.palette.primary.main, 0.12) + : "inherit", + py: isMobile ? 1.5 : 1, + }} + > + + {isEntitySelected ? ( + + ) : ( + + )} + + + {entity.description} + {entity.secondaryInfo && !isMobile && ( + + {entity.secondaryInfo} + + )} + + } + primaryTypographyProps={{ + variant: "body2", + fontWeight: isEntitySelected ? 600 : 400, + }} + secondaryTypographyProps={{ + variant: "caption", + }} + /> + {entity.status && ( + + )} + + ); + })} + + )} + + + {/* Footer con conteggio */} + + + {filteredEntities.length} risultati + + + + ); + }; + return ( - - - Anteprima Report - - - - Seleziona un'entità per ogni dataset da utilizzare nell'anteprima - - + {/* Mobile AppBar */} + {isMobile ? ( + + + + + + + Anteprima Report + + + + + ) : ( + + + Anteprima Report + + + + Seleziona un'entità per ogni dataset da utilizzare nell'anteprima + + + )} - + {hasError && ( Errore nel caricamento dei dati disponibili @@ -243,7 +556,38 @@ export default function PreviewDialog({ almeno un dataset per poter generare l'anteprima. + ) : isMobile ? ( + // Mobile: Switch between dataset list and entity list + + {mobileShowList ? ( + + + + Seleziona un'entità per ogni dataset + + + {renderDatasetList()} + + ) : ( + renderEntityList() + )} + ) : ( + // Desktop: Side by side <> {/* Lista dataset (sidebar) */} - - {selectedDatasets.map((dataset, index) => { - const isSelected = selections[dataset.id] !== null; - const isActive = activeDataset === dataset.id; - const query = entityQueries[index]; - const selectedEntity = isSelected - ? ((query.data as EntityListItemDto[]) || []).find( - (e) => e.id === selections[dataset.id], - ) - : null; - - return ( - setActiveDataset(dataset.id)} - sx={{ - borderBottom: 1, - borderColor: "divider", - bgcolor: isSelected - ? (theme) => alpha(theme.palette.success.main, 0.08) - : "inherit", - }} - > - - {isSelected ? ( - - ) : ( - getDatasetIcon(dataset.icon) - )} - - - {isSelected && ( - - { - e.stopPropagation(); - handleClearSelection(dataset.id); - }} - > - - - - )} - - ); - })} - + {renderDatasetList()} {/* Area principale con lista entità */} - - {activeDatasetObj && ( - <> - {/* Header dataset attivo */} - - - {getDatasetIcon(activeDatasetObj.icon)} - - {activeDatasetObj.name} - - - - - {activeDatasetObj.description} - - - {/* Ricerca */} - - handleSearchChange(activeDataset!, e.target.value) - } - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchTerms[activeDataset || ""] && ( - - - handleSearchChange(activeDataset!, "") - } - > - - - - ), - }} - /> - - - {/* Lista entità */} - - {activeQuery?.isLoading ? ( - - - - ) : filteredEntities.length === 0 ? ( - - - {searchTerms[activeDataset || ""] - ? "Nessun risultato trovato per la ricerca" - : "Nessuna entità disponibile"} - - - ) : ( - - {filteredEntities.map((entity) => { - const isEntitySelected = - selections[activeDataset!] === entity.id; - - return ( - - handleSelectionChange(activeDataset!, entity.id) - } - selected={isEntitySelected} - sx={{ - borderBottom: 1, - borderColor: "divider", - bgcolor: isEntitySelected - ? (theme) => - alpha(theme.palette.primary.main, 0.12) - : "inherit", - }} - > - - {isEntitySelected ? ( - - ) : ( - - )} - - - {entity.description} - {entity.secondaryInfo && ( - - {entity.secondaryInfo} - - )} - - } - primaryTypographyProps={{ - variant: "body2", - fontWeight: isEntitySelected ? 600 : 400, - }} - secondaryTypographyProps={{ - variant: "caption", - }} - /> - {entity.status && ( - - )} - - ); - })} - - )} - - - {/* Footer con conteggio */} - - - {filteredEntities.length} risultati - {searchTerms[activeDataset || ""] && - ` per "${searchTerms[activeDataset || ""]}"`} - - - - )} - + {renderEntityList()} )} - - + + diff --git a/frontend/src/components/reportEditor/PropertiesPanel.tsx b/frontend/src/components/reportEditor/PropertiesPanel.tsx index 40302e5..a9df476 100644 --- a/frontend/src/components/reportEditor/PropertiesPanel.tsx +++ b/frontend/src/components/reportEditor/PropertiesPanel.tsx @@ -22,6 +22,8 @@ import { ListItemSecondaryAction, Button, Chip, + useMediaQuery, + useTheme, } from "@mui/material"; import { ExpandMore as ExpandMoreIcon, @@ -92,6 +94,9 @@ export default function PropertiesPanel({ currentPage, onUpdateCurrentPage, }: PropertiesPanelProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [expanded, setExpanded] = useState([ "position", "style", @@ -175,23 +180,37 @@ export default function PropertiesPanel({ } }; + // Panel width based on context + const panelWidth = isMobile ? "100%" : 280; + if (!element) { // Show page settings when no element selected return ( - - {currentPage ? `Pagina: ${currentPage.name}` : "Impostazioni Pagina"} - + {!isMobile && ( + + {currentPage + ? `Pagina: ${currentPage.name}` + : "Impostazioni Pagina"} + + )} - + {/* Page name (only if currentPage is available) */} {currentPage && onUpdateCurrentPage && ( <> @@ -354,10 +373,12 @@ export default function PropertiesPanel({ return ( @@ -373,11 +394,13 @@ export default function PropertiesPanel({ }> Posizione - + }> Stile - + {element.type === "text" && ( <> @@ -474,7 +499,12 @@ export default function PropertiesPanel({ - + updateStyle("fontSize", Number(e.target.value)) } - sx={{ width: 100 }} + sx={{ width: isMobile ? "100%" : 100 }} /> - + - + Colore Bordo }> Immagine - + {/* Image Preview or Upload Button */} {element.imageSettings?.src ? ( @@ -642,7 +674,7 @@ export default function PropertiesPanel({ - Mantieni proporzioni + Proporzioni - {/* Alignment */} - - - Allineamento Orizzontale - - - value && updateImageSettings("horizontalAlign", value) - } - size="small" - fullWidth - > - - - - - - - - - - - - - - - Allineamento Verticale - - - value && updateImageSettings("verticalAlign", value) - } - size="small" - fullWidth - > - Alto - Centro - Basso - - - {/* Opacity */} @@ -878,11 +867,13 @@ export default function PropertiesPanel({ }> Contenuto - + Tipo @@ -915,7 +906,7 @@ export default function PropertiesPanel({ placeholder="{{evento.codice}}" value={element.content?.expression || ""} onChange={(e) => updateContent("expression", e.target.value)} - helperText="Es: {{evento.codice}}, {{cliente.ragioneSociale}}" + helperText={isMobile ? undefined : "Es: {{evento.codice}}"} /> )} @@ -959,11 +950,13 @@ export default function PropertiesPanel({ }> Sezione - + Sezione + + {/* Mobile import button inline with filter */} + {isMobile && ( + + )} + {/* Empty State */} {templates.length === 0 ? ( - + Nessun template trovato @@ -194,11 +247,16 @@ export default function ReportTemplatesPage() { Crea il tuo primo template di report o importane uno esistente - + @@ -206,27 +264,44 @@ export default function ReportTemplatesPage() { variant="contained" startIcon={} onClick={() => navigate("/report-editor")} + fullWidth={isMobile} > Crea Template - + ) : ( - + /* Template Grid */ + {templates.map((template) => ( - + + {/* Thumbnail */} {template.thumbnailBase64 ? ( - + )} - + + {/* Content */} + - + {template.nome} {template.descrizione && ( {template.descrizione} @@ -276,7 +370,17 @@ export default function ReportTemplatesPage() { : "Orizzontale"} - + + {/* Actions */} + - + @@ -293,7 +397,7 @@ export default function ReportTemplatesPage() { size="small" onClick={() => cloneMutation.mutate(template.id)} > - + @@ -301,7 +405,9 @@ export default function ReportTemplatesPage() { size="small" onClick={() => handleExport(template)} > - + @@ -311,7 +417,7 @@ export default function ReportTemplatesPage() { color="error" onClick={() => setDeleteDialog({ open: true, template })} > - + @@ -321,10 +427,32 @@ export default function ReportTemplatesPage() { )} + {/* Mobile FAB */} + {isMobile && ( + + navigate("/report-editor")} + sx={{ + position: "fixed", + bottom: 16, + right: 16, + zIndex: 1000, + }} + > + + + + )} + {/* Delete Dialog */} setDeleteDialog({ open: false, template: null })} + fullWidth + maxWidth="xs" + fullScreen={isMobile} > Conferma Eliminazione @@ -336,9 +464,10 @@ export default function ReportTemplatesPage() { Questa azione non può essere annullata. - + @@ -350,6 +479,7 @@ export default function ReportTemplatesPage() { deleteMutation.mutate(deleteDialog.template.id) } disabled={deleteMutation.isPending} + fullWidth={isMobile} > {deleteMutation.isPending ? "Eliminazione..." : "Elimina"} @@ -363,6 +493,9 @@ export default function ReportTemplatesPage() { setImportDialog(false); setImportFile(null); }} + fullWidth + maxWidth="xs" + fullScreen={isMobile} > Importa Template @@ -379,12 +512,13 @@ export default function ReportTemplatesPage() { /> - + @@ -392,6 +526,7 @@ export default function ReportTemplatesPage() { variant="contained" onClick={handleImport} disabled={!importFile || importMutation.isPending} + fullWidth={isMobile} > {importMutation.isPending ? "Importazione..." : "Importa"} diff --git a/frontend/src/types/report.ts b/frontend/src/types/report.ts index c95972a..4e9440e 100644 --- a/frontend/src/types/report.ts +++ b/frontend/src/types/report.ts @@ -125,6 +125,7 @@ export interface AprtStyle { fontSize: number; fontWeight: "normal" | "bold"; fontStyle: "normal" | "italic"; + textDecoration?: "none" | "underline" | "line-through"; color: string; backgroundColor?: string; textAlign: "left" | "center" | "right" | "justify"; diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm deleted file mode 100644 index b16d5fc..0000000 Binary files a/src/Apollinare.API/apollinare.db-shm and /dev/null differ diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal deleted file mode 100644 index b6c8af8..0000000 Binary files a/src/Apollinare.API/apollinare.db-wal and /dev/null differ