Initial commit
This commit is contained in:
402
frontend/src/pages/ReportTemplatesPage.tsx
Normal file
402
frontend/src/pages/ReportTemplatesPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user