Skip to content
Extraits de code Groupes Projets
Valider f3bfd159 rédigé par NassihWalid's avatar NassihWalid
Parcourir les fichiers

implemented display of list of subscribers and list of subscriber by plan

parent 3af9dbb4
Branches
Étiquettes
1 requête de fusion!93implemented display of list of subscribers and list of subscriber by plan
Pipeline #20624 réussi avec les étapes
in 4 minutes et 52 secondes
Affichage de
avec 942 ajouts et 1173 suppressions
import type { IDatePickerControl } from './common';
export type ISubscriberFilters = {
name: string;
email: string;
status: string;
subscriptions: string[];
startDate?: IDatePickerControl;
endDate?: IDatePickerControl;
};
export interface ISubscriberItem {
id: string;
parentId: number;
name: string;
email: string;
phone: string;
address: string;
status: string;
createdAt: string;
subscriptionId: number;
subscriptionStatus: string;
planTitle: string;
subscriptionStartDate: string;
subscriptionEndDate: string;
amount: number;
billingCycle: string;
subscriptions: ISubscriptionPlan[];
paymentMethod: string;
lastPaymentDate: string;
updatedAt: string;
}
export interface ISubscriptionPlan {
id: string;
title: string;
status: string;
amount: number;
billingCycle: string;
}
export interface BackendSubscriberDTO {
parentId: number;
name: string;
email: string;
phone: string;
status: string;
address: string;
createdAt: string;
subscriptionId: number;
subscriptionStatus: string;
planTitle: string;
subscriptionStartDate: string;
subscriptionEndDate: string;
amount: number;
billingCycle: string;
}
\ No newline at end of file
import { GATEWAY_API_URL } from 'src/config-global';
const PLAN_PREFIX = `${GATEWAY_API_URL}/api/subscriptions`;
export const planEndpoints = {
plan: {
list: `${PLAN_PREFIX}/subscription/plans`,
fullDetails: `${PLAN_PREFIX}/subscription/plans/full/:id`,
changePublishType: `${PLAN_PREFIX}/subscription/plans/:id/publish-type/:publishType`,
},
list: `${GATEWAY_API_URL}/subscription/plans`,
fullDetails: `${GATEWAY_API_URL}/subscription/plans/full/:id`,
changePublishType: `${GATEWAY_API_URL}/subscription/plans/:id/publish-type/:publishType`,
}
};
\ No newline at end of file
import { GATEWAY_API_URL } from 'src/config-global';
export const subscriberEndpoints = {
getAllSubscribers: `${GATEWAY_API_URL}/subscription/subscribers`,
getSubscribersByPlan: (planId: string) => `${GATEWAY_API_URL}/subscription/subscribers/plan/${planId}`,
getSubscriberDetails: (subscriberId: string) => `${GATEWAY_API_URL}/subscription/subscribers/${subscriberId}`,
deleteSubscriber: (subscriberId: string) => `${GATEWAY_API_URL}/subscription/subscribers/${subscriberId}`,
updateSubscriber: (subscriberId: string) => `${GATEWAY_API_URL}/subscription/subscribers/${subscriberId}`,
} as const;
\ No newline at end of file
......@@ -166,17 +166,6 @@ export const usePlanStore = create<PlanState>()(
set({ loading: true, error: null });
try {
const response = await axios.put(
planEndpoints.plan.changePublishType
.replace(':id', planId)
.replace(':publishType', publishType),
{},
{
headers: { 'Content-Type': 'application/json' }
}
);
// Update the plan in the store
const currentPlans = get().plans;
const updatedPlans = currentPlans.map(plan =>
plan.id === planId
......@@ -190,7 +179,6 @@ export const usePlanStore = create<PlanState>()(
error: null
});
// Clear cache for this plan to force refresh
const newCache = new Map(get().planDetailsCache);
newCache.delete(planId);
set({ planDetailsCache: newCache });
......
import type {
ISubscriberItem,
ISubscriptionPlan,
ISubscriberFilters,
BackendSubscriberDTO
} from 'src/contexts/types/subscribers';
import axios from 'axios';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { subscriberEndpoints } from '../endpoints/subscriber';
const transformBackendToFrontend = (backendDto: BackendSubscriberDTO): ISubscriberItem => {
const subscriptionPlan: ISubscriptionPlan = {
id: backendDto.subscriptionId.toString(),
title: backendDto.planTitle,
status: backendDto.subscriptionStatus,
amount: backendDto.amount,
billingCycle: backendDto.billingCycle,
};
return {
id: backendDto.parentId.toString(),
parentId: backendDto.parentId,
name: backendDto.name,
email: backendDto.email,
phone: backendDto.phone || 'Non disponible',
address: backendDto.address,
status: backendDto.status,
createdAt: backendDto.createdAt,
subscriptionId: backendDto.subscriptionId,
subscriptionStatus: backendDto.subscriptionStatus,
planTitle: backendDto.planTitle,
subscriptionStartDate: backendDto.subscriptionStartDate,
subscriptionEndDate: backendDto.subscriptionEndDate,
amount: backendDto.amount,
billingCycle: backendDto.billingCycle,
subscriptions: [subscriptionPlan],
paymentMethod: 'unknown',
lastPaymentDate: backendDto.subscriptionStartDate,
updatedAt: backendDto.createdAt,
};
};
interface SubscriberMetrics {
total: number;
active: number;
inactive: number;
pending: number;
deleted: number;
trialing: number;
past_due: number;
}
interface SubscriberState {
subscribers: ISubscriberItem[];
filteredSubscribers: ISubscriberItem[];
metrics: SubscriberMetrics;
loading: boolean;
error: string | null;
initialized: boolean;
filters: ISubscriberFilters;
searchQuery: string;
sortBy: string;
selectedPlanId: string | null;
fetchAllSubscribers: () => Promise<void>;
fetchSubscribersByPlan: (planId: string) => Promise<void>;
setFilters: (filters: Partial<ISubscriberFilters>) => void;
setSearchQuery: (query: string) => void;
setSortBy: (sortBy: string) => void;
setSelectedPlanId: (planId: string | null) => void;
resetFilters: () => void;
clearError: () => void;
setLoading: (loading: boolean) => void;
getFilteredSubscribers: () => ISubscriberItem[];
getSubscriberMetrics: () => SubscriberMetrics;
}
const defaultFilters: ISubscriberFilters = {
name: '',
email: '',
status: '',
subscriptions: [],
startDate: null,
endDate: null,
};
export const useSubscriberStore = create<SubscriberState>()(
devtools((set, get) => ({
subscribers: [],
filteredSubscribers: [],
metrics: {
total: 0,
active: 0,
inactive: 0,
pending: 0,
deleted: 0,
trialing: 0,
past_due: 0,
},
loading: false,
error: null,
initialized: false,
filters: defaultFilters,
searchQuery: '',
sortBy: 'Plus récent',
selectedPlanId: null,
fetchAllSubscribers: async () => {
const state = get();
if (state.loading) return;
set({ loading: true, error: null, selectedPlanId: null });
try {
const response = await axios.get<BackendSubscriberDTO[]>(
subscriberEndpoints.getAllSubscribers,
{
headers: { 'Content-Type': 'application/json' }
}
);
if (response.data) {
const subscribers = response.data.map(transformBackendToFrontend);
const metrics = calculateMetrics(subscribers);
const filtered = applyFilters(subscribers, get().filters, get().searchQuery);
set({
subscribers,
filteredSubscribers: filtered,
metrics,
loading: false,
initialized: true,
error: null
});
}
} catch (error: any) {
console.error('Error fetching subscribers:', error);
const errorMessage = error?.response?.data?.message || error.message || 'Failed to fetch subscribers';
set({
error: errorMessage,
loading: false,
initialized: true
});
throw error;
}
},
fetchSubscribersByPlan: async (planId: string) => {
const state = get();
if (state.loading) return;
set({ loading: true, error: null, selectedPlanId: planId });
try {
const response = await axios.get<BackendSubscriberDTO[]>(
subscriberEndpoints.getSubscribersByPlan(planId),
{
headers: { 'Content-Type': 'application/json' }
}
);
if (response.data) {
const subscribers = response.data.map(transformBackendToFrontend);
const metrics = calculateMetrics(subscribers);
const filtered = applyFilters(subscribers, get().filters, get().searchQuery);
set({
subscribers,
filteredSubscribers: filtered,
metrics,
loading: false,
initialized: true,
error: null
});
}
} catch (error: any) {
console.error('Error fetching subscribers by plan:', error);
const errorMessage = error?.response?.data?.message || error.message || 'Failed to fetch subscribers by plan';
set({
error: errorMessage,
loading: false,
initialized: true
});
throw error;
}
},
setFilters: (newFilters: Partial<ISubscriberFilters>) => {
const currentFilters = get().filters;
const updatedFilters = { ...currentFilters, ...newFilters };
const filtered = applyFilters(get().subscribers, updatedFilters, get().searchQuery);
set({
filters: updatedFilters,
filteredSubscribers: filtered
});
},
setSearchQuery: (query: string) => {
const filtered = applyFilters(get().subscribers, get().filters, query);
set({
searchQuery: query,
filteredSubscribers: filtered
});
},
setSortBy: (sortBy: string) => {
const filtered = applySorting(get().filteredSubscribers, sortBy);
set({
sortBy,
filteredSubscribers: filtered
});
},
setSelectedPlanId: (planId: string | null) => {
set({ selectedPlanId: planId });
},
resetFilters: () => {
const filtered = applyFilters(get().subscribers, defaultFilters, '');
set({
filters: defaultFilters,
searchQuery: '',
sortBy: 'Plus récent',
selectedPlanId: null,
filteredSubscribers: filtered
});
},
clearError: () => {
set({ error: null });
},
setLoading: (loading: boolean) => {
set({ loading });
},
getFilteredSubscribers: () => {
const { subscribers, filters, searchQuery, sortBy } = get();
const filtered = applyFilters(subscribers, filters, searchQuery);
return applySorting(filtered, sortBy);
},
getSubscriberMetrics: () => get().metrics,
}), { name: 'subscriber-store' })
);
function calculateMetrics(subscribers: ISubscriberItem[]): SubscriberMetrics {
const statusCounts = subscribers.reduce((acc, sub) => {
const status = sub.status.toLowerCase();
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return {
total: subscribers.length,
active: statusCounts.active || 0,
inactive: statusCounts.inactive || 0,
pending: statusCounts.pending || 0,
deleted: statusCounts.deleted || 0,
trialing: statusCounts.trialing || 0,
past_due: statusCounts.past_due || 0,
};
}
function applyFilters(
subscribers: ISubscriberItem[],
filters: ISubscriberFilters,
searchQuery: string
): ISubscriberItem[] {
let filtered = [...subscribers];
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(sub =>
sub.name.toLowerCase().includes(query) ||
sub.email.toLowerCase().includes(query) ||
sub.phone.toLowerCase().includes(query)
);
}
if (filters.name) {
filtered = filtered.filter(sub =>
sub.name.toLowerCase().includes(filters.name.toLowerCase())
);
}
if (filters.email) {
filtered = filtered.filter(sub =>
sub.email.toLowerCase().includes(filters.email.toLowerCase())
);
}
if (filters.status && filters.status !== 'all') {
filtered = filtered.filter(sub => sub.status.toLowerCase() === filters.status.toLowerCase());
}
if (filters.subscriptions.length > 0) {
filtered = filtered.filter(sub =>
sub.subscriptions.some(subscription =>
filters.subscriptions.includes(subscription.title)
)
);
}
if (filters.startDate || filters.endDate) {
filtered = filtered.filter(sub => {
const subDate = new Date(sub.subscriptionStartDate);
if (filters.startDate && subDate < filters.startDate.toDate()) {
return false;
}
if (filters.endDate && subDate > filters.endDate.toDate()) {
return false;
}
return true;
});
}
return filtered;
}
function applySorting(subscribers: ISubscriberItem[], sortBy: string): ISubscriberItem[] {
const sorted = [...subscribers];
switch (sortBy) {
case 'Plus récent':
return sorted.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
case 'Plus ancien':
return sorted.sort((a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
case 'Nom A-Z':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'Nom Z-A':
return sorted.sort((a, b) => b.name.localeCompare(a.name));
default:
return sorted;
}
}
\ No newline at end of file
'use client';
import type { IAbonnementSubscribers } from 'src/contexts/types/common';
import { useEffect } from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Alert from '@mui/material/Alert';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Pagination from '@mui/material/Pagination';
import Typography from '@mui/material/Typography';
import ListItemText from '@mui/material/ListItemText';
import CircularProgress from '@mui/material/CircularProgress';
import { useSubscriberStore } from 'src/shared/api/stores/useSubscriberStore';
// ----------------------------------------------------------------------
type Props = {
subscribers: IAbonnementSubscribers[];
planId: string;
};
export function AbonnementDetailsSubscribers({ subscribers }: Props) {
export function AbonnementDetailsSubscribers({ planId }: Props) {
const {
filteredSubscribers,
loading,
error,
selectedPlanId,
fetchSubscribersByPlan,
clearError,
} = useSubscriberStore();
useEffect(() => {
if (planId && planId !== selectedPlanId) {
fetchSubscribersByPlan(planId).catch((err) => {
console.error('Failed to fetch subscribers for plan:', err);
});
}
}, [planId, selectedPlanId, fetchSubscribersByPlan]);
const getInitials = (name: string) => {
const parts = name.split(' ');
return parts.length > 1
? `${parts[0][0]}${parts[1][0]}`
: parts[0].substring(0, 2);
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Alert
severity="error"
action={
<Button
color="inherit"
size="small"
onClick={() => {
clearError();
fetchSubscribersByPlan(planId);
}}
>
Réessayer
</Button>
}
sx={{ m: 2 }}
>
{error}
</Alert>
);
}
if (filteredSubscribers.length === 0) {
return (
<Box sx={{ textAlign: 'center', p: 3 }}>
<Typography variant="h6" color="text.secondary">
Aucun abonné trouvé pour ce plan
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Il n&apos;y a actuellement aucun abonné inscrit à ce plan d&apos;abonnement.
</Typography>
</Box>
);
}
return (
<>
<Box
......@@ -23,18 +97,19 @@ export function AbonnementDetailsSubscribers({ subscribers }: Props) {
display="grid"
gridTemplateColumns={{ xs: 'repeat(1, 1fr)', md: 'repeat(4, 1fr)' }}
>
{subscribers.map((subscriber) => (
{filteredSubscribers.map((subscriber) => (
<Card key={subscriber.id} sx={{ p: 3, gap: 2, display: 'flex' }}>
<Avatar
alt={subscriber.name}
src={subscriber.avatarUrl}
sx={{ width: 48, height: 48 }}
/>
>
{getInitials(subscriber.name)}
</Avatar>
<Stack spacing={2}>
<Stack spacing={2} sx={{ flex: 1 }}>
<ListItemText
primary={subscriber.name}
secondary={subscriber.role}
secondary={subscriber.email}
secondaryTypographyProps={{
mt: 0.5,
component: 'span',
......@@ -50,4 +125,4 @@ export function AbonnementDetailsSubscribers({ subscribers }: Props) {
<Pagination count={10} sx={{ mt: { xs: 5, md: 8 }, mx: 'auto' }} />
</>
);
}
}
\ No newline at end of file
import type { ISubscriberItem } from 'src/contexts/types/subscriber';
import type { ISubscriberItem } from 'src/contexts/types/subscribers';
import React from 'react';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
......@@ -65,4 +65,4 @@ export function SubscriberDetailsToolbar({ subscriber, onDelete, onCloseDetails
/>
</>
);
}
}
\ No newline at end of file
import type { ISubscriberItem } from 'src/contexts/types/subscriber';
import type { ISubscriberItem } from 'src/contexts/types/subscribers';
import Link from 'next/link';
import { m } from 'framer-motion';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faPhone,
faHistory,
faReceipt,
faEnvelope,
faToggleOn,
faCreditCard,
faEuroSign,
faCalendarAlt,
faMapMarkerAlt,
faFileInvoiceDollar,
} from '@fortawesome/free-solid-svg-icons';
import Box from '@mui/material/Box';
......@@ -29,8 +26,6 @@ import {
ListItemText,
} from '@mui/material';
import { paths } from 'src/routes/paths';
import { fDate } from 'src/utils/format-time';
import { Scrollbar } from 'src/shared/components/scrollbar';
......@@ -40,12 +35,6 @@ import { SubscriberDetailsToolbar } from './subscriber-details-toolbar';
// ----------------------------------------------------------------------
// Options de méthodes de paiement en français
export const PAYMENT_METHOD_OPTIONS = [
{ label: 'Carte de crédit', value: 'credit_card' },
{ label: 'Virement bancaire', value: 'bank_transfer' },
];
// Options de cycles de facturation
export const BILLING_CYCLE_OPTIONS = [
{ label: 'Mensuel', value: 'monthly' },
......@@ -53,19 +42,34 @@ export const BILLING_CYCLE_OPTIONS = [
{ label: 'Annuel', value: 'yearly' },
];
// Fonction pour obtenir le libellé français de la méthode de paiement
const getPaymentMethodLabel = (value: string): string => {
const method = PAYMENT_METHOD_OPTIONS.find((option) => option.value === value);
return method ? method.label : value;
};
// Fonction pour obtenir le libellé français du cycle de facturation
const getBillingCycleLabel = (value: string): string => {
const cycle = BILLING_CYCLE_OPTIONS.find((option) => option.value === value);
return cycle ? cycle.label : value;
};
// Création d'un style pour rendre les abonnements cliquables
const getStatusLabel = (status: string): string => {
switch (status.toLowerCase()) {
case 'active':
return 'Actif';
case 'inactive':
return 'Inactif';
case 'pending':
return 'En attente';
case 'deleted':
return 'Supprimé';
case 'trialing':
return 'Période d\'essai';
case 'past_due':
return 'En retard de paiement';
default:
return status || 'Inconnu';
}
};
const formatCurrency = (amount: number) => new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
}).format(amount);
// ----------------------------------------------------------------------
......@@ -76,7 +80,6 @@ type Props = {
onTrue: () => void;
onFalse: () => void;
};
onDeleteSubscriber: (id: string) => void;
onCloseDetails: () => void;
};
......@@ -157,13 +160,7 @@ export function SubscriberDetails({
/>
<Chip
label={
subscriber.status === 'active'
? 'Actif'
: subscriber.status === 'pending'
? 'En attente'
: 'Inactif'
}
label={getStatusLabel(subscriber.status)}
size="small"
sx={{
bgcolor: alpha(
......@@ -216,10 +213,10 @@ export function SubscriberDetails({
}}
/>
<Typography variant="h5" color="text.primary">
{subscriber.subscriptions.length}
{subscriber.planTitle}
</Typography>
<Typography variant="body2" color="text.secondary">
Abonnements
Plan actuel
</Typography>
</Paper>
......@@ -235,7 +232,7 @@ export function SubscriberDetails({
}}
>
<FontAwesomeIcon
icon={faFileInvoiceDollar}
icon={faEuroSign}
style={{
color: theme.palette.success.main,
fontSize: 24,
......@@ -243,10 +240,10 @@ export function SubscriberDetails({
}}
/>
<Typography variant="h5" color="text.primary">
{getBillingCycleLabel(subscriber.billingCycle)}
{formatCurrency(subscriber.amount)}
</Typography>
<Typography variant="body2" color="text.secondary">
Cycle de facturation
{getBillingCycleLabel(subscriber.billingCycle)}
</Typography>
</Paper>
</Stack>
......@@ -373,6 +370,41 @@ export function SubscriberDetails({
}
/>
</ListItem>
<ListItem
sx={{
py: 1.5,
}}
>
<ListItemText
primary={
<Stack direction="row" spacing={2} alignItems="center">
<Box
sx={{
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
bgcolor: alpha(theme.palette.secondary.main, 0.1),
color: 'secondary.main',
}}
>
<FontAwesomeIcon icon={faCalendarAlt} size="sm" />
</Box>
<Typography variant="body2" color="text.secondary">
Inscrit le
</Typography>
</Stack>
}
secondary={
<Typography variant="body1" sx={{ mt: 0.5, ml: 6 }}>
{fDate(subscriber.createdAt)}
</Typography>
}
/>
</ListItem>
</List>
<Typography variant="subtitle1" gutterBottom fontWeight="fontWeightBold" sx={{ mb: 2 }}>
......@@ -482,16 +514,16 @@ export function SubscriberDetails({
color: 'success.main',
}}
>
<FontAwesomeIcon icon={faCreditCard} size="sm" />
<FontAwesomeIcon icon={faEuroSign} size="sm" />
</Box>
<Typography variant="body2" color="text.secondary">
Méthode de paiement
Montant
</Typography>
</Stack>
}
secondary={
<Typography variant="body1" sx={{ mt: 0.5, ml: 6 }}>
{getPaymentMethodLabel(subscriber.paymentMethod)}
{formatCurrency(subscriber.amount)}
</Typography>
}
/>
......@@ -518,17 +550,32 @@ export function SubscriberDetails({
color: 'info.main',
}}
>
<FontAwesomeIcon icon={faHistory} size="sm" />
<FontAwesomeIcon icon={faToggleOn} size="sm" />
</Box>
<Typography variant="body2" color="text.secondary">
Dernier paiement
Statut de l&apos;abonnement
</Typography>
</Stack>
}
secondary={
<Typography variant="body1" sx={{ mt: 0.5, ml: 6 }}>
{fDate(subscriber.lastPaymentDate)}
</Typography>
<Box sx={{ mt: 0.5, ml: 6, display: 'flex', alignItems: 'center' }}>
<Chip
label={getStatusLabel(subscriber.subscriptionStatus)}
size="small"
sx={{
color: 'white',
bgcolor:
subscriber.subscriptionStatus === 'active'
? 'success.main'
: subscriber.subscriptionStatus === 'pending'
? 'warning.main'
: subscriber.subscriptionStatus === 'trialing'
? 'info.main'
: 'error.main',
fontWeight: 'bold',
}}
/>
</Box>
}
/>
</ListItem>
......@@ -549,27 +596,21 @@ export function SubscriberDetails({
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
bgcolor: alpha(theme.palette.success.main, 0.1),
color: 'success.main',
bgcolor: alpha(theme.palette.secondary.main, 0.1),
color: 'secondary.main',
}}
>
<FontAwesomeIcon icon={faToggleOn} size="sm" />
</Box>
<Typography variant="body2" color="text.secondary">
Statut
Statut du compte
</Typography>
</Stack>
}
secondary={
<Box sx={{ mt: 0.5, ml: 6, display: 'flex', alignItems: 'center' }}>
<Chip
label={
subscriber.status === 'active'
? 'Actif'
: subscriber.status === 'pending'
? 'En attente'
: 'Inactif'
}
label={getStatusLabel(subscriber.status)}
size="small"
sx={{
color: 'white',
......@@ -588,100 +629,56 @@ export function SubscriberDetails({
</ListItem>
</List>
{/* Subscriptions */}
<Typography variant="subtitle1" gutterBottom fontWeight="fontWeightBold" sx={{ mb: 2 }}>
Abonnements associés
Détails du plan
</Typography>
{subscriber.subscriptions.length > 0 ? (
<Stack
component={m.div}
initial="initial"
animate="animate"
variants={varFade().inUp}
>
{subscriber.subscriptions.map((subscription) => (
<Link
key={subscription.id}
href={paths.dashboard.abonnements.details(subscription.id)}
passHref
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Paper
elevation={0}
sx={{
p: 2.5,
mb: 2,
borderRadius: 2,
position: 'relative',
overflow: 'hidden',
boxShadow: theme.customShadows?.z8,
transition: 'all 0.2s ease-in-out',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: theme.customShadows?.z16,
},
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '6px',
height: '100%',
backgroundColor: theme.palette.primary.main,
},
}}
>
<Stack spacing={1.5}>
<Typography variant="subtitle1" fontWeight="bold">
{subscription.title}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
maxHeight: 80,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
}}
>
{subscription.shortDescription || 'Aucune description disponible'}
</Typography>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle1" color="primary.main" fontWeight="bold">
{`${subscription.price?.monthly || 0} €`}
</Typography>
</Stack>
</Stack>
</Paper>
</Link>
))}
</Stack>
) : (
<Paper
component={m.div}
initial="initial"
animate="animate"
variants={varFade().inUp}
elevation={0}
sx={{
p: 3,
textAlign: 'center',
borderRadius: 2,
bgcolor: alpha(theme.palette.background.default, 0.8),
boxShadow: theme.customShadows?.z8,
}}
>
<Paper
component={m.div}
initial="initial"
animate="animate"
variants={varFade().inUp}
elevation={0}
sx={{
p: 2.5,
mb: 2,
borderRadius: 2,
position: 'relative',
overflow: 'hidden',
boxShadow: theme.customShadows?.z8,
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '6px',
height: '100%',
backgroundColor: theme.palette.primary.main,
},
}}
>
<Stack spacing={1.5}>
<Typography variant="subtitle1" fontWeight="bold">
{subscriber.planTitle}
</Typography>
<Typography variant="body2" color="text.secondary">
Aucun abonnement trouvé pour cet abonné
ID de l&apos;abonnement: {subscriber.subscriptionId}
</Typography>
</Paper>
)}
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle1" color="primary.main" fontWeight="bold">
{formatCurrency(subscriber.amount)}
</Typography>
<Chip
label={getBillingCycleLabel(subscriber.billingCycle)}
size="small"
variant="outlined"
color="primary"
/>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
</Scrollbar>
......@@ -694,4 +691,4 @@ export function SubscriberDetails({
/>
</Drawer>
);
}
}
\ No newline at end of file
import type { ISubscriberItem } from 'src/contexts/types/subscriber';
import type { ISubscriberItem } from 'src/contexts/types/subscribers';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEye, faTrashAlt, faEnvelope } from '@fortawesome/free-solid-svg-icons';
......@@ -18,7 +18,6 @@ import { useBoolean } from 'src/hooks/use-boolean';
import { fDate, fTime } from 'src/utils/format-time';
import { PAYMENT_METHOD_OPTIONS } from 'src/shared/_mock/_payment';
import { BILLING_CYCLE_OPTIONS } from 'src/shared/_mock/_subscriber';
import { Label } from 'src/shared/components/label';
......@@ -55,7 +54,6 @@ export function SubscriberTableRow({
const sendEmail = useBoolean();
const openDetails = useBoolean();
const isColumnVisible = (columnId: string) => visibleColumns.some((col) => col.id === columnId);
// Generate a avatar placeholder for the subscriber based on their name
......@@ -66,6 +64,43 @@ export function SubscriberTableRow({
: parts[0].substring(0, 2);
};
const getStatusLabel = (status: string) => {
switch (status.toLowerCase()) {
case 'active':
return 'Actif';
case 'inactive':
return 'Inactif';
case 'pending':
return 'En attente';
case 'deleted':
return 'Supprimé';
case 'trialing':
return 'Essai';
case 'past_due':
return 'En retard';
default:
return status || 'Inconnu';
}
};
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'active':
return 'success';
case 'pending':
return 'warning';
case 'inactive':
case 'deleted':
return 'error';
case 'trialing':
return 'info';
case 'past_due':
return 'error';
default:
return 'default';
}
};
return (
<>
<TableRow hover selected={selected}>
......@@ -114,15 +149,15 @@ export function SubscriberTableRow({
{isColumnVisible('email') && (
<TableCell
sx={{
maxWidth: 160,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
<Typography variant="body2" noWrap>{row.email}</Typography>
</TableCell>
sx={{
maxWidth: 160,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
<Typography variant="body2" noWrap>{row.email}</Typography>
</TableCell>
)}
{isColumnVisible('phone') && (
......@@ -162,12 +197,15 @@ export function SubscriberTableRow({
</TableCell>
)}
{isColumnVisible('paymentMethod') && (
{isColumnVisible('paymentMethod') && (
<TableCell>
<Typography variant="body2">
{PAYMENT_METHOD_OPTIONS.find((option) => option.value === row.paymentMethod)?.label ||
'Non disponible'}
</Typography>
<Label
variant="soft"
color={getStatusColor(row.subscriptionStatus)}
sx={{ textTransform: 'capitalize' }}
>
{getStatusLabel(row.subscriptionStatus)}
</Label>
</TableCell>
)}
......@@ -175,17 +213,10 @@ export function SubscriberTableRow({
<TableCell>
<Label
variant="soft"
color={
(row.status === 'active' && 'success') ||
(row.status === 'pending' && 'warning') ||
(row.status === 'inactive' && 'error') ||
'default'
}
color={getStatusColor(row.status)}
sx={{ textTransform: 'capitalize' }}
>
{(row.status === 'active' && 'Actif') ||
(row.status === 'pending' && 'En attente') ||
(row.status === 'inactive' && 'Inactif') ||
row.status || 'Inconnu'}
{getStatusLabel(row.status)}
</Label>
</TableCell>
) : null}
......@@ -220,7 +251,6 @@ export function SubscriberTableRow({
onCloseDetails={openDetails.onFalse}
/>
<ConfirmDialog
open={confirm.value}
onClose={confirm.onFalse}
......@@ -234,4 +264,4 @@ export function SubscriberTableRow({
/>
</>
);
}
}
\ No newline at end of file
import type { SelectChangeEvent } from '@mui/material/Select';
import type { UseSetStateReturn } from 'src/hooks/use-set-state';
import type { ISubscriberFilters } from 'src/contexts/types/subscriber';
import { useCallback } from 'react';
......@@ -11,49 +9,48 @@ import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import { useSubscriberStore } from 'src/shared/api/stores/useSubscriberStore';
// ----------------------------------------------------------------------
type Props = {
onResetPage: () => void;
filters: UseSetStateReturn<ISubscriberFilters>;
options: {
subscriptions: string[];
};
};
export function SubscriberTableToolbar() {
const { filters, setFilters, filteredSubscribers } = useSubscriberStore();
const subscriptionOptions = Array.from(
new Set(filteredSubscribers.map(subscriber => subscriber.planTitle))
).filter(Boolean);
export function SubscriberTableToolbar({ filters, options, onResetPage }: Props) {
const handleFilterService = useCallback(
(event: SelectChangeEvent<string[]>) => {
const newValue =
typeof event.target.value === 'string' ? event.target.value.split(',') : event.target.value;
onResetPage();
filters.setState({ subscriptions: newValue });
setFilters({ subscriptions: newValue });
},
[filters, onResetPage]
[setFilters]
);
return (
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 }, p: '25px' }}>
<InputLabel htmlFor="subscriber-filter-service-select-label" sx={{ p: '30px' }}>
Abonnements
Plans d&apos;abonnement
</InputLabel>
<Select
multiple
value={filters.state.subscriptions}
value={filters.subscriptions}
onChange={handleFilterService}
input={<OutlinedInput label="Abonnements" />}
input={<OutlinedInput label="Plans d'abonnement" />}
renderValue={(selected) => selected.map((value) => value).join(', ')}
inputProps={{ id: 'subscriber-filter-service-select-label' }}
sx={{ textTransform: 'capitalize' }}
>
{options.subscriptions.map((option) => (
{subscriptionOptions.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox
disableRipple
size="small"
checked={filters.state.subscriptions.includes(option)}
checked={filters.subscriptions.includes(option)}
/>
{option}
</MenuItem>
......@@ -61,4 +58,4 @@ export function SubscriberTableToolbar({ filters, options, onResetPage }: Props)
</Select>
</FormControl>
);
}
}
\ No newline at end of file
......@@ -147,10 +147,10 @@ export function AbonnementDetailsView({ planId }: Props) {
{tabs.value === 'content' && (
<AbonnementDetailsContent plan={planItem} />
)}
{tabs.value === 'subscribers' && (
<AbonnementDetailsSubscribers
subscribers={[]}
planId={planId}
/>
)}
</DashboardContent>
......
0% ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter