feat(admin): Sprint 3 — Réservations, Utilisateurs, Avis

This commit is contained in:
Claude Integration 2026-05-31 21:20:46 +00:00
parent 8f31047b36
commit d9ee072744
16 changed files with 1632 additions and 0 deletions

100
src/lib/admin/bookings.ts Normal file
View file

@ -0,0 +1,100 @@
import "server-only";
import { Prisma } from "@/generated/prisma/client";
import { BookingStatus, PaymentStatus } from "@/generated/prisma/enums";
import { prisma } from "@/lib/prisma";
export type AdminBookingFilters = {
q?: string;
status?: BookingStatus;
paymentStatus?: PaymentStatus;
carbetId?: string;
tenantId?: string;
from?: Date;
to?: Date;
};
export type AdminBookingListItem = {
id: string;
startDate: Date;
endDate: Date;
guestCount: number;
status: BookingStatus;
paymentStatus: PaymentStatus;
amount: string;
currency: string;
createdAt: Date;
carbet: { id: string; title: string; slug: string };
tenant: { id: string; firstName: string; lastName: string; email: string };
};
export async function listBookingsAdmin(
filters: AdminBookingFilters = {},
): Promise<AdminBookingListItem[]> {
const where: Prisma.BookingWhereInput = {};
if (filters.q) {
where.OR = [
{ id: { contains: filters.q, mode: "insensitive" } },
{ tenant: { email: { contains: filters.q, mode: "insensitive" } } },
{ tenant: { firstName: { contains: filters.q, mode: "insensitive" } } },
{ tenant: { lastName: { contains: filters.q, mode: "insensitive" } } },
{ carbet: { title: { contains: filters.q, mode: "insensitive" } } },
{ carbet: { slug: { contains: filters.q, mode: "insensitive" } } },
];
}
if (filters.status) where.status = filters.status;
if (filters.paymentStatus) where.paymentStatus = filters.paymentStatus;
if (filters.carbetId) where.carbetId = filters.carbetId;
if (filters.tenantId) where.tenantId = filters.tenantId;
if (filters.from || filters.to) {
where.startDate = {};
if (filters.from) where.startDate.gte = filters.from;
if (filters.to) where.startDate.lte = filters.to;
}
const rows = await prisma.booking.findMany({
where,
orderBy: [{ createdAt: "desc" }],
take: 200,
select: {
id: true,
startDate: true,
endDate: true,
guestCount: true,
status: true,
paymentStatus: true,
amount: true,
currency: true,
createdAt: true,
carbet: { select: { id: true, title: true, slug: true } },
tenant: { select: { id: true, firstName: true, lastName: true, email: true } },
},
});
return rows.map((r) => ({
...r,
amount: r.amount.toString(),
}));
}
export async function getBookingForAdmin(id: string) {
return prisma.booking.findUnique({
where: { id },
include: {
carbet: {
select: {
id: true,
title: true,
slug: true,
river: true,
owner: { select: { id: true, firstName: true, lastName: true, email: true } },
},
},
tenant: {
select: { id: true, firstName: true, lastName: true, email: true, phone: true, role: true },
},
review: { select: { id: true, rating: true, createdAt: true } },
},
});
}

69
src/lib/admin/reviews.ts Normal file
View file

@ -0,0 +1,69 @@
import "server-only";
import { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/prisma";
export type AdminReviewFilters = {
q?: string;
carbetId?: string;
rating?: number;
withResponse?: "yes" | "no";
};
export type AdminReviewListItem = {
id: string;
rating: number;
comment: string | null;
hostResponse: string | null;
hostRespondedAt: Date | null;
createdAt: Date;
carbet: { id: string; title: string; slug: string };
author: { id: string; firstName: string; lastName: string; email: string };
booking: { id: string };
};
export async function listReviewsAdmin(filters: AdminReviewFilters = {}): Promise<AdminReviewListItem[]> {
const where: Prisma.ReviewWhereInput = {};
if (filters.q) {
where.OR = [
{ id: { contains: filters.q, mode: "insensitive" } },
{ comment: { contains: filters.q, mode: "insensitive" } },
{ author: { email: { contains: filters.q, mode: "insensitive" } } },
{ author: { firstName: { contains: filters.q, mode: "insensitive" } } },
{ author: { lastName: { contains: filters.q, mode: "insensitive" } } },
{ carbet: { title: { contains: filters.q, mode: "insensitive" } } },
];
}
if (filters.carbetId) where.carbetId = filters.carbetId;
if (filters.rating) where.rating = filters.rating;
if (filters.withResponse === "yes") where.hostResponse = { not: null };
if (filters.withResponse === "no") where.hostResponse = null;
return prisma.review.findMany({
where,
orderBy: [{ createdAt: "desc" }],
take: 200,
select: {
id: true,
rating: true,
comment: true,
hostResponse: true,
hostRespondedAt: true,
createdAt: true,
booking: { select: { id: true } },
carbet: { select: { id: true, title: true, slug: true } },
author: { select: { id: true, firstName: true, lastName: true, email: true } },
},
});
}
export async function getReviewForAdmin(id: string) {
return prisma.review.findUnique({
where: { id },
include: {
booking: { select: { id: true, startDate: true, endDate: true, amount: true, currency: true } },
carbet: { select: { id: true, title: true, slug: true } },
author: { select: { id: true, firstName: true, lastName: true, email: true } },
},
});
}

91
src/lib/admin/users.ts Normal file
View file

@ -0,0 +1,91 @@
import "server-only";
import { Prisma } from "@/generated/prisma/client";
import { UserRole } from "@/generated/prisma/enums";
import { prisma } from "@/lib/prisma";
export type AdminUserFilters = {
q?: string;
role?: UserRole;
active?: "yes" | "no";
};
export type AdminUserListItem = {
id: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
isActive: boolean;
createdAt: Date;
carbetsCount: number;
bookingsCount: number;
reviewsCount: number;
};
export async function listUsersAdmin(filters: AdminUserFilters = {}): Promise<AdminUserListItem[]> {
const where: Prisma.UserWhereInput = {};
if (filters.q) {
where.OR = [
{ email: { contains: filters.q, mode: "insensitive" } },
{ firstName: { contains: filters.q, mode: "insensitive" } },
{ lastName: { contains: filters.q, mode: "insensitive" } },
{ phone: { contains: filters.q, mode: "insensitive" } },
];
}
if (filters.role) where.role = filters.role;
if (filters.active === "yes") where.isActive = true;
if (filters.active === "no") where.isActive = false;
const rows = await prisma.user.findMany({
where,
orderBy: [{ createdAt: "desc" }],
take: 300,
select: {
id: true,
email: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
createdAt: true,
_count: { select: { carbets: true, bookings: true, reviews: true } },
},
});
return rows.map((u) => ({
id: u.id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
role: u.role,
isActive: u.isActive,
createdAt: u.createdAt,
carbetsCount: u._count.carbets,
bookingsCount: u._count.bookings,
reviewsCount: u._count.reviews,
}));
}
export async function getUserForAdmin(id: string) {
return prisma.user.findUnique({
where: { id },
include: {
organization: { select: { id: true, name: true } },
_count: { select: { carbets: true, bookings: true, reviews: true, subscriptions: true } },
bookings: {
take: 10,
orderBy: { createdAt: "desc" },
select: {
id: true, status: true, paymentStatus: true, startDate: true, endDate: true, amount: true, currency: true,
carbet: { select: { id: true, title: true } },
},
},
carbets: {
take: 10,
orderBy: { updatedAt: "desc" },
select: { id: true, title: true, slug: true, status: true, updatedAt: true },
},
},
});
}