877 lines
27 KiB
TypeScript
877 lines
27 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
User,
|
|
Shield,
|
|
Truck,
|
|
Store,
|
|
Search,
|
|
XCircle,
|
|
Phone,
|
|
TrendingUp,
|
|
AlertCircle,
|
|
Filter,
|
|
Eye,
|
|
Leaf,
|
|
Wind,
|
|
} from "lucide-react";
|
|
import "./UserManagement.css";
|
|
import SidebarCabine from "../../components/SidebarCabine";
|
|
import { getAllUsers, getAllClients } from "../../api/api_admin";
|
|
import { isCabineAuthenticated } from "../../api/api_cabine"; // ⭐ AJOUT
|
|
import type { AdminResponse } from "../../api/api_admin_types";
|
|
import type { ClientResponse } from "../../api/api_admin";
|
|
|
|
// ============================================
|
|
// TYPES & INTERFACES
|
|
// ============================================
|
|
|
|
interface UserStats {
|
|
totalUsers: number;
|
|
totalCabines: number;
|
|
totalLivreurs: number;
|
|
}
|
|
|
|
type UserRole = "admin" | "cabine" | "livreur" | "client";
|
|
type RoleFilter = "all" | "cabine" | "livreur";
|
|
|
|
interface RoleConfig {
|
|
color: string;
|
|
label: string;
|
|
icon: React.ComponentType<{ size: number }>;
|
|
}
|
|
|
|
// ============================================
|
|
// CONSTANTS
|
|
// ============================================
|
|
|
|
const ROLE_CONFIG: Record<UserRole, RoleConfig> = {
|
|
admin: { color: "red", label: "Administrateur", icon: Shield },
|
|
cabine: { color: "purple", label: "Cabine", icon: Store },
|
|
livreur: { color: "blue", label: "Livreur", icon: Truck },
|
|
client: { color: "green", label: "Client", icon: User },
|
|
};
|
|
|
|
// ============================================
|
|
// MAIN COMPONENT
|
|
// ============================================
|
|
|
|
function UsersManagement() {
|
|
const navigate = useNavigate();
|
|
|
|
// État des utilisateurs
|
|
const [users, setUsers] = useState<AdminResponse[]>([]);
|
|
const [filteredUsers, setFilteredUsers] = useState<AdminResponse[]>([]);
|
|
const [stats, setStats] = useState<UserStats>({
|
|
totalUsers: 0,
|
|
totalCabines: 0,
|
|
totalLivreurs: 0,
|
|
});
|
|
|
|
// État des clients
|
|
const [clients, setClients] = useState<ClientResponse[]>([]);
|
|
const [filteredClients, setFilteredClients] = useState<ClientResponse[]>(
|
|
[],
|
|
);
|
|
const [totalClients, setTotalClients] = useState(0);
|
|
|
|
// État des filtres et recherche
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [clientSearchTerm, setClientSearchTerm] = useState("");
|
|
const [roleFilter, setRoleFilter] = useState<RoleFilter>("all");
|
|
|
|
// État du modal
|
|
const [selectedClient, setSelectedClient] = useState<ClientResponse | null>(
|
|
null,
|
|
);
|
|
const [showClientModal, setShowClientModal] = useState(false);
|
|
|
|
// État de chargement
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// ============================================
|
|
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
|
// ============================================
|
|
useEffect(() => {
|
|
const checkAuth = () => {
|
|
if (!isCabineAuthenticated()) {
|
|
console.log(
|
|
"❌ [UserManagement] Cabine non authentifié, redirection vers /login-cabine/cabine",
|
|
);
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [navigate]);
|
|
|
|
// ============================================
|
|
// 🔐 VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
|
// ============================================
|
|
useEffect(() => {
|
|
const authInterval = setInterval(() => {
|
|
if (!isCabineAuthenticated()) {
|
|
console.log(
|
|
"❌ [UserManagement] Session cabine expirée, redirection vers /login-cabine/cabine",
|
|
);
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearInterval(authInterval);
|
|
}, [navigate]);
|
|
|
|
// ============================================
|
|
// FETCH DATA
|
|
// ============================================
|
|
|
|
const fetchUsers = useCallback(async () => {
|
|
// ✅ Vérifier l'auth avant de charger
|
|
if (!isCabineAuthenticated()) {
|
|
console.log("❌ [fetchUsers] Cabine non authentifié");
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("👥 [USERS_MANAGEMENT] Chargement des utilisateurs...");
|
|
setLoading(true);
|
|
|
|
const usersData = await getAllUsers();
|
|
console.log(
|
|
"✅ [USERS_MANAGEMENT] Utilisateurs récupérés:",
|
|
usersData,
|
|
);
|
|
|
|
// Exclure les admins de la liste
|
|
const filteredUsersData = usersData.filter(
|
|
(u) => u.role !== "admin",
|
|
);
|
|
setUsers(filteredUsersData);
|
|
|
|
// Calculer les statistiques
|
|
const newStats: UserStats = {
|
|
totalUsers: filteredUsersData.length,
|
|
totalCabines: filteredUsersData.filter(
|
|
(u) => u.role === "cabine",
|
|
).length,
|
|
totalLivreurs: filteredUsersData.filter(
|
|
(u) => u.role === "livreur",
|
|
).length,
|
|
};
|
|
setStats(newStats);
|
|
|
|
console.log("📊 [USERS_MANAGEMENT] Stats:", newStats);
|
|
} catch (error) {
|
|
console.error("❌ [USERS_MANAGEMENT] Erreur chargement:", error);
|
|
|
|
if (error instanceof Error && error.message.includes("401")) {
|
|
console.log("🔓 [UserManagement] Token invalide");
|
|
sessionStorage.removeItem("admin_token");
|
|
sessionStorage.removeItem("admin_username");
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
return;
|
|
}
|
|
|
|
alert("Erreur lors du chargement des utilisateurs");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [navigate]);
|
|
|
|
const fetchClients = useCallback(async () => {
|
|
// ✅ Vérifier l'auth avant de charger
|
|
if (!isCabineAuthenticated()) {
|
|
console.log("❌ [fetchClients] Cabine non authentifié");
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("👥 [CLIENTS_MANAGEMENT] Chargement des clients...");
|
|
setLoading(true);
|
|
|
|
const clientData = await getAllClients();
|
|
console.log(
|
|
"✅ [CLIENTS_MANAGEMENT] Clients récupérés:",
|
|
clientData,
|
|
);
|
|
|
|
if (clientData && clientData.length > 0) {
|
|
const firstClient = clientData[0];
|
|
console.log("🔍 [DEBUG] Premier client:", {
|
|
username: firstClient.username,
|
|
points_weed: firstClient.point,
|
|
points_zipette: firstClient.points_zipette,
|
|
});
|
|
}
|
|
|
|
setClients(clientData);
|
|
setTotalClients(clientData.length);
|
|
} catch (error) {
|
|
console.error("❌ [CLIENTS_MANAGEMENT] Erreur:", error);
|
|
|
|
if (error instanceof Error && error.message.includes("401")) {
|
|
console.log("🔓 [UserManagement] Token invalide");
|
|
sessionStorage.removeItem("admin_token");
|
|
sessionStorage.removeItem("admin_username");
|
|
navigate("/login-cabine/cabine", { replace: true });
|
|
return;
|
|
}
|
|
|
|
alert("Erreur lors du chargement des clients");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [navigate]);
|
|
|
|
// ============================================
|
|
// FILTERS
|
|
// ============================================
|
|
|
|
const filterUsers = useCallback(() => {
|
|
let filtered = [...users];
|
|
|
|
if (searchTerm) {
|
|
filtered = filtered.filter((user) =>
|
|
user.username.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
if (roleFilter !== "all") {
|
|
filtered = filtered.filter((user) => user.role === roleFilter);
|
|
}
|
|
|
|
setFilteredUsers(filtered);
|
|
}, [users, searchTerm, roleFilter]);
|
|
|
|
const filterClients = useCallback(() => {
|
|
let filtered = [...clients];
|
|
|
|
if (clientSearchTerm) {
|
|
const searchLower = clientSearchTerm.toLowerCase();
|
|
filtered = filtered.filter(
|
|
(client) =>
|
|
client.nom.toLowerCase().includes(searchLower) ||
|
|
client.prenom.toLowerCase().includes(searchLower) ||
|
|
client.username.toLowerCase().includes(searchLower) ||
|
|
client.telephone.includes(clientSearchTerm),
|
|
);
|
|
}
|
|
|
|
setFilteredClients(filtered);
|
|
}, [clients, clientSearchTerm]);
|
|
|
|
// ============================================
|
|
// HANDLERS
|
|
// ============================================
|
|
|
|
const handleViewClient = useCallback((client: ClientResponse) => {
|
|
setSelectedClient(client);
|
|
setShowClientModal(true);
|
|
}, []);
|
|
|
|
const handleCloseModal = useCallback(() => {
|
|
setShowClientModal(false);
|
|
setSelectedClient(null);
|
|
}, []);
|
|
|
|
const handleRoleFilterChange = useCallback((filter: RoleFilter) => {
|
|
setRoleFilter(filter);
|
|
}, []);
|
|
|
|
// ============================================
|
|
// EFFECTS
|
|
// ============================================
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
fetchClients();
|
|
}, [fetchUsers, fetchClients]);
|
|
|
|
useEffect(() => {
|
|
filterUsers();
|
|
}, [filterUsers]);
|
|
|
|
useEffect(() => {
|
|
filterClients();
|
|
}, [filterClients]);
|
|
|
|
// ============================================
|
|
// RENDER HELPERS
|
|
// ============================================
|
|
|
|
const getRoleConfig = (role: string): RoleConfig => {
|
|
return ROLE_CONFIG[role as UserRole] || ROLE_CONFIG.client;
|
|
};
|
|
|
|
const getRoleIcon = (role: string): React.ReactElement => {
|
|
const config = getRoleConfig(role);
|
|
const IconComponent = config.icon;
|
|
return <IconComponent size={18} />;
|
|
};
|
|
|
|
// ============================================
|
|
// LOADING STATE
|
|
// ============================================
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="users-management-container">
|
|
<div className="loading-users">
|
|
<p>Chargement des utilisateurs...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// MAIN RENDER
|
|
// ============================================
|
|
|
|
return (
|
|
<>
|
|
<SidebarCabine />
|
|
|
|
<div className="users-management-container">
|
|
<Header />
|
|
<StatsGrid stats={stats} totalClients={totalClients} />
|
|
<FiltersSection
|
|
searchTerm={searchTerm}
|
|
setSearchTerm={setSearchTerm}
|
|
roleFilter={roleFilter}
|
|
onRoleFilterChange={handleRoleFilterChange}
|
|
stats={stats}
|
|
usersCount={users.length}
|
|
/>
|
|
<UsersList
|
|
filteredUsers={filteredUsers}
|
|
getRoleIcon={getRoleIcon}
|
|
getRoleConfig={getRoleConfig}
|
|
/>
|
|
<ClientsSection
|
|
clients={clients}
|
|
filteredClients={filteredClients}
|
|
clientSearchTerm={clientSearchTerm}
|
|
setClientSearchTerm={setClientSearchTerm}
|
|
onViewClient={handleViewClient}
|
|
/>
|
|
{showClientModal && selectedClient && (
|
|
<ClientModal
|
|
client={selectedClient}
|
|
onClose={handleCloseModal}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// SUB-COMPONENTS (identiques à l'original)
|
|
// ============================================
|
|
|
|
function Header() {
|
|
return (
|
|
<div className="dashboard-header">
|
|
<h1>Gestion des Utilisateurs</h1>
|
|
<p className="dashboard-subtitle">
|
|
Visualiser et gérer tous les comptes système
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface StatsGridProps {
|
|
stats: UserStats;
|
|
totalClients: number;
|
|
}
|
|
|
|
function StatsGrid({ stats, totalClients }: StatsGridProps) {
|
|
const statsCards = [
|
|
{
|
|
icon: User,
|
|
color: "blue",
|
|
label: "Total Utilisateurs",
|
|
value: stats.totalUsers,
|
|
},
|
|
{
|
|
icon: Store,
|
|
color: "purple",
|
|
label: "Cabines",
|
|
value: stats.totalCabines,
|
|
},
|
|
{
|
|
icon: Truck,
|
|
color: "blue",
|
|
label: "Livreurs",
|
|
value: stats.totalLivreurs,
|
|
},
|
|
{
|
|
icon: User,
|
|
color: "green",
|
|
label: "Clients",
|
|
value: totalClients,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="stats-grid">
|
|
{statsCards.map((card, index) => (
|
|
<StatCard key={index} {...card} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface StatCardProps {
|
|
icon: React.ComponentType<{ size: number }>;
|
|
color: string;
|
|
label: string;
|
|
value: number;
|
|
}
|
|
|
|
function StatCard({ icon: Icon, color, label, value }: StatCardProps) {
|
|
return (
|
|
<div className="stat-card">
|
|
<div className={`stat-icon ${color}`}>
|
|
<Icon size={24} />
|
|
</div>
|
|
<div className="stat-content">
|
|
<p className="stat-label">{label}</p>
|
|
<h3 className="stat-value">{value}</h3>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FiltersSectionProps {
|
|
searchTerm: string;
|
|
setSearchTerm: (term: string) => void;
|
|
roleFilter: RoleFilter;
|
|
onRoleFilterChange: (filter: RoleFilter) => void;
|
|
stats: UserStats;
|
|
usersCount: number;
|
|
}
|
|
|
|
function FiltersSection({
|
|
searchTerm,
|
|
setSearchTerm,
|
|
roleFilter,
|
|
onRoleFilterChange,
|
|
stats,
|
|
usersCount,
|
|
}: FiltersSectionProps) {
|
|
const filters = [
|
|
{
|
|
value: "all" as RoleFilter,
|
|
icon: Filter,
|
|
label: "Tous",
|
|
count: usersCount,
|
|
},
|
|
{
|
|
value: "cabine" as RoleFilter,
|
|
icon: Store,
|
|
label: "Cabines",
|
|
count: stats.totalCabines,
|
|
},
|
|
{
|
|
value: "livreur" as RoleFilter,
|
|
icon: Truck,
|
|
label: "Livreurs",
|
|
count: stats.totalLivreurs,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="filters-section">
|
|
<div className="search-bar">
|
|
<Search size={20} />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher par username..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="role-filters">
|
|
{filters.map((filter) => {
|
|
const IconComponent = filter.icon;
|
|
return (
|
|
<button
|
|
key={filter.value}
|
|
className={`filter-btn ${roleFilter === filter.value ? "active" : ""}`}
|
|
onClick={() => onRoleFilterChange(filter.value)}
|
|
>
|
|
<IconComponent size={18} />
|
|
{filter.label} ({filter.count})
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface UsersListProps {
|
|
filteredUsers: AdminResponse[];
|
|
getRoleIcon: (role: string) => React.ReactElement;
|
|
getRoleConfig: (role: string) => RoleConfig;
|
|
}
|
|
|
|
function UsersList({
|
|
filteredUsers,
|
|
getRoleIcon,
|
|
getRoleConfig,
|
|
}: UsersListProps) {
|
|
return (
|
|
<div className="users-section">
|
|
<h2>
|
|
<User size={24} />
|
|
Utilisateurs Système ({filteredUsers.length})
|
|
</h2>
|
|
|
|
<div className="users-list">
|
|
{filteredUsers.length === 0 ? (
|
|
<EmptyState
|
|
icon={User}
|
|
title="Aucun utilisateur trouvé"
|
|
message="Essayez de modifier vos filtres de recherche"
|
|
/>
|
|
) : (
|
|
filteredUsers.map((user) => (
|
|
<UserCard
|
|
key={user.id}
|
|
user={user}
|
|
getRoleIcon={getRoleIcon}
|
|
getRoleConfig={getRoleConfig}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface UserCardProps {
|
|
user: AdminResponse;
|
|
getRoleIcon: (role: string) => React.ReactElement;
|
|
getRoleConfig: (role: string) => RoleConfig;
|
|
}
|
|
|
|
function UserCard({ user, getRoleIcon, getRoleConfig }: UserCardProps) {
|
|
const roleConfig = getRoleConfig(user.role);
|
|
|
|
return (
|
|
<div className="user-card">
|
|
<div className="user-header">
|
|
<div className="user-info-main">
|
|
<div className="user-avatar">{getRoleIcon(user.role)}</div>
|
|
<div className="user-details">
|
|
<strong className="user-username">
|
|
{user.username}
|
|
</strong>
|
|
<span className="user-id">ID: {user.id}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`user-role-badge ${roleConfig.color}`}>
|
|
{getRoleIcon(user.role)}
|
|
<span>{roleConfig.label}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ClientsSectionProps {
|
|
clients: ClientResponse[];
|
|
filteredClients: ClientResponse[];
|
|
clientSearchTerm: string;
|
|
setClientSearchTerm: (term: string) => void;
|
|
onViewClient: (client: ClientResponse) => void;
|
|
}
|
|
|
|
function ClientsSection({
|
|
clients,
|
|
filteredClients,
|
|
clientSearchTerm,
|
|
setClientSearchTerm,
|
|
onViewClient,
|
|
}: ClientsSectionProps) {
|
|
return (
|
|
<div className="users-section clients-section">
|
|
<div className="section-header">
|
|
<h2>
|
|
<User size={24} />
|
|
Clients Enregistrés ({clients.length})
|
|
</h2>
|
|
<div className="search-bar compact">
|
|
<Search size={18} />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher un client..."
|
|
value={clientSearchTerm}
|
|
onChange={(e) => setClientSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="users-list">
|
|
{filteredClients.length === 0 ? (
|
|
<EmptyState
|
|
icon={User}
|
|
title="Aucun client trouvé"
|
|
message={
|
|
clientSearchTerm
|
|
? "Essayez de modifier votre recherche"
|
|
: "Les clients s'afficheront ici une fois inscrits"
|
|
}
|
|
/>
|
|
) : (
|
|
filteredClients.map((client) => (
|
|
<ClientCard
|
|
key={client.id}
|
|
client={client}
|
|
onViewClient={onViewClient}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ClientCardProps {
|
|
client: ClientResponse;
|
|
onViewClient: (client: ClientResponse) => void;
|
|
}
|
|
|
|
function ClientCard({ client, onViewClient }: ClientCardProps) {
|
|
return (
|
|
<div className="user-card client-card">
|
|
<div className="user-header">
|
|
<div className="user-info-main">
|
|
<div className="user-avatar client">
|
|
<User size={18} />
|
|
</div>
|
|
<div className="user-details">
|
|
<strong className="user-username">
|
|
{client.prenom} {client.nom}
|
|
</strong>
|
|
<span className="user-id">@{client.username}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="user-role-badge green">
|
|
<User size={18} />
|
|
<span>Client</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="client-info-grid">
|
|
<InfoItem icon={Phone} text={client.telephone} />
|
|
<InfoItem
|
|
icon={TrendingUp}
|
|
text={`${client.command} commande${client.command > 1 ? "s" : ""}`}
|
|
/>
|
|
<InfoItem icon={Leaf} text={`${client.point || 0} pts Weed`} />
|
|
<InfoItem
|
|
icon={Wind}
|
|
text={`${client.points_zipette || 0} pts Zipette`}
|
|
/>
|
|
<InfoItem
|
|
icon={AlertCircle}
|
|
text={`${client.amende}€ d'amendes`}
|
|
/>
|
|
</div>
|
|
|
|
<div className="user-actions">
|
|
<button
|
|
className="btn-view"
|
|
onClick={() => onViewClient(client)}
|
|
>
|
|
<Eye size={18} />
|
|
Voir détails
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface InfoItemProps {
|
|
icon: React.ComponentType<{ size: number }>;
|
|
text: string;
|
|
}
|
|
|
|
function InfoItem({ icon: Icon, text }: InfoItemProps) {
|
|
return (
|
|
<div className="info-item">
|
|
<Icon size={16} />
|
|
<span>{text}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ClientModalProps {
|
|
client: ClientResponse;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function ClientModal({ client, onClose }: ClientModalProps) {
|
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="modal-overlay" onClick={handleBackdropClick}>
|
|
<div
|
|
className="edit-modal-content client-modal"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="edit-modal-header">
|
|
<div className="modal-header-title">
|
|
<User size={24} />
|
|
<h3>Détails du Client</h3>
|
|
</div>
|
|
<button className="modal-close-btn" onClick={onClose}>
|
|
<XCircle size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="edit-modal-body">
|
|
<DetailSection title="Informations personnelles">
|
|
<DetailItem
|
|
icon={User}
|
|
label="Prénom"
|
|
value={client.prenom}
|
|
/>
|
|
<DetailItem
|
|
icon={User}
|
|
label="Nom"
|
|
value={client.nom}
|
|
/>
|
|
<DetailItem
|
|
icon={Shield}
|
|
label="Username"
|
|
value={`@${client.username}`}
|
|
/>
|
|
<DetailItem
|
|
icon={Phone}
|
|
label="Téléphone"
|
|
value={client.telephone}
|
|
/>
|
|
</DetailSection>
|
|
|
|
<DetailSection title="Statistiques">
|
|
<DetailItem
|
|
icon={TrendingUp}
|
|
label="Commandes"
|
|
value={`${client.command} commande${client.command > 1 ? "s" : ""}`}
|
|
/>
|
|
<DetailItem
|
|
icon={Leaf}
|
|
label="Points Weed/Hash"
|
|
value={`${client.point || 0} points`}
|
|
/>
|
|
<DetailItem
|
|
icon={Wind}
|
|
label="Points Zipette"
|
|
value={`${client.points_zipette || 0} points`}
|
|
/>
|
|
<DetailItem
|
|
icon={AlertCircle}
|
|
label="Amendes"
|
|
value={`${client.amende}€`}
|
|
/>
|
|
<DetailItem
|
|
icon={XCircle}
|
|
label="Annulations"
|
|
value={String(client.cancellations_count)}
|
|
/>
|
|
{client.last_penalty_reason && (
|
|
<DetailItem
|
|
icon={AlertCircle}
|
|
label="Dernière pénalité"
|
|
value={client.last_penalty_reason}
|
|
fullWidth
|
|
/>
|
|
)}
|
|
</DetailSection>
|
|
|
|
<DetailSection title="Informations système">
|
|
<DetailItem
|
|
icon={Shield}
|
|
label="ID Client"
|
|
value={`#${client.id}`}
|
|
/>
|
|
</DetailSection>
|
|
|
|
<div className="modal-actions">
|
|
<button className="btn-cancel" onClick={onClose}>
|
|
<XCircle size={18} />
|
|
Fermer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DetailSectionProps {
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function DetailSection({ title, children }: DetailSectionProps) {
|
|
return (
|
|
<div className="client-detail-section">
|
|
<h4>{title}</h4>
|
|
<div className="detail-grid">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DetailItemProps {
|
|
icon: React.ComponentType<{ size: number }>;
|
|
label: string;
|
|
value: string;
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
function DetailItem({ icon: Icon, label, value, fullWidth }: DetailItemProps) {
|
|
return (
|
|
<div className={`detail-item ${fullWidth ? "full" : ""}`}>
|
|
<label>
|
|
<Icon size={16} />
|
|
{label}
|
|
</label>
|
|
<p>{value}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface EmptyStateProps {
|
|
icon: React.ComponentType<{ size: number }>;
|
|
title: string;
|
|
message: string;
|
|
}
|
|
|
|
function EmptyState({ icon: Icon, title, message }: EmptyStateProps) {
|
|
return (
|
|
<div className="empty-state">
|
|
<div style={{ opacity: 0.3 }}>
|
|
<Icon size={64} />
|
|
</div>
|
|
<h3>{title}</h3>
|
|
<p>{message}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default UsersManagement;
|