Compare commits
5 Commits
d9e1328485
...
f946375e1f
| Author | SHA1 | Date | |
|---|---|---|---|
| f946375e1f | |||
| 52ab4a5998 | |||
| 51a8327a8d | |||
| 6e427e0199 | |||
| a4e0c276c6 |
@@ -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.
|
||||
|
||||
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".
|
||||
@@ -10,3 +10,5 @@ File riassuntivo dello stato di sviluppo di Zentral.
|
||||
- Ristrutturazione interfaccia: Sidebar a 2 livelli, Tabs, SearchBar.
|
||||
- [2025-12-03 Backend Fix](./devlog/2025-12-03_backend_fix.md) - **Completato**
|
||||
- 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.
|
||||
|
||||
36
docs/development/devlog/2025-12-03_module_management.md
Normal file
36
docs/development/devlog/2025-12-03_module_management.md
Normal 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.
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ModuleService
|
||||
.ThenBy(m => m.Name)
|
||||
.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>();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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))}");
|
||||
|
||||
@@ -72,9 +72,6 @@ public class ModuleSubscription : BaseEntity
|
||||
/// </summary>
|
||||
public bool IsValid()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return false;
|
||||
|
||||
// Se non c'è data di scadenza, è valido (licenza perpetua o core module)
|
||||
if (!EndDate.HasValue)
|
||||
return true;
|
||||
|
||||
@@ -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",
|
||||
|
||||
407
src/frontend/src/components/ModulePurchaseDialog.tsx
Normal file
407
src/frontend/src/components/ModulePurchaseDialog.tsx
Normal 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")];
|
||||
}
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,14 +41,17 @@ 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<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({
|
||||
@@ -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
|
||||
const renewMutation = useMutation({
|
||||
mutationFn: (code: string) => moduleService.renewSubscription(code),
|
||||
@@ -166,9 +189,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 +359,9 @@ export default function ModulesAdminPage() {
|
||||
</Typography>
|
||||
{disableMutation.isError && (
|
||||
<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")}
|
||||
</Alert>
|
||||
)}
|
||||
@@ -358,6 +387,36 @@ export default function ModulesAdminPage() {
|
||||
</Button>
|
||||
</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}
|
||||
open={!!purchaseModule}
|
||||
onClose={() => {
|
||||
setPurchaseModule(null);
|
||||
refreshModules();
|
||||
}}
|
||||
/>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
@@ -366,6 +425,7 @@ export default function ModulesAdminPage() {
|
||||
interface ModuleCardProps {
|
||||
module: ModuleDto;
|
||||
onToggle: () => void;
|
||||
onPurchase: () => void;
|
||||
onInfo: () => void;
|
||||
onRenew: () => void;
|
||||
isRenewing: boolean;
|
||||
@@ -376,6 +436,7 @@ interface ModuleCardProps {
|
||||
function ModuleCard({
|
||||
module,
|
||||
onToggle,
|
||||
onPurchase,
|
||||
onInfo,
|
||||
onRenew,
|
||||
isRenewing,
|
||||
@@ -421,6 +482,15 @@ function ModuleCard({
|
||||
<Box>
|
||||
{module.isCore ? (
|
||||
<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} />
|
||||
)}
|
||||
@@ -514,9 +584,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"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user