feat: add home featured toggle, improve account deletion UI, and update business schema and database configuration.
This commit is contained in:
@@ -24,21 +24,24 @@ export async function setFeaturedBusiness(id: string, data: {
|
||||
keyMetric: string;
|
||||
}) {
|
||||
try {
|
||||
// 1. Reset all others (we only want one entrepreneur of the month)
|
||||
// 1. Réinitialiser les autres pour n'avoir qu'une seule Tête d'affiche Afroshine active
|
||||
await prisma.business.updateMany({
|
||||
where: { isFeatured: true },
|
||||
data: { isFeatured: false },
|
||||
});
|
||||
|
||||
// 2. Set the new one
|
||||
// 2. Mettre à jour l'entreprise choisie
|
||||
await prisma.business.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isFeatured: true,
|
||||
...data,
|
||||
founderName: data.founderName || null,
|
||||
founderImageUrl: data.founderImageUrl || null,
|
||||
keyMetric: data.keyMetric || null,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/users");
|
||||
revalidatePath("/entrepreneurs");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
@@ -48,12 +51,28 @@ export async function setFeaturedBusiness(id: string, data: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleHomeFeatured(id: string, currentStatus: boolean) {
|
||||
try {
|
||||
await prisma.business.update({
|
||||
where: { id },
|
||||
data: { isHomeFeatured: !currentStatus },
|
||||
});
|
||||
revalidatePath("/users");
|
||||
revalidatePath("/");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle home featured:", error);
|
||||
return { success: false, error: "Erreur lors de la mise à jour de la mise à la une" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFeaturedBusiness(id: string) {
|
||||
try {
|
||||
await prisma.business.update({
|
||||
where: { id },
|
||||
data: { isFeatured: false },
|
||||
});
|
||||
revalidatePath("/users");
|
||||
revalidatePath("/entrepreneurs");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,19 +6,48 @@ import {
|
||||
Store,
|
||||
Clock,
|
||||
MessageCircle,
|
||||
Eye
|
||||
Eye,
|
||||
CheckCircle2,
|
||||
UserPlus
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Stats {
|
||||
usersCount: number;
|
||||
businessCount: number;
|
||||
pendingCount: number;
|
||||
commentsCount: number;
|
||||
totalViews: number;
|
||||
uniqueVisitors: number;
|
||||
interface DashboardData {
|
||||
stats: {
|
||||
usersCount: number;
|
||||
businessCount: number;
|
||||
pendingCount: number;
|
||||
commentsCount: number;
|
||||
totalViews: number;
|
||||
uniqueVisitors: number;
|
||||
};
|
||||
latestBusinesses: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
logoUrl: string;
|
||||
location: string;
|
||||
createdAt: Date;
|
||||
verified: boolean;
|
||||
plan: string;
|
||||
owner?: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
}>;
|
||||
activities: Array<{
|
||||
id: string;
|
||||
type: 'comment' | 'user';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
timestamp: Date;
|
||||
avatar?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function DashboardClient({ stats }: { stats: Stats }) {
|
||||
export default function DashboardClient({ initialData }: { initialData: DashboardData }) {
|
||||
const { stats, latestBusinesses, activities } = initialData;
|
||||
|
||||
const statCards = [
|
||||
{ label: 'Utilisateurs', value: stats.usersCount, icon: Users, color: 'text-blue-400' },
|
||||
{ label: 'Visiteurs Uniques', value: stats.uniqueVisitors, icon: Eye, color: 'text-indigo-400' },
|
||||
@@ -50,16 +79,86 @@ export default function DashboardClient({ stats }: { stats: Stats }) {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="card">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Derniers entrepreneurs</h2>
|
||||
<div className="space-y-4">
|
||||
<p className="text-slate-500 italic">Bientôt disponible...</p>
|
||||
{/* Derniers entrepreneurs */}
|
||||
<div className="card flex flex-col">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold text-white">Derniers entrepreneurs</h2>
|
||||
<Link href="/users" className="text-xs text-indigo-400 hover:underline font-semibold">
|
||||
Voir tout
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 flex-1">
|
||||
{latestBusinesses.length === 0 ? (
|
||||
<p className="text-slate-500 italic py-8 text-center">Aucune entreprise récente.</p>
|
||||
) : (
|
||||
latestBusinesses.map((b) => (
|
||||
<div key={b.id} className="flex items-center justify-between p-3 rounded-xl bg-slate-800/40 hover:bg-slate-800/80 transition-colors border border-slate-800">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-800 overflow-hidden flex items-center justify-center border border-slate-700 flex-shrink-0">
|
||||
{b.logoUrl ? (
|
||||
<img src={b.logoUrl} alt={b.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Store className="w-4 h-4 text-slate-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-white text-sm truncate">{b.name}</div>
|
||||
<div className="text-xs text-slate-400 truncate">{b.category} • {b.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0 ml-3">
|
||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase ${
|
||||
b.plan === 'EMPIRE' ? 'bg-purple-500/10 text-purple-400 border border-purple-500/20' :
|
||||
b.plan === 'BOOSTER' ? 'bg-amber-500/10 text-amber-400 border border-amber-500/20' :
|
||||
'bg-slate-500/10 text-slate-400'
|
||||
}`}>
|
||||
{b.plan}
|
||||
</span>
|
||||
{b.verified ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" title="Vérifié" />
|
||||
) : (
|
||||
<Clock className="w-4 h-4 text-amber-500" title="En attente" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Activité récente</h2>
|
||||
<div className="space-y-4">
|
||||
<p className="text-slate-500 italic">Bientôt disponible...</p>
|
||||
|
||||
{/* Activité récente */}
|
||||
<div className="card flex flex-col">
|
||||
<h2 className="text-xl font-bold text-white mb-6">Activité récente</h2>
|
||||
|
||||
<div className="space-y-4 flex-1">
|
||||
{activities.length === 0 ? (
|
||||
<p className="text-slate-500 italic py-8 text-center">Aucune activité enregistrée.</p>
|
||||
) : (
|
||||
activities.map((act) => (
|
||||
<div key={act.id} className="flex items-start gap-3.5 p-3 rounded-xl bg-slate-800/20 border border-slate-800/50">
|
||||
<div className="mt-0.5 w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center border border-slate-700 flex-shrink-0 overflow-hidden">
|
||||
{act.avatar ? (
|
||||
<img src={act.avatar} alt="" className="w-full h-full object-cover" />
|
||||
) : act.type === 'comment' ? (
|
||||
<MessageCircle className="w-3.5 h-3.5 text-purple-400" />
|
||||
) : (
|
||||
<UserPlus className="w-3.5 h-3.5 text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold text-slate-200 truncate">{act.title}</p>
|
||||
<span className="text-[10px] text-slate-500 flex-shrink-0">
|
||||
{new Date(act.timestamp).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">{act.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import DashboardClient from './DashboardClient';
|
||||
|
||||
async function getStats() {
|
||||
async function getDashboardData() {
|
||||
const usersCount = await prisma.user.count();
|
||||
const businessCount = await prisma.business.count();
|
||||
const pendingCount = await prisma.business.count({ where: { verified: false } });
|
||||
@@ -21,10 +21,81 @@ async function getStats() {
|
||||
});
|
||||
const uniqueVisitors = uniqueVisitorsRes.length;
|
||||
|
||||
return { usersCount, businessCount, pendingCount, commentsCount, totalViews, uniqueVisitors };
|
||||
// Derniers entrepreneurs
|
||||
const latestBusinesses = await prisma.business.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
category: true,
|
||||
logoUrl: true,
|
||||
location: true,
|
||||
createdAt: true,
|
||||
verified: true,
|
||||
plan: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Activité récente (combiner commentaires et nouveaux utilisateurs)
|
||||
const recentComments = await prisma.comment.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
author: { select: { name: true, avatar: true } },
|
||||
business: { select: { name: true } }
|
||||
}
|
||||
});
|
||||
|
||||
const recentUsers = await prisma.user.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
role: true,
|
||||
avatar: true,
|
||||
createdAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Mapper et trier
|
||||
const activities = [
|
||||
...recentComments.map(c => ({
|
||||
id: `comment-${c.id}`,
|
||||
type: 'comment' as const,
|
||||
title: `${c.author?.name || 'Un utilisateur'} a commenté sur ${c.business?.name || 'une entreprise'}`,
|
||||
subtitle: c.content.length > 60 ? c.content.substring(0, 60) + '...' : c.content,
|
||||
timestamp: c.createdAt,
|
||||
avatar: c.author?.avatar
|
||||
})),
|
||||
...recentUsers.map(u => ({
|
||||
id: `user-${u.id}`,
|
||||
type: 'user' as const,
|
||||
title: `Nouvel utilisateur inscrit`,
|
||||
subtitle: `${u.name} a rejoint la plateforme en tant que ${u.role === 'ENTREPRENEUR' ? 'Entrepreneur' : 'Visiteur'}`,
|
||||
timestamp: u.createdAt,
|
||||
avatar: u.avatar
|
||||
}))
|
||||
].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, 6);
|
||||
|
||||
return {
|
||||
stats: { usersCount, businessCount, pendingCount, commentsCount, totalViews, uniqueVisitors },
|
||||
latestBusinesses,
|
||||
activities
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const stats = await getStats();
|
||||
return <DashboardClient stats={stats} />;
|
||||
const data = await getDashboardData();
|
||||
return <DashboardClient initialData={data} />;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
@@ -42,12 +43,16 @@ body {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
margin-left: 260px;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
width: calc(100% - 260px);
|
||||
max-width: calc(100vw - 260px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function RootLayout({
|
||||
<body className={inter.className}>
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="admin-content flex-1">
|
||||
<main className="admin-content flex-1 min-w-0">
|
||||
<Toaster position="top-right" />
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma';
|
||||
import { getClients } from "@/app/actions/user";
|
||||
import ToggleVerifyButton from '@/components/ToggleVerifyButton';
|
||||
import FeaturedModal from '@/components/FeaturedModal';
|
||||
import ToggleHomeFeatureButton from '@/components/ToggleHomeFeatureButton';
|
||||
import EntrepreneurActions from '@/components/EntrepreneurActions';
|
||||
import PlanSelector from '@/components/PlanSelector';
|
||||
import ClientActions from "@/components/ClientActions";
|
||||
@@ -9,6 +10,7 @@ import ManualVerifyButton from "@/components/ManualVerifyButton";
|
||||
import { Trash2, ShieldAlert, ShieldX, ShieldCheck, Users, Briefcase, User, Mail, Phone, MapPin, MailCheck, MailX } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
// Force Turbopack native load
|
||||
async function getBusinesses() {
|
||||
return await prisma.business.findMany({
|
||||
where: {
|
||||
@@ -87,30 +89,32 @@ export default async function UsersPage({
|
||||
)}
|
||||
|
||||
<div className="card overflow-hidden p-0">
|
||||
<table className="data-table">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table min-w-[1200px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entreprise</th>
|
||||
<th className="sticky left-0 z-30 bg-[#1e293b] border-r border-slate-700">Entreprise</th>
|
||||
<th>Catégorie</th>
|
||||
<th>Propriétaire</th>
|
||||
<th>Email</th>
|
||||
<th>Statut</th>
|
||||
<th className="text-center">Forfait</th>
|
||||
<th className="text-center">Mise en avant</th>
|
||||
<th className="text-center whitespace-nowrap">À la une (Accueil)</th>
|
||||
<th className="text-center whitespace-nowrap">Afroshine (Annuaire)</th>
|
||||
<th className="text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{businesses.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-10 text-slate-500">
|
||||
<td colSpan={9} className="text-center py-10 text-slate-500">
|
||||
Aucun entrepreneur trouvé.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
businesses.map((business: any) => (
|
||||
<tr key={business.id} className={`transition-colors ${business.owner?.deletedAt ? 'bg-red-500/5 opacity-80' : (business.isSuspended || business.owner?.isSuspended) ? 'opacity-60 grayscale-[0.5]' : ''}`}>
|
||||
<td>
|
||||
<tr key={business.id} className={`transition-colors ${business.owner?.deletedAt ? 'bg-red-500/5' : (business.isSuspended || business.owner?.isSuspended) ? 'grayscale-[0.5]' : ''}`}>
|
||||
<td className="sticky left-0 z-20 bg-[#1e293b] border-r border-slate-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center overflow-hidden">
|
||||
{business.logoUrl ? (
|
||||
@@ -129,11 +133,11 @@ export default async function UsersPage({
|
||||
<span className="text-sm text-slate-300">{business.category}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-sm text-white">{business.owner.name}</div>
|
||||
<div className="text-xs text-slate-500">{business.owner.email}</div>
|
||||
<div className="text-sm text-white">{business.owner?.name || 'Inconnu'}</div>
|
||||
<div className="text-xs text-slate-500">{business.owner?.email || 'N/A'}</div>
|
||||
</td>
|
||||
<td>
|
||||
{business.owner.emailVerified ? (
|
||||
{business.owner?.emailVerified ? (
|
||||
<span className="flex items-center gap-1.5 text-[10px] text-emerald-500 font-bold uppercase">
|
||||
<MailCheck className="w-3.5 h-3.5" /> Vérifié
|
||||
</span>
|
||||
@@ -142,7 +146,7 @@ export default async function UsersPage({
|
||||
<span className="flex items-center gap-1.5 text-[10px] text-rose-500 font-bold uppercase">
|
||||
<MailX className="w-3.5 h-3.5" /> Non vérifié
|
||||
</span>
|
||||
<ManualVerifyButton userId={business.owner.id} />
|
||||
{business.owner?.id && <ManualVerifyButton userId={business.owner.id} />}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -170,6 +174,9 @@ export default async function UsersPage({
|
||||
<td className="text-center">
|
||||
<PlanSelector businessId={business.id} currentPlan={business.plan} />
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<ToggleHomeFeatureButton id={business.id} isHomeFeatured={!!business.isHomeFeatured} />
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<FeaturedModal business={business} />
|
||||
</td>
|
||||
@@ -180,8 +187,8 @@ export default async function UsersPage({
|
||||
<EntrepreneurActions
|
||||
businessId={business.id}
|
||||
businessName={business.name}
|
||||
userId={business.owner.id}
|
||||
userName={business.owner.name}
|
||||
userId={business.owner?.id || ''}
|
||||
userName={business.owner?.name || 'Inconnu'}
|
||||
isBusinessSuspended={!!business.isSuspended}
|
||||
isUserSuspended={!!business.owner?.isSuspended}
|
||||
metaTitle={business.metaTitle}
|
||||
@@ -194,15 +201,16 @@ export default async function UsersPage({
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card overflow-hidden">
|
||||
<div className="card overflow-hidden p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table">
|
||||
<table className="data-table min-w-[1000px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Utilisateur</th>
|
||||
<th className="sticky left-0 z-30 bg-[#1e293b] border-r border-slate-700">Utilisateur</th>
|
||||
<th>Contact</th>
|
||||
<th>Vérification</th>
|
||||
<th>Localisation</th>
|
||||
@@ -222,8 +230,8 @@ export default async function UsersPage({
|
||||
</tr>
|
||||
) : (
|
||||
clients.map((client: any) => (
|
||||
<tr key={client.id} className={`hover:bg-slate-800/30 transition-colors ${client.deletedAt ? 'bg-red-500/5 opacity-80' : client.isSuspended ? 'opacity-60 grayscale-[0.5]' : ''}`}>
|
||||
<td className="py-4">
|
||||
<tr key={client.id} className={`hover:bg-slate-800/30 transition-colors ${client.deletedAt ? 'bg-red-500/5' : client.isSuspended ? 'grayscale-[0.5]' : ''}`}>
|
||||
<td className="sticky left-0 z-20 bg-[#1e293b] border-r border-slate-700 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 font-bold text-sm overflow-hidden border border-slate-700">
|
||||
{client.avatar ? (
|
||||
|
||||
Reference in New Issue
Block a user