Compare commits

...

5 Commits

10 changed files with 536 additions and 18 deletions

View File

@@ -25,3 +25,5 @@ Il gestionale è multilingua e gestisce la funzione i18n, per ora le lingue gest
Tutta la parte database deve essere gestita sempre in code first, non voglio vedere query SQL RAW da nessuna parte e tutto il database deve sempre essere gestito a migrazioni con ef migration, come database si deve usare sqlite per lo sviluppo e mysql per la produzione. Tutta la parte database deve essere gestita sempre in code first, non voglio vedere query SQL RAW da nessuna parte e tutto il database deve sempre essere gestito a migrazioni con ef migration, come database si deve usare sqlite per lo sviluppo e mysql per la produzione.
Prima ancora di pianificare la nuova attività, però, dobbiamo sempre verificare che attualmente l'applicazione funzioni quindi va avviata e testata preliminarmente nelle sue funzioni di base (e tenuta avviata sempre finchè si sviluppa). Prima ancora di pianificare la nuova attività, però, dobbiamo sempre verificare che attualmente l'applicazione funzioni quindi va avviata e testata preliminarmente nelle sue funzioni di base (e tenuta avviata sempre finchè si sviluppa).
Se il backend restituisce un errore specifico, questo deve essere chiaramente notificato all'utente, invece di un generico "Errore".

View File

@@ -10,3 +10,5 @@ File riassuntivo dello stato di sviluppo di Zentral.
- Ristrutturazione interfaccia: Sidebar a 2 livelli, Tabs, SearchBar. - Ristrutturazione interfaccia: Sidebar a 2 livelli, Tabs, SearchBar.
- [2025-12-03 Backend Fix](./devlog/2025-12-03_backend_fix.md) - **Completato** - [2025-12-03 Backend Fix](./devlog/2025-12-03_backend_fix.md) - **Completato**
- Fix mancata migrazione database e avvio backend. - Fix mancata migrazione database e avvio backend.
- [2025-12-03 Module Management Refinement](./devlog/2025-12-03_module_management.md) - **Completato**
- Refinement logica attivazione/acquisto moduli, gestione dipendenze e UI.

View File

@@ -0,0 +1,36 @@
# Refinement Gestione Moduli
**Data:** 2025-12-03
**Stato:** Completato
## Obiettivo
Raffinare il flusso di attivazione e acquisto dei moduli, separando chiaramente lo stato di "acquisto" (subscription valida) dallo stato di "attivazione" (abilitazione utente). Implementare la gestione delle dipendenze sia in fase di acquisto (bulk purchase) che di disattivazione.
## Modifiche Apportate
### Backend (.NET)
- **Entità `ModuleSubscription`**: Modificato il metodo `IsValid()` per controllare *solo* la validità temporale della subscription (date), ignorando il flag `IsEnabled`.
- **Servizio `ModuleService`**:
- Aggiornato `IsModuleEnabledAsync` per verificare sia `IsEnabled` (utente) che `IsValid` (date).
- Aggiornato `DisableModuleAsync` per permettere la disattivazione di un modulo se i suoi dipendenti sono già stati disattivati dall'utente (anche se validi).
- Aggiornato `GetActiveModulesAsync` per filtrare i moduli visibili nel menu solo se sono sia validi che abilitati dall'utente.
- **Controller `ModulesController`**: Aggiornato il mapping DTO per riflettere correttamente lo stato `IsEnabled` verso il frontend.
### Frontend (React)
- **Nuovo Componente `ModulePurchaseDialog`**:
- Gestisce l'acquisto dei moduli.
- **Dependency Resolution**: Calcola ricorsivamente le dipendenze mancanti e le include nel totale dell'acquisto.
- Mostra un riepilogo chiaro con i costi aggiuntivi per le dipendenze.
- **Pagina `ModulesAdminPage`**:
- Aggiornata la UI delle card per mostrare il tasto "Acquista" solo se la subscription è scaduta/inesistente.
- Se il modulo è valido ma disattivato, mostra lo stato "Disattivato" e permette la riattivazione tramite toggle.
- Implementata gestione errori avanzata: mostra messaggi specifici dal backend (es. dipendenze attive che impediscono la disattivazione).
- **Sidebar**: Verifica che i moduli disattivati non compaiano nel menu laterale.
## Verifica
- [x] Il tasto "Acquista" appare solo per moduli non posseduti o scaduti.
- [x] Disattivare un modulo valido lo mantiene "Disattivato" ma non richiede nuovo acquisto.
- [x] Disattivare un modulo con dipendenze attive mostra un errore specifico.
- [x] Disattivare un modulo con dipendenze disattivate (ma valide) è permesso.
- [x] I moduli disattivati scompaiono dal menu laterale.
- [x] L'acquisto di un modulo include automaticamente le dipendenze mancanti nel prezzo e nell'attivazione.

