feat: bundle and activate module dependencies during purchase, updating dialog UI and adding activation error handling.
This commit is contained in:
@@ -22,10 +22,9 @@ import {
|
||||
} from "@mui/material";
|
||||
import {
|
||||
CheckCircle as CheckIcon,
|
||||
ShoppingCart as CartIcon,
|
||||
ShoppingCart as ShoppingCartIcon,
|
||||
CalendarMonth as MonthlyIcon,
|
||||
CalendarToday as AnnualIcon,
|
||||
Warning as WarningIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModules } from "../contexts/ModuleContext";
|
||||
@@ -47,7 +46,7 @@ export default function ModulePurchaseDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: ModulePurchaseDialogProps) {
|
||||
const { enableModule, isModuleEnabled } = useModules();
|
||||
const { modules, enableModule, isModuleEnabled } = useModules();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [subscriptionType, setSubscriptionType] = useState<SubscriptionType>(
|
||||
@@ -55,14 +54,96 @@ export default function ModulePurchaseDialog({
|
||||
);
|
||||
const [autoRenew, setAutoRenew] = useState(true);
|
||||
|
||||
// Mutation per attivare il modulo
|
||||
// Trova tutte le dipendenze mancanti ricorsivamente
|
||||
const getAllMissingDependencies = (
|
||||
moduleCode: string,
|
||||
checked: Set<string> = new Set()
|
||||
): ModuleDto[] => {
|
||||
if (checked.has(moduleCode)) return [];
|
||||
checked.add(moduleCode);
|
||||
|
||||
const module = modules.find((m) => m.code === moduleCode);
|
||||
if (!module) return [];
|
||||
|
||||
let missing: ModuleDto[] = [];
|
||||
|
||||
// Se il modulo corrente non ha una subscription valida, va aggiunto
|
||||
// (ma non se è il modulo target iniziale, che gestiamo a parte)
|
||||
if (
|
||||
module.code !== (module?.code || "") &&
|
||||
!module.subscription?.isValid &&
|
||||
!module.isCore
|
||||
) {
|
||||
missing.push(module);
|
||||
}
|
||||
|
||||
// Controlla le dipendenze di questo modulo
|
||||
for (const depCode of module.dependencies) {
|
||||
// Se la dipendenza è già valida, non serve controllarla ricorsivamente
|
||||
// (a meno che non vogliamo essere sicuri al 100%, ma assumiamo che se è valida è ok)
|
||||
// Però se è valida ma disabilitata? Il backend richiede che sia ABILITATA.
|
||||
// Quindi controlliamo isModuleEnabled(depCode).
|
||||
if (!isModuleEnabled(depCode)) {
|
||||
// Se non è abilitata, potrebbe essere valida ma spenta, o non valida.
|
||||
// Se è valida ma spenta, dobbiamo solo abilitarla? O il backend lo fa?
|
||||
// Il backend EnableModule richiede che le dipendenze siano ATTIVE.
|
||||
// Quindi se è valida ma spenta, dobbiamo riattivarla.
|
||||
// Se non è valida, dobbiamo acquistarla.
|
||||
// Per semplicità, consideriamo "missing" qualsiasi cosa non abilitata.
|
||||
// Ma dobbiamo distinguere tra "da acquistare" e "da attivare".
|
||||
// Qui ci concentriamo sull'acquisto.
|
||||
const depModule = modules.find((m) => m.code === depCode);
|
||||
if (depModule) {
|
||||
if (!depModule.subscription?.isValid && !depModule.isCore) {
|
||||
missing.push(depModule);
|
||||
}
|
||||
// Ricorsione
|
||||
missing = [...missing, ...getAllMissingDependencies(depCode, checked)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rimuovi duplicati
|
||||
return Array.from(new Set(missing.map((m) => m.code)))
|
||||
.map((code) => modules.find((m) => m.code === code)!)
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const missingDependencies = module ? getAllMissingDependencies(module.code) : [];
|
||||
const modulesToPurchase = module ? [module, ...missingDependencies] : [];
|
||||
|
||||
// Calcola il prezzo totale
|
||||
const calculateTotal = (type: SubscriptionType) => {
|
||||
return modulesToPurchase.reduce((total, m) => {
|
||||
return (
|
||||
total +
|
||||
(type === SubscriptionType.Monthly ? m.monthlyPrice : m.basePrice)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const totalPrice = calculateTotal(subscriptionType);
|
||||
|
||||
// Mutation per attivare il modulo e le dipendenze
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!module?.code) throw new Error("Codice modulo mancante");
|
||||
return enableModule(module.code, {
|
||||
|
||||
// Attiva prima le dipendenze (in ordine inverso di dipendenza sarebbe meglio,
|
||||
// ma qui le abbiamo piatte. Dobbiamo ordinarle o sperare che l'ordine sia corretto.
|
||||
// I moduli base (es. warehouse) dovrebbero essere attivati prima.
|
||||
// Ordiniamo per SortOrder o livello di dipendenza.
|
||||
// Assumiamo che SortOrder rifletta la gerarchia (Warehouse=10, Purchases=20...).
|
||||
const sortedModules = [...modulesToPurchase].sort(
|
||||
(a, b) => a.sortOrder - b.sortOrder
|
||||
);
|
||||
|
||||
for (const m of sortedModules) {
|
||||
await enableModule(m.code, {
|
||||
subscriptionType,
|
||||
autoRenew,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
@@ -71,11 +152,7 @@ export default function ModulePurchaseDialog({
|
||||
|
||||
if (!module) return null;
|
||||
|
||||
// Calcola il prezzo in base al tipo di subscription
|
||||
const price =
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? module.monthlyPrice
|
||||
: module.basePrice;
|
||||
|
||||
|
||||
const priceLabel =
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
@@ -83,14 +160,14 @@ export default function ModulePurchaseDialog({
|
||||
: t("modules.admin.perYear");
|
||||
|
||||
// Calcola risparmio annuale
|
||||
const annualSavings = module.monthlyPrice * 12 - module.basePrice;
|
||||
const savingsPercent = Math.round(
|
||||
(annualSavings / (module.monthlyPrice * 12)) * 100
|
||||
);
|
||||
const annualSavings = modulesToPurchase.reduce((acc, m) => {
|
||||
return acc + (m.monthlyPrice * 12 - m.basePrice);
|
||||
}, 0);
|
||||
|
||||
// Verifica dipendenze mancanti
|
||||
const missingDependencies = module.dependencies.filter(
|
||||
(dep) => !isModuleEnabled(dep)
|
||||
const totalMonthlyPrice = modulesToPurchase.reduce((acc, m) => acc + m.monthlyPrice, 0);
|
||||
|
||||
const savingsPercent = Math.round(
|
||||
(annualSavings / (totalMonthlyPrice * 12)) * 100
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -109,19 +186,27 @@ export default function ModulePurchaseDialog({
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Alert dipendenze mancanti */}
|
||||
{/* Alert dipendenze incluse */}
|
||||
{missingDependencies.length > 0 && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||
<Alert severity="info" sx={{ mb: 3 }} icon={<ShoppingCartIcon />}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{t("modules.admin.missingDependencies")}
|
||||
{t("modules.admin.dependenciesIncluded")}
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
{t("modules.admin.dependenciesIncludedText")}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||
{missingDependencies.map((dep) => (
|
||||
<Chip
|
||||
key={dep}
|
||||
label={dep}
|
||||
key={dep.code}
|
||||
label={`${dep.name} (+${formatPrice(
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? dep.monthlyPrice
|
||||
: dep.basePrice
|
||||
)})`}
|
||||
size="small"
|
||||
color="warning"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -147,7 +232,7 @@ export default function ModulePurchaseDialog({
|
||||
{t("modules.admin.monthly")}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.monthlyPrice)}
|
||||
{formatPrice(calculateTotal(SubscriptionType.Monthly))}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
@@ -165,7 +250,7 @@ export default function ModulePurchaseDialog({
|
||||
{t("modules.admin.annual")}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.basePrice)}
|
||||
{formatPrice(calculateTotal(SubscriptionType.Annual))}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
@@ -218,8 +303,31 @@ export default function ModulePurchaseDialog({
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography>{t("modules.admin.module")} {module.name}</Typography>
|
||||
<Typography>{formatPrice(price)}</Typography>
|
||||
<Typography>
|
||||
{formatPrice(
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? module.monthlyPrice
|
||||
: module.basePrice
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{missingDependencies.map((dep) => (
|
||||
<Box
|
||||
key={dep.code}
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography color="text.secondary">
|
||||
+ {dep.name}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
{formatPrice(
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? dep.monthlyPrice
|
||||
: dep.basePrice
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
@@ -233,7 +341,7 @@ export default function ModulePurchaseDialog({
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography variant="h6">{t("modules.admin.total")}</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(price)}
|
||||
{formatPrice(totalPrice)}
|
||||
{priceLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -274,13 +382,11 @@ export default function ModulePurchaseDialog({
|
||||
enableMutation.isPending ? (
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
) : (
|
||||
<CartIcon />
|
||||
<ShoppingCartIcon />
|
||||
)
|
||||
}
|
||||
onClick={() => enableMutation.mutate()}
|
||||
disabled={
|
||||
enableMutation.isPending || missingDependencies.length > 0
|
||||
}
|
||||
disabled={enableMutation.isPending}
|
||||
>
|
||||
{enableMutation.isPending
|
||||
? t("modules.admin.activating")
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function ModulesAdminPage() {
|
||||
const [selectedModule, setSelectedModule] = useState<ModuleDto | null>(null);
|
||||
const [purchaseModule, setPurchaseModule] = useState<ModuleDto | null>(null);
|
||||
const [confirmDisable, setConfirmDisable] = useState<string | null>(null);
|
||||
const [enableError, setEnableError] = useState<string | null>(null);
|
||||
|
||||
// Query per moduli in scadenza
|
||||
const { data: expiringModules = [] } = useQuery({
|
||||
@@ -77,6 +78,13 @@ export default function ModulesAdminPage() {
|
||||
onSuccess: () => {
|
||||
refreshModules();
|
||||
},
|
||||
onError: (error) => {
|
||||
setEnableError(
|
||||
(error as AxiosError<{ message: string }>)?.response?.data?.message ||
|
||||
(error as Error)?.message ||
|
||||
t("common.error")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation per rinnovare subscription
|
||||
@@ -380,6 +388,26 @@ export default function ModulesAdminPage() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog errore attivazione */}
|
||||
<Dialog
|
||||
open={!!enableError}
|
||||
onClose={() => setEnableError(null)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{t("common.error")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{enableError}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setEnableError(null)}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog acquisto modulo */}
|
||||
<ModulePurchaseDialog
|
||||
module={purchaseModule}
|
||||
|
||||
Reference in New Issue
Block a user