-
This commit is contained in:
386
frontend/src/pages/ModulePurchasePage.tsx
Normal file
386
frontend/src/pages/ModulePurchasePage.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
CheckCircle as CheckIcon,
|
||||
ArrowBack as BackIcon,
|
||||
ShoppingCart as CartIcon,
|
||||
CalendarMonth as MonthlyIcon,
|
||||
CalendarToday as AnnualIcon,
|
||||
Warning as WarningIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useModule, useModules } from "../contexts/ModuleContext";
|
||||
import {
|
||||
SubscriptionType,
|
||||
formatPrice,
|
||||
getSubscriptionTypeName,
|
||||
} from "../types/module";
|
||||
|
||||
export default function ModulePurchasePage() {
|
||||
const { code } = useParams<{ code: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const module = useModule(code || "");
|
||||
const { enableModule, isModuleEnabled } = useModules();
|
||||
|
||||
const [subscriptionType, setSubscriptionType] = useState<SubscriptionType>(
|
||||
SubscriptionType.Annual,
|
||||
);
|
||||
const [autoRenew, setAutoRenew] = useState(true);
|
||||
|
||||
// Mutation per attivare il modulo
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!code) throw new Error("Codice modulo mancante");
|
||||
return enableModule(code, {
|
||||
subscriptionType,
|
||||
autoRenew,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Redirect alla pagina originale o alla home del modulo
|
||||
const from = (location.state as { from?: string })?.from;
|
||||
navigate(from || module?.routePath || "/");
|
||||
},
|
||||
});
|
||||
|
||||
// Se il modulo è già abilitato, redirect
|
||||
if (code && isModuleEnabled(code)) {
|
||||
navigate(module?.routePath || "/");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!module) {
|
||||
return (
|
||||
<Box sx={{ p: 3, textAlign: "center" }}>
|
||||
<Typography variant="h5" color="error" gutterBottom>
|
||||
Modulo non trovato
|
||||
</Typography>
|
||||
<Typography color="text.secondary" paragraph>
|
||||
Il modulo richiesto non esiste.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<BackIcon />}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
Torna alla Home
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Calcola il prezzo in base al tipo di subscription
|
||||
const price =
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? module.monthlyPrice
|
||||
: module.basePrice;
|
||||
|
||||
const priceLabel =
|
||||
subscriptionType === SubscriptionType.Monthly ? "/mese" : "/anno";
|
||||
|
||||
// Calcola risparmio annuale
|
||||
const annualSavings = module.monthlyPrice * 12 - module.basePrice;
|
||||
const savingsPercent = Math.round(
|
||||
(annualSavings / (module.monthlyPrice * 12)) * 100,
|
||||
);
|
||||
|
||||
// Verifica dipendenze mancanti
|
||||
const missingDependencies = module.dependencies.filter(
|
||||
(dep) => !isModuleEnabled(dep),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, maxWidth: 800, mx: "auto" }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Button
|
||||
startIcon={<BackIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Indietro
|
||||
</Button>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Attiva Modulo
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Scegli il piano di abbonamento per il modulo {module.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Alert dipendenze mancanti */}
|
||||
{missingDependencies.length > 0 && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Questo modulo richiede i seguenti moduli che non sono attivi:
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||
{missingDependencies.map((dep) => (
|
||||
<Chip
|
||||
key={dep}
|
||||
label={dep}
|
||||
size="small"
|
||||
color="warning"
|
||||
onClick={() => navigate(`/modules/purchase/${dep}`)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Card principale */}
|
||||
<Card elevation={3}>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
{/* Info modulo */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{module.name}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" paragraph>
|
||||
{module.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Selezione tipo abbonamento */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight="medium">
|
||||
Tipo di abbonamento
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={subscriptionType}
|
||||
exclusive
|
||||
onChange={(_, value) => value && setSubscriptionType(value)}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<ToggleButton value={SubscriptionType.Monthly}>
|
||||
<Box sx={{ py: 1, textAlign: "center" }}>
|
||||
<MonthlyIcon sx={{ mb: 0.5 }} />
|
||||
<Typography variant="body2" display="block">
|
||||
Mensile
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.monthlyPrice)}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
/mese
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={SubscriptionType.Annual}>
|
||||
<Box sx={{ py: 1, textAlign: "center" }}>
|
||||
<AnnualIcon sx={{ mb: 0.5 }} />
|
||||
<Typography variant="body2" display="block">
|
||||
Annuale
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.basePrice)}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
/anno
|
||||
</Typography>
|
||||
</Typography>
|
||||
{savingsPercent > 0 && (
|
||||
<Chip
|
||||
label={`Risparmi ${savingsPercent}%`}
|
||||
size="small"
|
||||
color="success"
|
||||
sx={{ mt: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{/* Opzione auto-rinnovo */}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<ToggleButton
|
||||
value="autoRenew"
|
||||
selected={autoRenew}
|
||||
onChange={() => setAutoRenew(!autoRenew)}
|
||||
size="small"
|
||||
>
|
||||
{autoRenew ? <CheckIcon /> : null}
|
||||
</ToggleButton>
|
||||
<Typography variant="body2">
|
||||
Rinnovo automatico alla scadenza
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Riepilogo */}
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{ p: 2, mb: 3, bgcolor: "action.hover" }}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Riepilogo ordine
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography>Modulo {module.name}</Typography>
|
||||
<Typography>{formatPrice(price)}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography color="text.secondary">
|
||||
Abbonamento{" "}
|
||||
{getSubscriptionTypeName(subscriptionType).toLowerCase()}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">{priceLabel}</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography variant="h6">Totale</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(price)}
|
||||
{priceLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Errore */}
|
||||
{enableMutation.isError && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{(enableMutation.error as Error)?.message ||
|
||||
"Errore durante l'attivazione del modulo"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Pulsante attivazione */}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
startIcon={
|
||||
enableMutation.isPending ? (
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
) : (
|
||||
<CartIcon />
|
||||
)
|
||||
}
|
||||
onClick={() => enableMutation.mutate()}
|
||||
disabled={
|
||||
enableMutation.isPending || missingDependencies.length > 0
|
||||
}
|
||||
>
|
||||
{enableMutation.isPending
|
||||
? "Attivazione in corso..."
|
||||
: "Attiva Modulo"}
|
||||
</Button>
|
||||
|
||||
{/* Note */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
textAlign="center"
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Potrai disattivare il modulo in qualsiasi momento dalle
|
||||
impostazioni. I dati inseriti rimarranno disponibili.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Funzionalità incluse */}
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Funzionalità incluse
|
||||
</Typography>
|
||||
<List dense>
|
||||
{getModuleFeatures(module.code).map((feature, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<CheckIcon color="success" fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={feature} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper per ottenere le funzionalità di un modulo
|
||||
function getModuleFeatures(code: string): string[] {
|
||||
const features: Record<string, string[]> = {
|
||||
warehouse: [
|
||||
"Gestione anagrafica articoli",
|
||||
"Movimenti di magazzino (carico/scarico)",
|
||||
"Giacenze in tempo reale",
|
||||
"Valorizzazione scorte (FIFO, LIFO, medio ponderato)",
|
||||
"Inventario e rettifiche",
|
||||
"Report giacenze e movimenti",
|
||||
],
|
||||
purchases: [
|
||||
"Gestione ordini a fornitore",
|
||||
"DDT di entrata",
|
||||
"Fatture passive",
|
||||
"Scadenziario pagamenti",
|
||||
"Analisi acquisti per fornitore/articolo",
|
||||
"Storico prezzi di acquisto",
|
||||
],
|
||||
sales: [
|
||||
"Gestione ordini cliente",
|
||||
"DDT di uscita",
|
||||
"Fatturazione elettronica",
|
||||
"Scadenziario incassi",
|
||||
"Analisi vendite per cliente/articolo",
|
||||
"Listini prezzi",
|
||||
],
|
||||
production: [
|
||||
"Distinte base multilivello",
|
||||
"Cicli di lavoro",
|
||||
"Ordini di produzione",
|
||||
"Pianificazione MRP",
|
||||
"Avanzamento produzione",
|
||||
"Costi di produzione",
|
||||
],
|
||||
quality: [
|
||||
"Piani di controllo",
|
||||
"Registrazione controlli",
|
||||
"Gestione non conformità",
|
||||
"Azioni correttive/preventive",
|
||||
"Certificazioni e audit",
|
||||
"Statistiche qualità",
|
||||
],
|
||||
};
|
||||
|
||||
return features[code] || ["Funzionalità complete del modulo"];
|
||||
}
|
||||
Reference in New Issue
Block a user