View File

@@ -244,7 +244,7 @@ public class ModulesController : ControllerBase
Dependencies = module.GetDependencies().ToList(), Dependencies = module.GetDependencies().ToList(),
RoutePath = module.RoutePath, RoutePath = module.RoutePath,
IsAvailable = module.IsAvailable, 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 Subscription = module.Subscription != null ? MapSubscriptionToDto(module.Subscription) : null
}; };
} }

View File

@@ -60,7 +60,7 @@ public class ModuleService
.ThenBy(m => m.Name) .ThenBy(m => m.Name)
.ToListAsync(); .ToListAsync();
return modules.Where(m => m.IsCore || (m.Subscription?.IsValid() ?? false)).ToList(); return modules.Where(m => m.IsCore || ((m.Subscription?.IsEnabled ?? false) && (m.Subscription?.IsValid() ?? false))).ToList();
}) ?? new List<AppModule>(); }) ?? new List<AppModule>();
} }
@@ -87,7 +87,7 @@ public class ModuleService
if (module.IsCore) if (module.IsCore)
return true; return true;
return module.Subscription?.IsValid() ?? false; return (module.Subscription?.IsEnabled ?? false) && (module.Subscription?.IsValid() ?? false);
} }
/// <summary> /// <summary>
@@ -200,7 +200,7 @@ public class ModuleService
// Verifica se altri moduli dipendono da questo // Verifica se altri moduli dipendono da questo
var dependentModules = await GetDependentModulesAsync(code); 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()) if (activeDependents.Any())
throw new InvalidOperationException( throw new InvalidOperationException(
$"I seguenti moduli attivi dipendono da questo modulo: {string.Join(", ", activeDependents.Select(m => m.Name))}"); $"I seguenti moduli attivi dipendono da questo modulo: {string.Join(", ", activeDependents.Select(m => m.Name))}");

View File

