diff --git a/src/backend/Zentral.API/Controllers/ModulesController.cs b/src/backend/Zentral.API/Controllers/ModulesController.cs
index 206e301..d2c9a8a 100644
--- a/src/backend/Zentral.API/Controllers/ModulesController.cs
+++ b/src/backend/Zentral.API/Controllers/ModulesController.cs
@@ -244,7 +244,7 @@ public class ModulesController : ControllerBase
Dependencies = module.GetDependencies().ToList(),
RoutePath = module.RoutePath,
IsAvailable = module.IsAvailable,
- IsEnabled = module.IsCore || (module.Subscription?.IsValid() ?? false),
+ IsEnabled = module.IsCore || ((module.Subscription?.IsEnabled ?? false) && (module.Subscription?.IsValid() ?? false)),
Subscription = module.Subscription != null ? MapSubscriptionToDto(module.Subscription) : null
};
}
diff --git a/src/backend/Zentral.API/Services/ModuleService.cs b/src/backend/Zentral.API/Services/ModuleService.cs
index 78c6386..9964615 100644
--- a/src/backend/Zentral.API/Services/ModuleService.cs
+++ b/src/backend/Zentral.API/Services/ModuleService.cs
@@ -87,7 +87,7 @@ public class ModuleService
if (module.IsCore)
return true;
- return module.Subscription?.IsValid() ?? false;
+ return (module.Subscription?.IsEnabled ?? false) && (module.Subscription?.IsValid() ?? false);
}
///
@@ -200,7 +200,7 @@ public class ModuleService
// Verifica se altri moduli dipendono da questo
var dependentModules = await GetDependentModulesAsync(code);
- var activeDependents = dependentModules.Where(m => m.Subscription?.IsValid() ?? false).ToList();
+ var activeDependents = dependentModules.Where(m => (m.Subscription?.IsEnabled ?? false) && (m.Subscription?.IsValid() ?? false)).ToList();
if (activeDependents.Any())
throw new InvalidOperationException(
$"I seguenti moduli attivi dipendono da questo modulo: {string.Join(", ", activeDependents.Select(m => m.Name))}");
diff --git a/src/backend/Zentral.Domain/Entities/ModuleSubscription.cs b/src/backend/Zentral.Domain/Entities/ModuleSubscription.cs
index 59c81ad..8fa52f1 100644
--- a/src/backend/Zentral.Domain/Entities/ModuleSubscription.cs
+++ b/src/backend/Zentral.Domain/Entities/ModuleSubscription.cs
@@ -72,9 +72,6 @@ public class ModuleSubscription : BaseEntity
///
public bool IsValid()
{
- if (!IsEnabled)
- return false;
-
// Se non c'è data di scadenza, è valido (licenza perpetua o core module)
if (!EndDate.HasValue)
return true;
diff --git a/src/frontend/public/locales/it/translation.json b/src/frontend/public/locales/it/translation.json
index 5e9ae3e..0a6d047 100644
--- a/src/frontend/public/locales/it/translation.json
+++ b/src/frontend/public/locales/it/translation.json
@@ -271,6 +271,7 @@
"disableConfirmSubtext": "I dati inseriti rimarranno nel sistema ma non saranno più accessibili fino alla riattivazione.",
"disable": "Disattiva",
"enable": "Attiva",
+ "purchase": "Acquista",
"details": "Dettagli",
"renew": "Rinnova",
"active": "Attivo",
diff --git a/src/frontend/src/components/ModulePurchaseDialog.tsx b/src/frontend/src/components/ModulePurchaseDialog.tsx
new file mode 100644
index 0000000..b71074c
--- /dev/null
+++ b/src/frontend/src/components/ModulePurchaseDialog.tsx
@@ -0,0 +1,301 @@
+import { useState } from "react";
+import { useMutation } from "@tanstack/react-query";
+import {
+ Box,
+ Typography,
+ Button,
+ ToggleButton,
+ ToggleButtonGroup,
+ Alert,
+ CircularProgress,
+ Chip,
+ Divider,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Paper,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+} from "@mui/material";
+import {
+ CheckCircle as CheckIcon,
+ ShoppingCart as CartIcon,
+ CalendarMonth as MonthlyIcon,
+ CalendarToday as AnnualIcon,
+ Warning as WarningIcon,
+} from "@mui/icons-material";
+import { useTranslation } from "react-i18next";
+import { useModules } from "../contexts/ModuleContext";
+import {
+ ModuleDto,
+ SubscriptionType,
+ formatPrice,
+ getSubscriptionTypeName,
+} from "../types/module";
+
+interface ModulePurchaseDialogProps {
+ module: ModuleDto | null;
+ open: boolean;
+ onClose: () => void;
+}
+
+export default function ModulePurchaseDialog({
+ module,
+ open,
+ onClose,
+}: ModulePurchaseDialogProps) {
+ const { enableModule, isModuleEnabled } = useModules();
+ const { t } = useTranslation();
+
+ const [subscriptionType, setSubscriptionType] = useState(
+ SubscriptionType.Annual
+ );
+ const [autoRenew, setAutoRenew] = useState(true);
+
+ // Mutation per attivare il modulo
+ const enableMutation = useMutation({
+ mutationFn: async () => {
+ if (!module?.code) throw new Error("Codice modulo mancante");
+ return enableModule(module.code, {
+ subscriptionType,
+ autoRenew,
+ });
+ },
+ onSuccess: () => {
+ onClose();
+ },
+ });
+
+ 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
+ ? t("modules.admin.perMonth")
+ : t("modules.admin.perYear");
+
+ // 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 (
+
+ );
+}
+
+// Helper per ottenere le funzionalità di un modulo
+function getModuleFeatures(code: string, t: (key: string) => string): string[] {
+ const featureKeys = [0, 1, 2, 3, 4, 5];
+ if (["warehouse", "purchases", "sales", "production", "quality"].includes(code)) {
+ return featureKeys.map((i) => t(`modules.features.${code}.${i}`));
+ }
+ return [t("modules.features.default")];
+}
diff --git a/src/frontend/src/pages/ModulePurchasePage.tsx b/src/frontend/src/pages/ModulePurchasePage.tsx
index 1862693..8fbcbd6 100644
--- a/src/frontend/src/pages/ModulePurchasePage.tsx
+++ b/src/frontend/src/pages/ModulePurchasePage.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { useParams, useNavigate, useLocation } from "react-router-dom";
+import { useParams, useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import {
Box,
@@ -38,7 +38,6 @@ import {
export default function ModulePurchasePage() {
const { code } = useParams<{ code: string }>();
const navigate = useNavigate();
- const location = useLocation();
const module = useModule(code || "");
const { enableModule, isModuleEnabled } = useModules();
const { t } = useTranslation();
@@ -58,9 +57,8 @@ export default function ModulePurchasePage() {
});
},
onSuccess: () => {
- // Redirect alla pagina originale o alla home del modulo
- const from = (location.state as { from?: string })?.from;
- navigate(from || module?.routePath || "/");
+ // Redirect alla pagina dei moduli
+ navigate("/modules");
},
});
diff --git a/src/frontend/src/pages/ModulesAdminPage.tsx b/src/frontend/src/pages/ModulesAdminPage.tsx
index 039c8b9..be80591 100644
--- a/src/frontend/src/pages/ModulesAdminPage.tsx
+++ b/src/frontend/src/pages/ModulesAdminPage.tsx
@@ -1,5 +1,4 @@
import { useState } from "react";
-import { useNavigate } from "react-router-dom";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
Box,
@@ -28,8 +27,10 @@ import {
Warning as WarningIcon,
Schedule as ScheduleIcon,
Autorenew as RenewIcon,
+ ShoppingCart,
} from "@mui/icons-material";
import * as Icons from "@mui/icons-material";
+import { AxiosError } from "axios";
import { useTranslation } from "react-i18next";
import { useModules } from "../contexts/ModuleContext";
import { moduleService } from "../services/moduleService";
@@ -40,13 +41,15 @@ import {
getDaysRemainingText,
getSubscriptionStatusColor,
getSubscriptionStatusText,
+ SubscriptionType,
} from "../types/module";
+import ModulePurchaseDialog from "../components/ModulePurchaseDialog";
export default function ModulesAdminPage() {
- const navigate = useNavigate();
const { t } = useTranslation();
const { modules, isLoading, refreshModules } = useModules();
const [selectedModule, setSelectedModule] = useState(null);
+ const [purchaseModule, setPurchaseModule] = useState(null);
const [confirmDisable, setConfirmDisable] = useState(null);
// Query per moduli in scadenza
@@ -64,6 +67,18 @@ export default function ModulesAdminPage() {
},
});
+ // Mutation per attivare modulo
+ const enableMutation = useMutation({
+ mutationFn: (code: string) =>
+ moduleService.enable(code, {
+ subscriptionType: SubscriptionType.Annual,
+ autoRenew: true,
+ }),
+ onSuccess: () => {
+ refreshModules();
+ },
+ });
+
// Mutation per rinnovare subscription
const renewMutation = useMutation({
mutationFn: (code: string) => moduleService.renewSubscription(code),
@@ -166,9 +181,13 @@ export default function ModulesAdminPage() {
if (module.isEnabled && !module.isCore) {
setConfirmDisable(module.code);
} else if (!module.isEnabled) {
- navigate(`/modules/purchase/${module.code}`);
+ // Se ha una subscription valida, lo abilitiamo direttamente
+ if (module.subscription?.isValid) {
+ enableMutation.mutate(module.code);
+ }
}
}}
+ onPurchase={() => setPurchaseModule(module)}
onInfo={() => setSelectedModule(module)}
onRenew={() => renewMutation.mutate(module.code)}
isRenewing={renewMutation.isPending}
@@ -332,7 +351,9 @@ export default function ModulesAdminPage() {
{disableMutation.isError && (
- {(disableMutation.error as Error)?.message ||
+ {(disableMutation.error as AxiosError<{ message: string }>)?.response
+ ?.data?.message ||
+ (disableMutation.error as Error)?.message ||
t("common.error")}
)}
@@ -358,7 +379,17 @@ export default function ModulesAdminPage() {
-
+
+ {/* Dialog acquisto modulo */}
+ {
+ setPurchaseModule(null);
+ refreshModules();
+ }}
+ />
+
);
}
@@ -366,6 +397,7 @@ export default function ModulesAdminPage() {
interface ModuleCardProps {
module: ModuleDto;
onToggle: () => void;
+ onPurchase: () => void;
onInfo: () => void;
onRenew: () => void;
isRenewing: boolean;
@@ -376,6 +408,7 @@ interface ModuleCardProps {
function ModuleCard({
module,
onToggle,
+ onPurchase,
onInfo,
onRenew,
isRenewing,
@@ -421,6 +454,15 @@ function ModuleCard({
{module.isCore ? (
+ ) : !module.subscription?.isValid ? (
+ }
+ onClick={onPurchase}
+ sx={{ cursor: "pointer" }}
+ />
) : (
)}
@@ -514,9 +556,14 @@ function ModuleCard({
checked={module.isEnabled}
onChange={onToggle}
color={module.isEnabled ? "success" : "default"}
+ disabled={!module.subscription?.isValid}
/>
}
- label={module.isEnabled ? t("modules.admin.active") : t("modules.admin.inactive")}
+ label={
+ module.isEnabled
+ ? t("modules.admin.active")
+ : t("modules.admin.inactive")
+ }
labelPlacement="start"
/>
)}