diff --git a/.env b/.env index 73ab351..0946db5 100644 --- a/.env +++ b/.env @@ -1,9 +1,6 @@ # Connexion locale via le Docker Compose fourni : #DATABASE_URL="postgresql://admin:adminpassword@localhost:5432/afrohub_dev?schema=public" -DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?connect_timeout=300" - - -#SHADOW_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/shadow_db?schema=public" +DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?schema=public" # Exemple avec Supabase ou Neon (si base distante) # DATABASE_URL="postgresql://postgres:[MOT_DE_PASSE]@db.[ID_PROJET].supabase.co:5432/postgres" diff --git a/admin/src/app/actions/business.ts b/admin/src/app/actions/business.ts index 7469b6d..9d5d09d 100644 --- a/admin/src/app/actions/business.ts +++ b/admin/src/app/actions/business.ts @@ -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) { diff --git a/admin/src/app/dashboard/DashboardClient.tsx b/admin/src/app/dashboard/DashboardClient.tsx index a2abfaa..920077e 100644 --- a/admin/src/app/dashboard/DashboardClient.tsx +++ b/admin/src/app/dashboard/DashboardClient.tsx @@ -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 }) {
-
-

Derniers entrepreneurs

-
-

Bientôt disponible...

+ {/* Derniers entrepreneurs */} +
+
+

Derniers entrepreneurs

+ + Voir tout + +
+ +
+ {latestBusinesses.length === 0 ? ( +

Aucune entreprise récente.

+ ) : ( + latestBusinesses.map((b) => ( +
+
+
+ {b.logoUrl ? ( + {b.name} + ) : ( + + )} +
+
+
{b.name}
+
{b.category} • {b.location}
+
+
+ +
+ + {b.plan} + + {b.verified ? ( + + ) : ( + + )} +
+
+ )) + )}
-
-

Activité récente

-
-

Bientôt disponible...

+ + {/* Activité récente */} +
+

Activité récente

+ +
+ {activities.length === 0 ? ( +

Aucune activité enregistrée.

+ ) : ( + activities.map((act) => ( +
+
+ {act.avatar ? ( + + ) : act.type === 'comment' ? ( + + ) : ( + + )} +
+
+
+

{act.title}

+ + {new Date(act.timestamp).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} + +
+

{act.subtitle}

+
+
+ )) + )}
diff --git a/admin/src/app/dashboard/page.tsx b/admin/src/app/dashboard/page.tsx index 91f83d4..f8c15e0 100644 --- a/admin/src/app/dashboard/page.tsx +++ b/admin/src/app/dashboard/page.tsx @@ -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 ; + const data = await getDashboardData(); + return ; } diff --git a/admin/src/app/globals.css b/admin/src/app/globals.css index 7951e51..c9ab9a9 100644 --- a/admin/src/app/globals.css +++ b/admin/src/app/globals.css @@ -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 { diff --git a/admin/src/app/layout.tsx b/admin/src/app/layout.tsx index 062432b..310aa58 100644 --- a/admin/src/app/layout.tsx +++ b/admin/src/app/layout.tsx @@ -22,7 +22,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/admin/src/app/users/page.tsx b/admin/src/app/users/page.tsx index 8035e9a..88e3970 100644 --- a/admin/src/app/users/page.tsx +++ b/admin/src/app/users/page.tsx @@ -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({ )}
- +
+
- + - + + {businesses.length === 0 ? ( - ) : ( businesses.map((business: any) => ( - - + @@ -170,6 +174,9 @@ export default async function UsersPage({ + @@ -180,8 +187,8 @@ export default async function UsersPage({
EntrepriseEntreprise Catégorie Propriétaire Email Statut ForfaitMise en avantÀ la une (Accueil)Afroshine (Annuaire) Action
+ Aucun entrepreneur trouvé.
+
{business.logoUrl ? ( @@ -129,11 +133,11 @@ export default async function UsersPage({ {business.category}
-
{business.owner.name}
-
{business.owner.email}
+
{business.owner?.name || 'Inconnu'}
+
{business.owner?.email || 'N/A'}
- {business.owner.emailVerified ? ( + {business.owner?.emailVerified ? ( Vérifié @@ -142,7 +146,7 @@ export default async function UsersPage({ Non vérifié - + {business.owner?.id && } )} + +
+
) : ( -
+
- +
- + @@ -222,8 +230,8 @@ export default async function UsersPage({ ) : ( clients.map((client: any) => ( - - +
UtilisateurUtilisateur Contact Vérification Localisation
+
{client.avatar ? ( diff --git a/admin/src/components/BusinessSeoModal.tsx b/admin/src/components/BusinessSeoModal.tsx index 6dcde76..bf00644 100644 --- a/admin/src/components/BusinessSeoModal.tsx +++ b/admin/src/components/BusinessSeoModal.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState, useTransition } from 'react'; +import React, { useState, useEffect, useTransition } from 'react'; +import { createPortal } from 'react-dom'; import { X, Save, Loader2, Globe } from 'lucide-react'; import { updateBusinessSeo } from '@/app/actions/business'; import { toast } from 'react-hot-toast'; @@ -20,8 +21,14 @@ export default function BusinessSeoModal({ isOpen, onClose, business }: Business const [isPending, startTransition] = useTransition(); const [metaTitle, setMetaTitle] = useState(business.metaTitle || ''); const [metaDescription, setMetaDescription] = useState(business.metaDescription || ''); + const [mounted, setMounted] = useState(false); - if (!isOpen) return null; + useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + + if (!isOpen || !mounted) return null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -39,8 +46,8 @@ export default function BusinessSeoModal({ isOpen, onClose, business }: Business }); }; - return ( -
+ const modalContent = ( +
@@ -117,4 +124,6 @@ export default function BusinessSeoModal({ isOpen, onClose, business }: Business
); + + return createPortal(modalContent, document.body); } diff --git a/admin/src/components/DeleteAccountButton.tsx b/admin/src/components/DeleteAccountButton.tsx index 06328af..b8ac040 100644 --- a/admin/src/components/DeleteAccountButton.tsx +++ b/admin/src/components/DeleteAccountButton.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { UserX, Loader2 } from 'lucide-react'; import { scheduleUserDeletion } from '@/app/actions/user'; import { toast } from 'react-hot-toast'; @@ -16,6 +17,12 @@ export default function DeleteAccountButton({ userId, userName }: DeleteAccountB const [reason, setReason] = useState("Non-respect des conditions générales d'utilisation"); const [customReason, setCustomReason] = useState(""); const [delay, setDelay] = useState(30); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); const reasons = [ "Non-respect des conditions générales d'utilisation", @@ -57,6 +64,112 @@ export default function DeleteAccountButton({ userId, userName }: DeleteAccountB } }; + const modalContent = ( +
+
+
+
+ +
+
+

Supprimer le compte

+

Configuration de la suppression pour {userName}

+
+
+ +
+ {/* Raison selector */} +
+ + +
+ + {/* Custom Reason Field */} + {reason === "Autre" && ( +
+ +