-
This commit is contained in:
@@ -22,6 +22,11 @@ import {
|
||||
Chip,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
Stack,
|
||||
Fab,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Add as AddIcon,
|
||||
@@ -38,6 +43,11 @@ import type { ReportTemplateDto } from "../types/report";
|
||||
export default function ReportTemplatesPage() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const theme = useTheme();
|
||||
|
||||
// Breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const [filterCategoria, setFilterCategoria] = useState<string>("");
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
open: boolean;
|
||||
@@ -138,34 +148,54 @@ export default function ReportTemplatesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ pb: isMobile ? 10 : 0 }}>
|
||||
{/* Header */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mb={3}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
justifyContent: "space-between",
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
gap: 2,
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Template Report</Typography>
|
||||
<Box display="flex" gap={2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => setImportDialog(true)}
|
||||
>
|
||||
Importa
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => navigate("/report-editor")}
|
||||
>
|
||||
Nuovo Template
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant={isMobile ? "h5" : "h4"} sx={{ fontWeight: 600 }}>
|
||||
Template Report
|
||||
</Typography>
|
||||
|
||||
{/* Desktop/Tablet buttons */}
|
||||
{!isMobile && (
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => setImportDialog(true)}
|
||||
>
|
||||
Importa
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => navigate("/report-editor")}
|
||||
>
|
||||
Nuovo Template
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box mb={3}>
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
{/* Filter */}
|
||||
<Box
|
||||
sx={{
|
||||
mb: 3,
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
gap: 2,
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
}}
|
||||
>
|
||||
<FormControl size="small" sx={{ minWidth: { xs: "100%", sm: 200 } }}>
|
||||
<InputLabel>Filtra per categoria</InputLabel>
|
||||
<Select
|
||||
value={filterCategoria}
|
||||
@@ -180,13 +210,36 @@ export default function ReportTemplatesPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Mobile import button inline with filter */}
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => setImportDialog(true)}
|
||||
fullWidth
|
||||
>
|
||||
Importa Template
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Empty State */}
|
||||
{templates.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center", py: 6 }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
py: { xs: 4, sm: 6 },
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<DescriptionIcon
|
||||
sx={{ fontSize: 64, color: "text.secondary", mb: 2 }}
|
||||
sx={{
|
||||
fontSize: { xs: 48, sm: 64 },
|
||||
color: "text.secondary",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
Nessun template trovato
|
||||
@@ -194,11 +247,16 @@ export default function ReportTemplatesPage() {
|
||||
<Typography color="text.secondary" mb={3}>
|
||||
Crea il tuo primo template di report o importane uno esistente
|
||||
</Typography>
|
||||
<Box display="flex" gap={2} justifyContent="center">
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => setImportDialog(true)}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Importa Template
|
||||
</Button>
|
||||
@@ -206,27 +264,44 @@ export default function ReportTemplatesPage() {
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => navigate("/report-editor")}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Crea Template
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
/* Template Grid */
|
||||
<Grid container spacing={{ xs: 2, sm: 3 }}>
|
||||
{templates.map((template) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={template.id}>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
sm: 6,
|
||||
md: 4,
|
||||
lg: 3,
|
||||
xl: 2,
|
||||
}}
|
||||
key={template.id}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "transform 0.2s, box-shadow 0.2s",
|
||||
"&:hover": {
|
||||
transform: { sm: "translateY(-4px)" },
|
||||
boxShadow: { sm: 4 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
{template.thumbnailBase64 ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="160"
|
||||
height={isMobile ? 120 : 160}
|
||||
image={`data:image/png;base64,${template.thumbnailBase64}`}
|
||||
alt={template.nome}
|
||||
sx={{ objectFit: "contain", bgcolor: "grey.100" }}
|
||||
@@ -234,37 +309,56 @@ export default function ReportTemplatesPage() {
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: 160,
|
||||
height: isMobile ? 120 : 160,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
bgcolor: "grey.100",
|
||||
}}
|
||||
>
|
||||
<DescriptionIcon sx={{ fontSize: 64, color: "grey.400" }} />
|
||||
<DescriptionIcon
|
||||
sx={{ fontSize: { xs: 48, sm: 64 }, color: "grey.400" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<CardContent sx={{ flexGrow: 1 }}>
|
||||
|
||||
{/* Content */}
|
||||
<CardContent sx={{ flexGrow: 1, p: { xs: 1.5, sm: 2 } }}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
mb={1}
|
||||
gap={1}
|
||||
>
|
||||
<Typography variant="h6" noWrap sx={{ maxWidth: "70%" }}>
|
||||
<Typography
|
||||
variant={isMobile ? "body1" : "h6"}
|
||||
noWrap
|
||||
sx={{
|
||||
flex: 1,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{template.nome}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={template.categoria}
|
||||
size="small"
|
||||
color={getCategoriaColor(template.categoria)}
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
{template.descrizione && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 1 }}
|
||||
sx={{
|
||||
mb: 1,
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{template.descrizione}
|
||||
</Typography>
|
||||
@@ -276,7 +370,17 @@ export default function ReportTemplatesPage() {
|
||||
: "Orizzontale"}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ justifyContent: "space-between" }}>
|
||||
|
||||
{/* Actions */}
|
||||
<CardActions
|
||||
sx={{
|
||||
justifyContent: "space-between",
|
||||
px: { xs: 1, sm: 1.5 },
|
||||
py: 1,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Tooltip title="Modifica">
|
||||
<IconButton
|
||||
@@ -285,7 +389,7 @@ export default function ReportTemplatesPage() {
|
||||
navigate(`/report-editor/${template.id}`)
|
||||
}
|
||||
>
|
||||
<EditIcon />
|
||||
<EditIcon fontSize={isMobile ? "small" : "medium"} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Duplica">
|
||||
@@ -293,7 +397,7 @@ export default function ReportTemplatesPage() {
|
||||
size="small"
|
||||
onClick={() => cloneMutation.mutate(template.id)}
|
||||
>
|
||||
<CopyIcon />
|
||||
<CopyIcon fontSize={isMobile ? "small" : "medium"} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Esporta">
|
||||
@@ -301,7 +405,9 @@ export default function ReportTemplatesPage() {
|
||||
size="small"
|
||||
onClick={() => handleExport(template)}
|
||||
>
|
||||
<DownloadIcon />
|
||||
<DownloadIcon
|
||||
fontSize={isMobile ? "small" : "medium"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -311,7 +417,7 @@ export default function ReportTemplatesPage() {
|
||||
color="error"
|
||||
onClick={() => setDeleteDialog({ open: true, template })}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<DeleteIcon fontSize={isMobile ? "small" : "medium"} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</CardActions>
|
||||
@@ -321,10 +427,32 @@ export default function ReportTemplatesPage() {
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Mobile FAB */}
|
||||
{isMobile && (
|
||||
<Zoom in>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="Nuovo template"
|
||||
onClick={() => navigate("/report-editor")}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
)}
|
||||
|
||||
{/* Delete Dialog */}
|
||||
<Dialog
|
||||
open={deleteDialog.open}
|
||||
onClose={() => setDeleteDialog({ open: false, template: null })}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Conferma Eliminazione</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -336,9 +464,10 @@ export default function ReportTemplatesPage() {
|
||||
Questa azione non può essere annullata.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogActions sx={{ p: { xs: 2, sm: 1 } }}>
|
||||
<Button
|
||||
onClick={() => setDeleteDialog({ open: false, template: null })}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Annulla
|
||||
</Button>
|
||||
@@ -350,6 +479,7 @@ export default function ReportTemplatesPage() {
|
||||
deleteMutation.mutate(deleteDialog.template.id)
|
||||
}
|
||||
disabled={deleteMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
|
||||
</Button>
|
||||
@@ -363,6 +493,9 @@ export default function ReportTemplatesPage() {
|
||||
setImportDialog(false);
|
||||
setImportFile(null);
|
||||
}}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Importa Template</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -379,12 +512,13 @@ export default function ReportTemplatesPage() {
|
||||
/>
|
||||
</Button>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogActions sx={{ p: { xs: 2, sm: 1 } }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setImportDialog(false);
|
||||
setImportFile(null);
|
||||
}}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Annulla
|
||||
</Button>
|
||||
@@ -392,6 +526,7 @@ export default function ReportTemplatesPage() {
|
||||
variant="contained"
|
||||
onClick={handleImport}
|
||||
disabled={!importFile || importMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{importMutation.isPending ? "Importazione..." : "Importa"}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user