@@ -72,9 +72,6 @@ public class ModuleSubscription : BaseEntity
/// </summary> /// </summary>
public bool IsValid() public bool IsValid()
{ {
if (!IsEnabled)
return false;
// Se non c'è data di scadenza, è valido (licenza perpetua o core module) // Se non c'è data di scadenza, è valido (licenza perpetua o core module)
if (!EndDate.HasValue) if (!EndDate.HasValue)
return true; return true;

View File

@@ -271,6 +271,7 @@
"disableConfirmSubtext": "I dati inseriti rimarranno nel sistema ma non saranno più accessibili fino alla riattivazione.", "disableConfirmSubtext": "I dati inseriti rimarranno nel sistema ma non saranno più accessibili fino alla riattivazione.",
"disable": "Disattiva", "disable": "Disattiva",
"enable": "Attiva", "enable": "Attiva",
"purchase": "Acquista",
"details": "Dettagli", "details": "Dettagli",
"renew": "Rinnova", "renew": "Rinnova",
"active": "Attivo", "active": "Attivo",

View File

@@ -0,0 +1,407 @@
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 ShoppingCartIcon,
CalendarMonth as MonthlyIcon,
CalendarToday as AnnualIcon,
} 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 { modules, enableModule, isModuleEnabled } = useModules();
const { t } = useTranslation();
const [subscriptionType, setSubscriptionType] = useState<SubscriptionType>(
SubscriptionType.Annual
);
const [autoRenew, setAutoRenew] = useState(true);
// 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");
// 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();
},
});
if (!module) return null;
const priceLabel =
subscriptionType === SubscriptionType.Monthly
? t("modules.admin.perMonth")
: t("modules.admin.perYear");
// Calcola risparmio annuale
const annualSavings = modulesToPurchase.reduce((acc, m) => {
return acc + (m.monthlyPrice * 12 - m.basePrice);
}, 0);
const totalMonthlyPrice = modulesToPurchase.reduce((acc, m) => acc + m.monthlyPrice, 0);
const savingsPercent = Math.round(
(annualSavings / (totalMonthlyPrice * 12)) * 100
);
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h6">
{t("modules.admin.purchaseTitle")} - {module.name}
</Typography>
</Box>
</DialogTitle>
<DialogContent dividers>
<Box sx={{ mb: 3 }}>
<Typography color="text.secondary" paragraph>
{module.description}
</Typography>
</Box>
{/* Alert dipendenze incluse */}
{missingDependencies.length > 0 && (
<Alert severity="info" sx={{ mb: 3 }} icon={<ShoppingCartIcon />}>
<Typography variant="subtitle2" gutterBottom>
{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.code}
label={`${dep.name} (+${formatPrice(
subscriptionType === SubscriptionType.Monthly
? dep.monthlyPrice
: dep.basePrice
)})`}
size="small"
color="info"
variant="outlined"
/>
))}
</Box>
</Alert>
)}
{/* Selezione tipo abbonamento */}
<Box sx={{ mb: 4 }}>
<Typography variant="subtitle1" gutterBottom fontWeight="medium">
{t("modules.admin.subscriptionType")}
</Typography>
<ToggleButtonGroup
value={subscriptionType}
exclusive
onChange={(_, value) => value && setSubscriptionType(value)}
fullWidth
sx={{ mb: 2 }}
>
<ToggleButton value={SubscriptionType.Monthly}>
<Box sx={{ py: 1, textAlign: "center", width: "100%" }}>
<MonthlyIcon sx={{ mb: 0.5 }} />
<Typography variant="body2" display="block">
{t("modules.admin.monthly")}
</Typography>
<Typography variant="h6" color="primary">
{formatPrice(calculateTotal(SubscriptionType.Monthly))}
<Typography
component="span"
variant="body2"
color="text.secondary"
>
{t("modules.admin.perMonth")}
</Typography>
</Typography>
</Box>
</ToggleButton>
<ToggleButton value={SubscriptionType.Annual}>
<Box sx={{ py: 1, textAlign: "center", width: "100%" }}>
<AnnualIcon sx={{ mb: 0.5 }} />
<Typography variant="body2" display="block">
{t("modules.admin.annual")}
</Typography>
<Typography variant="h6" color="primary">
{formatPrice(calculateTotal(SubscriptionType.Annual))}
<Typography
component="span"
variant="body2"
color="text.secondary"
>
{t("modules.admin.perYear")}
</Typography>
</Typography>
{savingsPercent > 0 && (
<Chip
label={t("modules.admin.savings", {
percent: 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">
{t("modules.admin.autoRenewLabel")}
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
{/* Riepilogo */}
<Paper
variant="outlined"
sx={{ p: 2, mb: 3, bgcolor: "action.hover" }}
>
<Typography variant="subtitle2" gutterBottom>
{t("modules.admin.orderSummary")}
</Typography>
<Box
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
>
<Typography>{t("modules.admin.module")} {module.name}</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 }}
>
<Typography color="text.secondary">
{t("modules.admin.subscription")}{" "}
{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">{t("modules.admin.total")}</Typography>
<Typography variant="h6" color="primary">
{formatPrice(totalPrice)}
{priceLabel}
</Typography>
</Box>
</Paper>
{/* Errore */}
{enableMutation.isError && (
<Alert severity="error" sx={{ mb: 3 }}>
{(enableMutation.error as Error)?.message ||
t("modules.admin.activationError")}
</Alert>
)}
{/* Funzionalità incluse */}
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" gutterBottom>
{t("modules.admin.includedFeatures")}
</Typography>
<List dense>
{getModuleFeatures(module.code, t).map((feature, index) => (
<ListItem key={index} disablePadding>
<ListItemIcon sx={{ minWidth: 36 }}>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary={feature} />
</ListItem>
))}
</List>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={enableMutation.isPending}>
{t("common.cancel")}
</Button>
<Button
variant="contained"
startIcon={
enableMutation.isPending ? (
<CircularProgress size={20} color="inherit" />
) : (
<ShoppingCartIcon />
)
}
onClick={() => enableMutation.mutate()}
disabled={enableMutation.isPending}
>
{enableMutation.isPending
? t("modules.admin.activating")
: t("modules.admin.activateModule")}
</Button>
</DialogActions>
</Dialog>
);
}
// 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")];
}

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; 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 { useMutation } from "@tanstack/react-query";
import { import {
Box, Box,
@@ -38,7 +38,6 @@ import {
export default function ModulePurchasePage() { export default function ModulePurchasePage() {
const { code } = useParams<{ code: string }>(); const { code } = useParams<{ code: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const module = useModule(code || ""); const module = useModule(code || "");
const { enableModule, isModuleEnabled } = useModules(); const { enableModule, isModuleEnabled } = useModules();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -58,9 +57,8 @@ export default function ModulePurchasePage() {
}); });
}, },
onSuccess: () => { onSuccess: () => {
// Redirect alla pagina originale o alla home del modulo // Redirect alla pagina dei moduli
const from = (location.state as { from?: string })?.from; navigate("/modules");
navigate(from || module?.routePath || "/");
}, },
}); });

