This commit is contained in:
2025-11-29 13:30:28 +01:00
parent 824a761bf6
commit bb2d0729e1
16 changed files with 3102 additions and 37 deletions

View File

@@ -0,0 +1,515 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
Box,
Card,
CardContent,
Typography,
Button,
Chip,
Grid,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Alert,
CircularProgress,
Tooltip,
LinearProgress,
Divider,
Switch,
FormControlLabel,
} from "@mui/material";
import {
Refresh as RefreshIcon,
Info as InfoIcon,
Warning as WarningIcon,
Schedule as ScheduleIcon,
Autorenew as RenewIcon,
} from "@mui/icons-material";
import * as Icons from "@mui/icons-material";
import { useModules } from "../contexts/ModuleContext";
import { moduleService } from "../services/moduleService";
import type { ModuleDto } from "../types/module";
import {
formatPrice,
formatDate,
getDaysRemainingText,
getSubscriptionStatusColor,
getSubscriptionStatusText,
} from "../types/module";
export default function ModulesAdminPage() {
const navigate = useNavigate();
const { modules, isLoading, refreshModules } = useModules();
const [selectedModule, setSelectedModule] = useState<ModuleDto | null>(null);
const [confirmDisable, setConfirmDisable] = useState<string | null>(null);
// Query per moduli in scadenza
const { data: expiringModules = [] } = useQuery({
queryKey: ["modules", "expiring"],
queryFn: () => moduleService.getExpiring(30),
});
// Mutation per disattivare modulo
const disableMutation = useMutation({
mutationFn: (code: string) => moduleService.disable(code),
onSuccess: () => {
refreshModules();
setConfirmDisable(null);
},
});
// Mutation per rinnovare subscription
const renewMutation = useMutation({
mutationFn: (code: string) => moduleService.renewSubscription(code),
onSuccess: () => {
refreshModules();
},
});
// Mutation per controllare scadenze
const checkExpiredMutation = useMutation({
mutationFn: () => moduleService.checkExpired(),
onSuccess: () => {
refreshModules();
},
});
// Helper per ottenere icona modulo
const getModuleIcon = (iconName?: string) => {
if (!iconName) return <Icons.Extension />;
const IconComponent = (Icons as Record<string, React.ComponentType>)[
iconName.replace(/\s+/g, "")
];
return IconComponent ? <IconComponent /> : <Icons.Extension />;
};
if (isLoading) {
return (
<Box sx={{ p: 3 }}>
<LinearProgress />
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
{/* Header */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 3,
}}
>
<Box>
<Typography variant="h4" gutterBottom>
Gestione Moduli
</Typography>
<Typography color="text.secondary">
Configura i moduli attivi e gestisci le subscription
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1 }}>
<Button
variant="outlined"
startIcon={<ScheduleIcon />}
onClick={() => checkExpiredMutation.mutate()}
disabled={checkExpiredMutation.isPending}
>
Controlla Scadenze
</Button>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => refreshModules()}
>
Aggiorna
</Button>
</Box>
</Box>
{/* Alert moduli in scadenza */}
{expiringModules.length > 0 && (
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
<Typography variant="subtitle2" gutterBottom>
{expiringModules.length} modulo/i in scadenza nei prossimi 30
giorni:
</Typography>
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
{expiringModules.map((m) => (
<Chip
key={m.code}
label={`${m.name} (${getDaysRemainingText(m.subscription?.daysRemaining)})`}
size="small"
color="warning"
/>
))}
</Box>
</Alert>
)}
{/* Griglia moduli */}
<Grid container spacing={3}>
{modules.map((module) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={module.code}>
<ModuleCard
module={module}
onToggle={() => {
if (module.isEnabled && !module.isCore) {
setConfirmDisable(module.code);
} else if (!module.isEnabled) {
navigate(`/modules/purchase/${module.code}`);
}
}}
onInfo={() => setSelectedModule(module)}
onRenew={() => renewMutation.mutate(module.code)}
isRenewing={renewMutation.isPending}
getIcon={getModuleIcon}
/>
</Grid>
))}
</Grid>
{/* Dialog dettagli modulo */}
<Dialog
open={!!selectedModule}
onClose={() => setSelectedModule(null)}
maxWidth="sm"
fullWidth
>
{selectedModule && (
<>
<DialogTitle>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{getModuleIcon(selectedModule.icon)}
{selectedModule.name}
</Box>
</DialogTitle>
<DialogContent dividers>
<Typography paragraph>{selectedModule.description}</Typography>
<Divider sx={{ my: 2 }} />
<Grid container spacing={2}>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Prezzo annuale
</Typography>
<Typography variant="h6">
{formatPrice(selectedModule.basePrice)}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Prezzo mensile
</Typography>
<Typography variant="h6">
{formatPrice(selectedModule.monthlyPrice)}
</Typography>
</Grid>
</Grid>
{selectedModule.dependencies.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="text.secondary">
Dipendenze
</Typography>
<Box sx={{ display: "flex", gap: 0.5, mt: 0.5 }}>
{selectedModule.dependencies.map((dep) => (
<Chip key={dep} label={dep} size="small" />
))}
</Box>
</Box>
)}
{selectedModule.subscription && (
<>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" gutterBottom>
Dettagli Subscription
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Tipo
</Typography>
<Typography>
{selectedModule.subscription.subscriptionTypeName}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Stato
</Typography>
<Typography>
<Chip
label={getSubscriptionStatusText(
selectedModule.subscription,
)}
size="small"
color={getSubscriptionStatusColor(
selectedModule.subscription,
)}
/>
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Data inizio
</Typography>
<Typography>
{formatDate(selectedModule.subscription.startDate)}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Data scadenza
</Typography>
<Typography>
{formatDate(selectedModule.subscription.endDate)}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Giorni rimanenti
</Typography>
<Typography>
{getDaysRemainingText(
selectedModule.subscription.daysRemaining,
)}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="caption" color="text.secondary">
Rinnovo automatico
</Typography>
<Typography>
{selectedModule.subscription.autoRenew ? "Sì" : "No"}
</Typography>
</Grid>
</Grid>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setSelectedModule(null)}>Chiudi</Button>
</DialogActions>
</>
)}
</Dialog>
{/* Dialog conferma disattivazione */}
<Dialog
open={!!confirmDisable}
onClose={() => setConfirmDisable(null)}
maxWidth="xs"
fullWidth
>
<DialogTitle>Conferma disattivazione</DialogTitle>
<DialogContent>
<Typography>
Sei sicuro di voler disattivare il modulo{" "}
<strong>
{modules.find((m) => m.code === confirmDisable)?.name}
</strong>
?
</Typography>
<Typography color="text.secondary" sx={{ mt: 1 }}>
I dati inseriti rimarranno nel sistema ma non saranno più
accessibili fino alla riattivazione.
</Typography>
{disableMutation.isError && (
<Alert severity="error" sx={{ mt: 2 }}>
{(disableMutation.error as Error)?.message ||
"Errore durante la disattivazione"}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDisable(null)}>Annulla</Button>
<Button
color="error"
variant="contained"
onClick={() =>
confirmDisable && disableMutation.mutate(confirmDisable)
}
disabled={disableMutation.isPending}
startIcon={
disableMutation.isPending ? (
<CircularProgress size={16} color="inherit" />
) : null
}
>
Disattiva
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
// Componente Card singolo modulo
interface ModuleCardProps {
module: ModuleDto;
onToggle: () => void;
onInfo: () => void;
onRenew: () => void;
isRenewing: boolean;
getIcon: (iconName?: string) => React.ReactNode;
}
function ModuleCard({
module,
onToggle,
onInfo,
onRenew,
isRenewing,
getIcon,
}: ModuleCardProps) {
const statusColor = getSubscriptionStatusColor(module.subscription);
const statusText = getSubscriptionStatusText(module.subscription);
return (
<Card
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
borderLeft: 4,
borderColor: module.isEnabled
? statusColor === "success"
? "success.main"
: statusColor === "warning"
? "warning.main"
: "error.main"
: "grey.300",
opacity: module.isEnabled ? 1 : 0.7,
}}
>
<CardContent sx={{ flexGrow: 1 }}>
{/* Header */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
mb: 2,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Box sx={{ color: module.isEnabled ? "primary.main" : "grey.500" }}>
{getIcon(module.icon)}
</Box>
<Typography variant="h6">{module.name}</Typography>
</Box>
<Box>
{module.isCore ? (
<Chip label="Core" size="small" color="info" />
) : (
<Chip label={statusText} size="small" color={statusColor} />
)}
</Box>
</Box>
{/* Descrizione */}
<Typography
variant="body2"
color="text.secondary"
sx={{
mb: 2,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{module.description}
</Typography>
{/* Info subscription */}
{module.subscription && module.isEnabled && (
<Box sx={{ mb: 2 }}>
<Typography variant="caption" color="text.secondary">
Scadenza: {formatDate(module.subscription.endDate)}
{module.subscription.daysRemaining !== undefined &&
module.subscription.daysRemaining <= 30 && (
<Chip
label={getDaysRemainingText(
module.subscription.daysRemaining,
)}
size="small"
color={
module.subscription.daysRemaining <= 7
? "error"
: "warning"
}
sx={{ ml: 1 }}
/>
)}
</Typography>
</Box>
)}
{/* Prezzo */}
<Typography variant="body2">
{formatPrice(module.basePrice)}
<Typography component="span" variant="caption" color="text.secondary">
/anno
</Typography>
</Typography>
</CardContent>
{/* Actions */}
<Box
sx={{
px: 2,
pb: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box>
<Tooltip title="Dettagli">
<IconButton size="small" onClick={onInfo}>
<InfoIcon />
</IconButton>
</Tooltip>
{module.isEnabled &&
!module.isCore &&
module.subscription?.isExpiringSoon && (
<Tooltip title="Rinnova">
<IconButton
size="small"
color="warning"
onClick={onRenew}
disabled={isRenewing}
>
{isRenewing ? <CircularProgress size={20} /> : <RenewIcon />}
</IconButton>
</Tooltip>
)}
</Box>
{!module.isCore && (
<FormControlLabel
control={
<Switch
checked={module.isEnabled}
onChange={onToggle}
color={module.isEnabled ? "success" : "default"}
/>
}
label={module.isEnabled ? "Attivo" : "Disattivo"}
labelPlacement="start"
/>
)}
</Box>
</Card>
);
}