feat: improve user management and profile features

- Replace email verification status with active/inactive status
- Add user archiving (soft delete) instead of hard delete
- Add search by name/email in user management
- Add status filter (active/inactive) in user management
- Simplify actions to show only "Détails" button
- Add Google Maps with clickable marker on contact page
- Update button labels from "Désactiver" to "Supprimer"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-11-28 13:59:52 +01:00
parent c4ac79ef8b
commit dce1559a32
16 changed files with 245 additions and 149 deletions

View File

@ -416,11 +416,6 @@ export default function AdminDashboardAdvanced() {
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
<StatRow
label="Emails vérifiés"
value={stats?.users?.verifiedEmails || 0}
color="green"
/>
</div>
</div>
</div>

View File

@ -203,11 +203,6 @@ export default function AdminDashboard() {
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
<StatRow
label="Emails vérifiés"
value={stats?.users?.verifiedEmails || 0}
color="green"
/>
</div>
</div>
</div>

View File

@ -416,11 +416,6 @@ export default function AdminDashboardAdvanced() {
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
<StatRow
label="Emails vérifiés"
value={stats?.users?.verifiedEmails || 0}
color="green"
/>
</div>
</div>
</div>

View File

@ -159,7 +159,6 @@ export default function MarketingPage() {
'Code Postal',
'Genre',
'Âge',
'Vérifié',
'A joué',
'A gagné',
'Nombre de tickets',
@ -173,7 +172,6 @@ export default function MarketingPage() {
user.postal_code || '',
user.gender || '',
user.age || '',
user.is_verified ? 'Oui' : 'Non',
user.has_played ? 'Oui' : 'Non',
user.has_won ? 'Oui' : 'Non',
user.ticket_count || 0,
@ -388,19 +386,7 @@ export default function MarketingPage() {
</div>
{/* Filtres additionnels */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={filters.verified}
onChange={(e) => setFilters({ ...filters, verified: e.target.checked })}
className="w-4 h-4"
/>
<span className="text-sm text-gray-700">Emails vérifiés uniquement</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Ville</label>
<select

View File

@ -184,18 +184,6 @@ export default function AdminProfilePage() {
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
</div>
<div className="p-6 space-y-4">
<div>
<label className="text-sm font-medium text-gray-600">
Email vérifié
</label>
<div className="mt-1">
{user.isVerified ? (
<Badge variant="success">Vérifié </Badge>
) : (
<Badge variant="warning">Non vérifié</Badge>
)}
</div>
</div>
<div>
<label className="text-sm font-medium text-gray-600">
Membre depuis

View File

@ -505,9 +505,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
Lots Gagnés
</th>
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
Statut
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
@ -537,19 +534,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
{participant.prizes_won}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{participant.is_verified ? (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-4 h-4 mr-1" />
Vérifié
</span>
) : (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
<AlertCircle className="w-4 h-4 mr-1" />
Non vérifié
</span>
)}
</td>
</tr>
))}
</tbody>

View File

@ -200,23 +200,13 @@ export default function UserDetailsPage() {
<h2 className="text-lg font-semibold text-gray-900 mb-4">Statut du compte</h2>
<div className="space-y-3">
<div>
<p className="text-sm font-medium text-gray-600 mb-1">Vérification email</p>
<p className="text-sm font-medium text-gray-600 mb-1">Statut</p>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
user.isVerified
user.isActive !== false
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{user.isVerified ? (
<>
<CheckCircle className="w-4 h-4 mr-1" />
Vérifié
</>
) : (
<>
<Clock className="w-4 h-4 mr-1" />
Non vérifié
</>
)}
{user.isActive !== false ? 'Actif' : 'Inactif'}
</span>
</div>
<div>
@ -245,14 +235,6 @@ export default function UserDetailsPage() {
>
Modifier l'utilisateur
</button>
{user.role === 'CLIENT' && (
<button
onClick={() => router.push(`/admin/tickets?userId=${user.id}`)}
className="w-full px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
Voir les tickets
</button>
)}
</div>
</div>
</div>

View File

@ -214,11 +214,16 @@ export default function ContactPage() {
<div className="w-12 h-12 bg-gradient-to-br from-[#d4a574] to-[#c4956a] rounded-full flex items-center justify-center text-xl shadow-md flex-shrink-0">📍</div>
<div>
<h3 className="font-semibold text-[#5a5a4e] mb-1">Siège social</h3>
<p className="text-[#8a8a7a] text-sm">
<a
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
target="_blank"
rel="noopener noreferrer"
className="text-[#8a8a7a] text-sm hover:text-[#d4a574] transition-colors block"
>
Thé Tip Top<br />
18 Avenue Thiers<br />
06000 Nice, France
</p>
</a>
</div>
</div>
@ -265,6 +270,44 @@ export default function ContactPage() {
</div>
</div>
</div>
{/* Google Maps */}
<div className="bg-white rounded-xl shadow-md p-4 border border-[#e5e4dc]">
<h2 className="text-xl font-bold text-[#5a5a4e] mb-4">Nous trouver</h2>
<div className="rounded-lg overflow-hidden relative">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2884.5!2d7.2619!3d43.7102!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12cdd0106a852d31%3A0x40819a5fd979a70!2s18%20Av.%20Thiers%2C%2006000%20Nice%2C%20France!5e0!3m2!1sfr!2sfr!4v1700000000000!5m2!1sfr!2sfr"
width="100%"
height="250"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Localisation Thé Tip Top - 18 Avenue Thiers, Nice"
></iframe>
{/* Point rouge cliquable */}
<a
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
target="_blank"
rel="noopener noreferrer"
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 group"
>
<span className="block w-6 h-6 bg-red-600 rounded-full border-2 border-white shadow-lg cursor-pointer hover:scale-125 transition-transform animate-pulse"></span>
<span className="absolute -bottom-1 left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[8px] border-l-transparent border-r-transparent border-t-red-600"></span>
</a>
</div>
<a
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
target="_blank"
rel="noopener noreferrer"
className="mt-3 inline-flex items-center gap-2 text-sm text-[#d4a574] hover:text-[#c4956a] transition-colors"
>
Ouvrir dans Google Maps
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
</div>
</div>
</div>

View File

@ -182,18 +182,6 @@ export default function EmployeProfilePage() {
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
</div>
<div className="p-6 space-y-4">
<div>
<label className="text-sm font-medium text-gray-600">
Email vérifié
</label>
<div className="mt-1">
{user.isVerified ? (
<Badge variant="success">Vérifié </Badge>
) : (
<Badge variant="warning">Non vérifié</Badge>
)}
</div>
</div>
<div>
<label className="text-sm font-medium text-gray-600">
Membre depuis

59
app/not-found.tsx Normal file
View File

@ -0,0 +1,59 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
export default function NotFound() {
const router = useRouter();
return (
<div className="min-h-screen bg-gradient-to-br from-[#f5f5f0] via-[#faf9f5] to-[#f5f5f0] flex items-center justify-center px-4">
<div className="text-center max-w-2xl mx-auto">
{/* Logo and 404 */}
<div className="mb-8">
<div className="flex justify-center mb-8">
<img
src="/logos/logo.svg"
alt="Thé Tip Top Logo"
className="w-64 h-auto md:w-80"
/>
</div>
<div className="text-[120px] md:text-[150px] leading-none font-bold text-[#d4a574] opacity-30 select-none">
404
</div>
</div>
{/* Text content */}
<h1 className="text-4xl md:text-5xl font-bold text-[#5a5a4e] mb-4">
Oups ! Page introuvable
</h1>
<p className="text-lg text-[#8a8a7a] mb-8 max-w-md mx-auto">
Il semble que cette page se soit évaporée comme la vapeur d'une bonne tasse de thé...
</p>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={() => router.back()}
className="inline-flex items-center justify-center gap-2 border-2 border-[#d4a574] text-[#d4a574] hover:bg-[#d4a574] hover:text-white font-bold px-8 py-3 rounded-lg transition-all duration-300"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Retour
</button>
<Link
href="/"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-8 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Accueil
</Link>
</div>
</div>
</div>
);
}

View File

@ -13,15 +13,18 @@ import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { ROUTES } from "@/utils/constants";
import { formatDate } from "@/utils/helpers";
import { Modal } from "@/components/ui/Modal";
export const dynamic = 'force-dynamic';
export default function ProfilePage() {
const { user, isAuthenticated, refreshUser } = useAuth();
const { user, isAuthenticated, refreshUser, logout } = useAuth();
const router = useRouter();
const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isReady, setIsReady] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const {
register,
@ -72,6 +75,21 @@ export default function ProfilePage() {
setIsEditing(false);
};
const handleDeleteAccount = async () => {
setIsDeleting(true);
try {
await userService.archiveAccount();
toast.success("Votre compte a été supprimé avec succès");
logout();
router.push(ROUTES.HOME);
} catch (error: any) {
toast.error(error.message || "Erreur lors de la suppression du compte");
} finally {
setIsDeleting(false);
setShowDeleteModal(false);
}
};
const getRoleBadgeVariant = (role: string) => {
switch (role) {
case "admin":
@ -145,13 +163,19 @@ export default function ProfilePage() {
</Badge>
</div>
</div>
<div className="pt-4">
<div className="pt-4 flex flex-wrap gap-3">
<button
onClick={() => setIsEditing(true)}
className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-6 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
>
Modifier mes informations
</button>
<button
onClick={() => setShowDeleteModal(true)}
className="bg-red-500 hover:bg-red-600 text-white font-bold px-6 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
>
Supprimer mon compte
</button>
</div>
</div>
) : (
@ -217,18 +241,6 @@ export default function ProfilePage() {
<h2 className="text-xl font-bold text-[#5a5a4e]">Statut du compte</h2>
</div>
<div className="p-6 space-y-4">
<div>
<label className="text-sm font-medium text-[#8a8a7a]">
Email vérifié
</label>
<div className="mt-1">
{user.isVerified ? (
<Badge variant="success">Vérifié </Badge>
) : (
<Badge variant="warning">Non vérifié</Badge>
)}
</div>
</div>
<div>
<label className="text-sm font-medium text-[#8a8a7a]">
Membre depuis
@ -280,9 +292,45 @@ export default function ProfilePage() {
)}
</div>
</div>
</div>
</div>
</div>
{/* Delete Confirmation Modal */}
<Modal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
title="Confirmer la suppression"
>
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-red-100 rounded-full">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-center text-gray-900 mb-2">
Êtes-vous sûr de vouloir supprimer votre compte ?
</h3>
<p className="text-sm text-center text-gray-600 mb-6">
Votre compte sera supprimé et vous ne pourrez plus vous connecter. Vos données seront conservées et un administrateur pourra réactiver votre compte si nécessaire.
</p>
<div className="flex gap-3">
<button
onClick={() => setShowDeleteModal(false)}
disabled={isDeleting}
className="flex-1 border-2 border-gray-300 hover:bg-gray-100 text-gray-700 font-bold px-6 py-3 rounded-lg transition-all"
>
Annuler
</button>
<button
onClick={handleDeleteAccount}
disabled={isDeleting}
className="flex-1 bg-red-500 hover:bg-red-600 disabled:bg-red-300 text-white font-bold px-6 py-3 rounded-lg transition-all"
>
{isDeleting ? "Suppression..." : "Supprimer"}
</button>
</div>
</Modal>
</div>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { adminService } from '@/services/admin.service';
import { User, CreateEmployeeData, UpdateUserData, PaginatedResponse } from '@/types';
@ -13,6 +13,20 @@ export default function UserManagement() {
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [filterRole, setFilterRole] = useState<string>('');
const [filterStatus, setFilterStatus] = useState<string>('');
const [searchQuery, setSearchQuery] = useState<string>('');
// Filtrage côté client par nom/email
const filteredUsers = useMemo(() => {
if (!searchQuery.trim()) return users;
const query = searchQuery.toLowerCase().trim();
return users.filter(user =>
user.firstName?.toLowerCase().includes(query) ||
user.lastName?.toLowerCase().includes(query) ||
user.email?.toLowerCase().includes(query) ||
`${user.firstName} ${user.lastName}`.toLowerCase().includes(query)
);
}, [users, searchQuery]);
// Modals
const [isCreateEmployeeModalOpen, setIsCreateEmployeeModalOpen] = useState(false);
@ -33,10 +47,14 @@ export default function UserManagement() {
try {
setLoading(true);
setError(null);
const filters: any = {};
if (filterRole) filters.role = filterRole;
if (filterStatus) filters.isActive = filterStatus === 'active';
const response: PaginatedResponse<User> = await adminService.getAllUsers(
page,
20,
filterRole ? { role: filterRole } : undefined
100, // Charger plus pour le filtrage côté client
Object.keys(filters).length > 0 ? filters : undefined
);
setUsers(response.data || []);
setTotalPages(response.totalPages || 1);
@ -46,7 +64,7 @@ export default function UserManagement() {
} finally {
setLoading(false);
}
}, [page, filterRole]);
}, [page, filterRole, filterStatus]);
useEffect(() => {
loadUsers();
@ -68,7 +86,6 @@ export default function UserManagement() {
setEditingUser(user);
setUserFormData({
role: user.role,
isVerified: user.isVerified,
firstName: user.firstName,
lastName: user.lastName,
});
@ -89,14 +106,15 @@ export default function UserManagement() {
}
};
const handleDeleteUser = async (userId: string) => {
if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) return;
const handleToggleUserStatus = async (user: User) => {
const action = user.isActive ? 'désactiver' : 'réactiver';
if (!confirm(`Êtes-vous sûr de vouloir ${action} cet utilisateur ?`)) return;
try {
await adminService.deleteUser(userId);
await adminService.updateUser(user.id, { isActive: !user.isActive });
loadUsers();
} catch (err: any) {
alert(err.message || 'Erreur lors de la suppression');
alert(err.message || `Erreur lors de la ${action}ation`);
}
};
@ -136,7 +154,16 @@ export default function UserManagement() {
)}
{/* Filtres */}
<div className="mb-4">
<div className="mb-4 flex flex-wrap gap-4">
<div className="flex-1 min-w-[250px]">
<input
type="text"
placeholder="Rechercher par nom ou email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full border rounded px-3 py-2"
/>
</div>
<select
value={filterRole}
onChange={(e) => {
@ -150,6 +177,18 @@ export default function UserManagement() {
<option value="EMPLOYEE">Employés</option>
<option value="ADMIN">Administrateurs</option>
</select>
<select
value={filterStatus}
onChange={(e) => {
setFilterStatus(e.target.value);
setPage(1);
}}
className="border rounded px-3 py-2"
>
<option value="">Tous les statuts</option>
<option value="active">Actifs</option>
<option value="inactive">Inactifs (archivés)</option>
</select>
</div>
{/* Table des utilisateurs */}
@ -178,14 +217,14 @@ export default function UserManagement() {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.length === 0 ? (
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">
Aucun utilisateur trouvé
{searchQuery ? 'Aucun résultat pour cette recherche' : 'Aucun utilisateur trouvé'}
</td>
</tr>
) : (
users.map((user) => (
filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
@ -201,31 +240,19 @@ export default function UserManagement() {
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isVerified ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
{user.isVerified ? 'Vérifié' : 'Non vérifié'}
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isActive !== false ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{user.isActive !== false ? 'Actif' : 'Inactif'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(user.createdAt).toLocaleDateString('fr-FR')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => router.push(`/admin/utilisateurs/${user.id}`)}
className="text-green-600 hover:text-green-900"
>
Détails
</button>
<button
onClick={() => handleEditUser(user)}
className="text-blue-600 hover:text-blue-900"
>
Modifier
</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="text-red-600 hover:text-red-900"
>
Supprimer
Détails
</button>
</td>
</tr>
@ -340,18 +367,6 @@ export default function UserManagement() {
<option value="ADMIN">Administrateur</option>
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="isVerified"
checked={userFormData.isVerified}
onChange={(e) => setUserFormData({ ...userFormData, isVerified: e.target.checked })}
className="rounded"
/>
<label htmlFor="isVerified" className="text-sm font-medium">
Email vérifié
</label>
</div>
<div className="flex gap-2 pt-4">
<button
type="submit"

View File

@ -134,12 +134,16 @@ export const adminService = {
limit = 20,
filters?: {
role?: string;
search?: string;
isActive?: boolean;
}
): Promise<PaginatedResponse<User>> => {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
...(filters?.role && { role: filters.role }),
...(filters?.search && { search: filters.search }),
...(filters?.isActive !== undefined && { isActive: filters.isActive.toString() }),
});
const response = await api.get<any>(
`${API_ENDPOINTS.USERS}?${params}`
@ -161,6 +165,7 @@ export const adminService = {
postalCode: user.postal_code || user.postalCode,
role: user.role,
isVerified: user.is_verified !== undefined ? user.is_verified : user.isVerified,
isActive: user.is_active !== undefined ? user.is_active : (user.isActive !== undefined ? user.isActive : true),
createdAt: user.created_at || user.createdAt,
ticketsCount: user.tickets_count || user.ticketsCount || 0,
}));

View File

@ -35,4 +35,14 @@ export const userService = {
changePassword: async (data: ChangePasswordData): Promise<void> => {
await api.post(API_ENDPOINTS.USER.CHANGE_PASSWORD, data);
},
// Archive account (désactive le compte au lieu de le supprimer)
archiveAccount: async (): Promise<void> => {
await api.put(API_ENDPOINTS.USER.UPDATE_PROFILE, { isActive: false });
},
// Delete account (kept for backwards compatibility)
deleteAccount: async (): Promise<void> => {
await api.delete(API_ENDPOINTS.USER.DELETE_ACCOUNT);
},
};

View File

@ -12,6 +12,7 @@ export interface User {
dateOfBirth?: string;
role: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
isVerified: boolean;
isActive: boolean;
createdAt: string;
updatedAt?: string;
// Ticket statistics (populated in getUserById)
@ -171,6 +172,7 @@ export interface CreateEmployeeData {
export interface UpdateUserData {
role?: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
isVerified?: boolean;
isActive?: boolean;
firstName?: string;
lastName?: string;
}

View File

@ -36,6 +36,7 @@ export const API_ENDPOINTS = {
PROFILE: '/users/profile',
UPDATE_PROFILE: '/users/profile',
CHANGE_PASSWORD: '/users/change-password',
DELETE_ACCOUNT: '/users/account',
},
NEWSLETTER: {
SUBSCRIBE: '/newsletter/subscribe',