feat: implement module purchase dialog with subscription type selection, auto-renew, and dependency checks, replacing the dedicated purchase page.
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))}");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
301
src/frontend/src/components/ModulePurchaseDialog.tsx
Normal file
301
src/frontend/src/components/ModulePurchaseDialog.tsx
Normal file
@@ -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>(
|
||||||
|
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 (
|
||||||
|
<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 mancanti */}
|
||||||
|
{missingDependencies.length > 0 && (
|
||||||
|
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
{t("modules.admin.missingDependencies")}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||||
|
{missingDependencies.map((dep) => (
|
||||||
|
<Chip
|
||||||
|
key={dep}
|
||||||
|
label={dep}
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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(module.monthlyPrice)}
|
||||||
|
<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(module.basePrice)}
|
||||||
|
<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(price)}</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(price)}
|
||||||
|
{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" />
|
||||||
|
) : (
|
||||||
|
<CartIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={() => enableMutation.mutate()}
|
||||||
|
disabled={
|
||||||
|
enableMutation.isPending || missingDependencies.length > 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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 { 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 || "/");
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +41,15 @@ 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);
|
||||||
|
|
||||||
// Query per moduli in scadenza
|
// 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
|
// Mutation per rinnovare subscription
|
||||||
const renewMutation = useMutation({
|
const renewMutation = useMutation({
|
||||||
mutationFn: (code: string) => moduleService.renewSubscription(code),
|
mutationFn: (code: string) => moduleService.renewSubscription(code),
|
||||||
@@ -166,9 +181,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 +351,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 +379,17 @@ export default function ModulesAdminPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
|
||||||
|
{/* Dialog acquisto modulo */}
|
||||||
|
<ModulePurchaseDialog
|
||||||
|
module={purchaseModule}
|
||||||
|
open={!!purchaseModule}
|
||||||
|
onClose={() => {
|
||||||
|
setPurchaseModule(null);
|
||||||
|
refreshModules();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +397,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 +408,7 @@ interface ModuleCardProps {
|
|||||||
function ModuleCard({
|
function ModuleCard({
|
||||||
module,
|
module,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
onPurchase,
|
||||||
onInfo,
|
onInfo,
|
||||||
onRenew,
|
onRenew,
|
||||||
isRenewing,
|
isRenewing,
|
||||||
@@ -421,6 +454,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 +556,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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user