diff --git a/src/frontend/src/components/ModulePurchaseDialog.tsx b/src/frontend/src/components/ModulePurchaseDialog.tsx index b71074c..ed07e81 100644 --- a/src/frontend/src/components/ModulePurchaseDialog.tsx +++ b/src/frontend/src/components/ModulePurchaseDialog.tsx @@ -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( @@ -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 = 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, { - subscriptionType, - autoRenew, - }); + + // 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({ - {/* Alert dipendenze mancanti */} + {/* Alert dipendenze incluse */} {missingDependencies.length > 0 && ( - }> + }> - {t("modules.admin.missingDependencies")} + {t("modules.admin.dependenciesIncluded")} + + + {t("modules.admin.dependenciesIncludedText")} {missingDependencies.map((dep) => ( ))} @@ -147,7 +232,7 @@ export default function ModulePurchaseDialog({ {t("modules.admin.monthly")} - {formatPrice(module.monthlyPrice)} + {formatPrice(calculateTotal(SubscriptionType.Monthly))} - {formatPrice(module.basePrice)} + {formatPrice(calculateTotal(SubscriptionType.Annual))} {t("modules.admin.module")} {module.name} - {formatPrice(price)} + + {formatPrice( + subscriptionType === SubscriptionType.Monthly + ? module.monthlyPrice + : module.basePrice + )} + + {missingDependencies.map((dep) => ( + + + + {dep.name} + + + {formatPrice( + subscriptionType === SubscriptionType.Monthly + ? dep.monthlyPrice + : dep.basePrice + )} + + + ))} @@ -233,7 +341,7 @@ export default function ModulePurchaseDialog({ {t("modules.admin.total")} - {formatPrice(price)} + {formatPrice(totalPrice)} {priceLabel} @@ -274,13 +382,11 @@ export default function ModulePurchaseDialog({ enableMutation.isPending ? ( ) : ( - + ) } onClick={() => enableMutation.mutate()} - disabled={ - enableMutation.isPending || missingDependencies.length > 0 - } + disabled={enableMutation.isPending} > {enableMutation.isPending ? t("modules.admin.activating") diff --git a/src/frontend/src/pages/ModulesAdminPage.tsx b/src/frontend/src/pages/ModulesAdminPage.tsx index be80591..72c473d 100644 --- a/src/frontend/src/pages/ModulesAdminPage.tsx +++ b/src/frontend/src/pages/ModulesAdminPage.tsx @@ -51,6 +51,7 @@ export default function ModulesAdminPage() { const [selectedModule, setSelectedModule] = useState(null); const [purchaseModule, setPurchaseModule] = useState(null); const [confirmDisable, setConfirmDisable] = useState(null); + const [enableError, setEnableError] = useState(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() { + {/* Dialog errore attivazione */} + setEnableError(null)} + maxWidth="xs" + fullWidth + > + {t("common.error")} + + + {enableError} + + + + + + + {/* Dialog acquisto modulo */}