Initial commit

This commit is contained in:
2025-11-28 10:59:10 +01:00
commit 14b3e965d0
540 changed files with 784121 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Box,
Button,
Card,
CardContent,
CardMedia,
CardActions,
Grid,
Typography,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Chip,
Tooltip,
CircularProgress,
} from "@mui/material";
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
ContentCopy as CopyIcon,
Download as DownloadIcon,
Upload as UploadIcon,
Description as DescriptionIcon,
} from "@mui/icons-material";
import { reportTemplateService, downloadBlob } from "../services/reportService";
import type { ReportTemplateDto } from "../types/report";
export default function ReportTemplatesPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [filterCategoria, setFilterCategoria] = useState<string>("");
const [deleteDialog, setDeleteDialog] = useState<{
open: boolean;
template: ReportTemplateDto | null;
}>({
open: false,
template: null,
});
const [importDialog, setImportDialog] = useState(false);
const [importFile, setImportFile] = useState<File | null>(null);
const { data: templates = [], isLoading } = useQuery({
queryKey: ["report-templates", filterCategoria],
queryFn: () => reportTemplateService.getAll(filterCategoria || undefined),
});
const { data: categories = [] } = useQuery({
queryKey: ["report-template-categories"],
queryFn: () => reportTemplateService.getCategories(),
});
const deleteMutation = useMutation({
mutationFn: (id: number) => reportTemplateService.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["report-templates"] });
setDeleteDialog({ open: false, template: null });
},
});
const cloneMutation = useMutation({
mutationFn: (id: number) => reportTemplateService.clone(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["report-templates"] });
},
});
const importMutation = useMutation({
mutationFn: (file: File) => reportTemplateService.import(file),
onSuccess: (newTemplate) => {
queryClient.invalidateQueries({ queryKey: ["report-templates"] });
setImportDialog(false);
setImportFile(null);
navigate(`/report-editor/${newTemplate.id}`);
},
});
const handleExport = async (template: ReportTemplateDto) => {
const blob = await reportTemplateService.export(template.id);
downloadBlob(blob, `${template.nome.replace(/\s+/g, "_")}.aprt`);
};
const handleImport = () => {
if (importFile) {
importMutation.mutate(importFile);
}
};
const getCategoriaColor = (
categoria: string,
):
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "error"
| "info" => {
const colors: Record<
string,
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "error"
| "info"
> = {
Evento: "primary",
Cliente: "secondary",
Articoli: "success",
Generale: "default",
Importato: "warning",
};
return colors[categoria] || "default";
};
if (isLoading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="400px"
>
<CircularProgress />
</Box>
);
}
return (
<Box>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
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>
</Box>
<Box mb={3}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>Filtra per categoria</InputLabel>
<Select
value={filterCategoria}
label="Filtra per categoria"
onChange={(e) => setFilterCategoria(e.target.value)}
>
<MenuItem value="">Tutte</MenuItem>
{categories.map((cat) => (
<MenuItem key={cat} value={cat}>
{cat}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{templates.length === 0 ? (
<Card>
<CardContent sx={{ textAlign: "center", py: 6 }}>
<DescriptionIcon
sx={{ fontSize: 64, color: "text.secondary", mb: 2 }}
/>
<Typography variant="h6" color="text.secondary" gutterBottom>
Nessun template trovato
</Typography>
<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">
<Button
variant="outlined"
startIcon={<UploadIcon />}
onClick={() => setImportDialog(true)}
>
Importa Template
</Button>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => navigate("/report-editor")}
>
Crea Template
</Button>
</Box>
</CardContent>
</Card>
) : (
<Grid container spacing={3}>
{templates.map((template) => (
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={template.id}>
<Card
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
}}
>
{template.thumbnailBase64 ? (
<CardMedia
component="img"
height="160"
image={`data:image/png;base64,${template.thumbnailBase64}`}
alt={template.nome}
sx={{ objectFit: "contain", bgcolor: "grey.100" }}
/>
) : (
<Box
sx={{
height: 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: "grey.100",
}}
>
<DescriptionIcon sx={{ fontSize: 64, color: "grey.400" }} />
</Box>
)}
<CardContent sx={{ flexGrow: 1 }}>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-start"
mb={1}
>
<Typography variant="h6" noWrap sx={{ maxWidth: "70%" }}>
{template.nome}
</Typography>
<Chip
label={template.categoria}
size="small"
color={getCategoriaColor(template.categoria)}
/>
</Box>
{template.descrizione && (
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 1 }}
>
{template.descrizione}
</Typography>
)}
<Typography variant="caption" color="text.secondary">
{template.pageSize} -{" "}
{template.orientation === "portrait"
? "Verticale"
: "Orizzontale"}
</Typography>
</CardContent>
<CardActions sx={{ justifyContent: "space-between" }}>
<Box>
<Tooltip title="Modifica">
<IconButton
size="small"
onClick={() =>
navigate(`/report-editor/${template.id}`)
}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Duplica">
<IconButton
size="small"
onClick={() => cloneMutation.mutate(template.id)}
>
<CopyIcon />
</IconButton>
</Tooltip>
<Tooltip title="Esporta">
<IconButton
size="small"
onClick={() => handleExport(template)}
>
<DownloadIcon />
</IconButton>
</Tooltip>
</Box>
<Tooltip title="Elimina">
<IconButton
size="small"
color="error"
onClick={() => setDeleteDialog({ open: true, template })}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</CardActions>
</Card>
</Grid>
))}
</Grid>
)}
{/* Delete Dialog */}
<Dialog
open={deleteDialog.open}
onClose={() => setDeleteDialog({ open: false, template: null })}
>
<DialogTitle>Conferma Eliminazione</DialogTitle>
<DialogContent>
<Typography>
Sei sicuro di voler eliminare il template "
{deleteDialog.template?.nome}"?
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Questa azione non può essere annullata.
</Typography>
</DialogContent>
<DialogActions>
<Button
onClick={() => setDeleteDialog({ open: false, template: null })}
>
Annulla
</Button>
<Button
color="error"
variant="contained"
onClick={() =>
deleteDialog.template &&
deleteMutation.mutate(deleteDialog.template.id)
}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
</Button>
</DialogActions>
</Dialog>
{/* Import Dialog */}
<Dialog
open={importDialog}
onClose={() => {
setImportDialog(false);
setImportFile(null);
}}
>
<DialogTitle>Importa Template</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" mb={2}>
Seleziona un file .aprt da importare
</Typography>
<Button variant="outlined" component="label" fullWidth>
{importFile ? importFile.name : "Seleziona File"}
<input
type="file"
hidden
accept=".aprt,.json"
onChange={(e) => setImportFile(e.target.files?.[0] || null)}
/>
</Button>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setImportDialog(false);
setImportFile(null);
}}
>
Annulla
</Button>
<Button
variant="contained"
onClick={handleImport}
disabled={!importFile || importMutation.isPending}
>
{importMutation.isPending ? "Importazione..." : "Importa"}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}