View File

@@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { import {
Box, Box,
@@ -28,8 +27,10 @@ import {
Warning as WarningIcon, Warning as WarningIcon,
Schedule as ScheduleIcon, Schedule as ScheduleIcon,
Autorenew as RenewIcon, Autorenew as RenewIcon,
ShoppingCart,
} from "@mui/icons-material"; } from "@mui/icons-material";
import * as Icons from "@mui/icons-material"; import * as Icons from "@mui/icons-material";
import { AxiosError } from "axios";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useModules } from "../contexts/ModuleContext"; import { useModules } from "../contexts/ModuleContext";
import { moduleService } from "../services/moduleService"; import { moduleService } from "../services/moduleService";
@@ -40,14 +41,17 @@ import {
getDaysRemainingText, getDaysRemainingText,
getSubscriptionStatusColor, getSubscriptionStatusColor,
getSubscriptionStatusText, getSubscriptionStatusText,
SubscriptionType,
} from "../types/module"; } from "../types/module";
import ModulePurchaseDialog from "../components/ModulePurchaseDialog";
export default function ModulesAdminPage() { export default function ModulesAdminPage() {
const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const { modules, isLoading, refreshModules } = useModules(); const { modules, isLoading, refreshModules } = useModules();
const [selectedModule, setSelectedModule] = useState<ModuleDto | null>(null); const [selectedModule, setSelectedModule] = useState<ModuleDto | null>(null);
const [purchaseModule, setPurchaseModule] = useState<ModuleDto | null>(null);
const [confirmDisable, setConfirmDisable] = useState<string | null>(null); const [confirmDisable, setConfirmDisable] = useState<string | null>(null);
const [enableError, setEnableError] = useState<string | null>(null);
// Query per moduli in scadenza // Query per moduli in scadenza
const { data: expiringModules = [] } = useQuery({ const { data: expiringModules = [] } = useQuery({
@@ -64,6 +68,25 @@ export default function ModulesAdminPage() {
}, },
}); });
// Mutation per attivare modulo
const enableMutation = useMutation({
mutationFn: (code: string) =>
moduleService.enable(code, {
subscriptionType: SubscriptionType.Annual,
autoRenew: true,
}),
onSuccess: () => {
refreshModules();
},
onError: (error) => {
setEnableError(
(error as AxiosError<{ message: string }>)?.response?.data?.message ||
(error as Error)?.message ||
t("common.error")
);
},
});
// Mutation per rinnovare subscription // Mutation per rinnovare subscription
const renewMutation = useMutation({ const renewMutation = useMutation({
mutationFn: (code: string) => moduleService.renewSubscription(code), mutationFn: (code: string) => moduleService.renewSubscription(code),
@@ -166,9 +189,13 @@ export default function ModulesAdminPage() {
if (module.isEnabled && !module.isCore) { if (module.isEnabled && !module.isCore) {
setConfirmDisable(module.code); setConfirmDisable(module.code);
} else if (!module.isEnabled) { } 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)} onInfo={() => setSelectedModule(module)}
onRenew={() => renewMutation.mutate(module.code)} onRenew={() => renewMutation.mutate(module.code)}
isRenewing={renewMutation.isPending} isRenewing={renewMutation.isPending}
@@ -332,7 +359,9 @@ export default function ModulesAdminPage() {
</Typography> </Typography>
{disableMutation.isError && ( {disableMutation.isError && (
<Alert severity="error" sx={{ mt: 2 }}> <Alert severity="error" sx={{ mt: 2 }}>
{(disableMutation.error as Error)?.message || {(disableMutation.error as AxiosError<{ message: string }>)?.response
?.data?.message ||
(disableMutation.error as Error)?.message ||
t("common.error")} t("common.error")}
</Alert> </Alert>
)} )}
@@ -358,7 +387,37 @@ export default function ModulesAdminPage() {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Box>
{/* 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}
open={!!purchaseModule}
onClose={() => {
setPurchaseModule(null);
refreshModules();
}}
/>
</Box >
); );
} }
@@ -366,6 +425,7 @@ export default function ModulesAdminPage() {
interface ModuleCardProps { interface ModuleCardProps {
module: ModuleDto; module: ModuleDto;
onToggle: () => void; onToggle: () => void;
onPurchase: () => void;
onInfo: () => void; onInfo: () => void;
onRenew: () => void; onRenew: () => void;
isRenewing: boolean; isRenewing: boolean;
@@ -376,6 +436,7 @@ interface ModuleCardProps {
function ModuleCard({ function ModuleCard({
module, module,
onToggle, onToggle,
onPurchase,
onInfo, onInfo,
onRenew, onRenew,
isRenewing, isRenewing,
@@ -421,6 +482,15 @@ function ModuleCard({
<Box> <Box>
{module.isCore ? ( {module.isCore ? (
<Chip label={t("modules.admin.core")} size="small" color="info" /> <Chip label={t("modules.admin.core")} size="small" color="info" />
) : !module.subscription?.isValid ? (
<Chip
label={t("modules.admin.purchase")}
size="small"
color="error"
icon={<ShoppingCart />}
onClick={onPurchase}
sx={{ cursor: "pointer" }}
/>
) : ( ) : (
<Chip label={statusText} size="small" color={statusColor} /> <Chip label={statusText} size="small" color={statusColor} />
)} )}
@@ -514,9 +584,14 @@ function ModuleCard({
checked={module.isEnabled} checked={module.isEnabled}
onChange={onToggle} onChange={onToggle}
color={module.isEnabled ? "success" : "default"} 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" labelPlacement="start"
/> />
)} )}