diff --git a/admin/src/app/actions/moderation.ts b/admin/src/app/actions/moderation.ts
index d55e7ac..c84b637 100644
--- a/admin/src/app/actions/moderation.ts
+++ b/admin/src/app/actions/moderation.ts
@@ -68,13 +68,65 @@ export async function getPendingRatings() {
export async function updateRatingStatus(id: string, status: 'APPROVED' | 'REJECTED') {
try {
- await prisma.rating.update({
+ const updated = await prisma.rating.update({
where: { id },
data: { status }
});
+
+ if (updated.businessId) {
+ const stats = await prisma.rating.aggregate({
+ where: { businessId: updated.businessId, status: 'APPROVED' },
+ _avg: { value: true },
+ _count: { value: true }
+ });
+
+ await prisma.business.update({
+ where: { id: updated.businessId },
+ data: {
+ rating: stats._avg.value || 0,
+ ratingCount: stats._count.value || 0
+ }
+ });
+ }
+
revalidatePath('/moderation');
return { success: true };
} catch (error) {
return { success: false, error: "Erreur lors de la mise à jour de l'avis" };
}
}
+
+export async function deleteRating(id: string) {
+ try {
+ const rating = await prisma.rating.findUnique({
+ where: { id },
+ select: { businessId: true }
+ });
+
+ await prisma.rating.delete({
+ where: { id }
+ });
+
+ if (rating?.businessId) {
+ const stats = await prisma.rating.aggregate({
+ where: { businessId: rating.businessId, status: 'APPROVED' },
+ _avg: { value: true },
+ _count: { value: true }
+ });
+
+ await prisma.business.update({
+ where: { id: rating.businessId },
+ data: {
+ rating: stats._avg.value || 0,
+ ratingCount: stats._count.value || 0
+ }
+ });
+ }
+
+ revalidatePath('/moderation');
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: "Erreur lors de la suppression de l'avis" };
+ }
+}
+
diff --git a/admin/src/app/api/cron/cleanup-users/route.ts b/admin/src/app/api/cron/cleanup-users/route.ts
index 244b875..e230b0c 100644
--- a/admin/src/app/api/cron/cleanup-users/route.ts
+++ b/admin/src/app/api/cron/cleanup-users/route.ts
@@ -30,12 +30,23 @@ export async function GET(request: Request) {
return NextResponse.json({ message: "Aucun compte à supprimer" });
}
- // 2. Suppression définitive
- // Prisma supprimera les données liées si onDelete: Cascade est configuré dans le schéma
+ const userIds = usersToDelete.map(u => u.id);
+
+ // 2. Préserver les avis rédigés en les dissociant du compte (deviennent anonymes / "Utilisateur supprimé")
+ await prisma.rating.updateMany({
+ where: {
+ userId: { in: userIds }
+ },
+ data: {
+ userId: null
+ }
+ });
+
+ // 3. Suppression définitive des comptes utilisateurs
const deleteResult = await prisma.user.deleteMany({
where: {
id: {
- in: usersToDelete.map(u => u.id)
+ in: userIds
}
}
});
diff --git a/admin/src/app/moderation/page.tsx b/admin/src/app/moderation/page.tsx
index 21052cd..d4ac943 100644
--- a/admin/src/app/moderation/page.tsx
+++ b/admin/src/app/moderation/page.tsx
@@ -131,10 +131,16 @@ export default async function ModerationPage({
Auteur
-
{r.user.name.charAt(0)}
+
+ {r.user?.name ? r.user.name.charAt(0) : '?'}
+
-
{r.user.name}
-
{r.user.email}
+
+ {r.user?.name || "Utilisateur supprimé"}
+
+
+ {r.user?.email || "Compte clôturé"}
+
@@ -142,7 +148,9 @@ export default async function ModerationPage({
Entrepreneur Concerné
-
{r.business.name}
+
+ {r.business?.name || "Entreprise introuvable ou supprimée"}
+
diff --git a/admin/src/components/RatingModerationButtons.tsx b/admin/src/components/RatingModerationButtons.tsx
index 8610787..c65fb8e 100644
--- a/admin/src/components/RatingModerationButtons.tsx
+++ b/admin/src/components/RatingModerationButtons.tsx
@@ -1,8 +1,8 @@
"use client";
import { useState } from 'react';
-import { Check, X, Loader2 } from 'lucide-react';
-import { updateRatingStatus } from '@/app/actions/moderation';
+import { Check, X, Loader2, Trash2 } from 'lucide-react';
+import { updateRatingStatus, deleteRating } from '@/app/actions/moderation';
import { toast } from 'react-hot-toast';
interface RatingModerationButtonsProps {
@@ -28,6 +28,25 @@ export default function RatingModerationButtons({ ratingId }: RatingModerationBu
}
};
+ const handleDelete = async () => {
+ if (!window.confirm("Êtes-vous sûr de vouloir supprimer définitivement cet avis ?")) {
+ return;
+ }
+ setIsPending(true);
+ try {
+ const result = await deleteRating(ratingId);
+ if (result.success) {
+ toast.success("Avis supprimé définitivement.");
+ } else {
+ toast.error(result.error || "Une erreur est survenue");
+ }
+ } catch (error) {
+ toast.error("Erreur lors de la suppression");
+ } finally {
+ setIsPending(false);
+ }
+ };
+
return (
+
);
}
diff --git a/app/api/businesses/[id]/ratings/route.ts b/app/api/businesses/[id]/ratings/route.ts
index 4fba201..282939a 100644
--- a/app/api/businesses/[id]/ratings/route.ts
+++ b/app/api/businesses/[id]/ratings/route.ts
@@ -46,7 +46,17 @@ export async function GET(
}
});
- return NextResponse.json(ratings);
+ // 3. Fallback for deleted users
+ const formattedRatings = ratings.map(r => ({
+ ...r,
+ user: r.user || {
+ id: null,
+ name: "Utilisateur supprimé",
+ avatar: null
+ }
+ }));
+
+ return NextResponse.json(formattedRatings);
} catch (error: any) {
console.error('Error fetching ratings:', error);
diff --git a/app/api/ratings/[id]/reply/route.ts b/app/api/ratings/[id]/reply/route.ts
index ea42948..0df3e5e 100644
--- a/app/api/ratings/[id]/reply/route.ts
+++ b/app/api/ratings/[id]/reply/route.ts
@@ -36,7 +36,7 @@ export async function POST(
}
// 2. Security: Only the owner of the business can reply
- if (rating.business.ownerId !== userId) {
+ if (!rating.business || rating.business.ownerId !== userId) {
return NextResponse.json({ error: 'Action non autorisée. Seul l\'entrepreneur concerné peut répondre.' }, { status: 403 });
}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e89673a..740d990 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -245,16 +245,16 @@ model AnalyticsEvent {
model Rating {
id String @id @default(uuid())
value Int
- userId String
- businessId String
+ userId String?
+ businessId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
comment String?
reply String?
replyAt DateTime?
status RatingStatus @default(PENDING)
- business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ business Business? @relation(fields: [businessId], references: [id], onDelete: Cascade)
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@unique([userId, businessId])
}