feat(admin): CRUD complet carbets + gestion médias (Sprint 2)
Server actions (src/app/admin/carbets/actions.ts) avec validation Zod : - createCarbetAction → INSERT + audit + redirect /admin/carbets/[id] - updateCarbetAction → UPDATE + revalidate page publique - updateCarbetStatusAction → DRAFT/PUBLISHED/ARCHIVED - deleteCarbetAction → soft archive (bookings/reviews FK Restrict) - addMediaAction(carbetId, fd) → INSERT Media + sortOrder - removeMediaAction, reorderMediaAction (transactionnel up/down) Helpers (src/lib/admin/carbets.ts) : - listCarbetsAdmin avec filtres (q/river/status/accessType) - listDistinctRivers, listOwners, listPirogueProviders - getCarbetForEdit (include owner, provider, media, _count bookings/reviews) - Options enum pour les selects (ACCESS_TYPE, TRANSPORT_MODE, STATUS) Pages : - /admin/carbets : liste tableau dense avec recherche/filtres GET, status badge, liens vers édition, count médias/résas - /admin/carbets/new : page création avec CarbetForm - /admin/carbets/[id] : header titre+badge+actions, MediaManager, CarbetForm d'édition. Lien public si PUBLISHED. Composants admin réutilisables : - StatusBadge (DRAFT/PUBLISHED/ARCHIVED + statuts Booking) - FormField + inputCls/selectCls/textareaCls - CarbetForm (client, 5 sections : identité, localisation, accès, séjour, publication) avec useTransition + erreur + succès inline - MediaManager (client, liste + reorder ↑↓ + suppression + ajout par URL) - StatusActions (client, publier/dépublier/archiver/réactiver avec confirm) API : - GET /api/admin/carbets/[id]/media pour refresh client après mutation Audit léger en log console (JSON structuré) — Sprint 5 ajoutera la table.
This commit is contained in:
parent
3ec7a3ff10
commit
9aa0771001
11 changed files with 1202 additions and 0 deletions
130
src/lib/admin/carbets.ts
Normal file
130
src/lib/admin/carbets.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Helpers admin Carbets — listing avec filtres, lookup pour formulaires.
|
||||
*/
|
||||
|
||||
import "server-only";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AccessType, CarbetStatus, TransportMode, UserRole } from "@/generated/prisma/enums";
|
||||
|
||||
export type AdminCarbetListItem = {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
river: string;
|
||||
capacity: number;
|
||||
status: CarbetStatus;
|
||||
accessType: AccessType;
|
||||
ownerName: string;
|
||||
ownerEmail: string;
|
||||
mediaCount: number;
|
||||
bookingsCount: number;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type AdminCarbetFilters = {
|
||||
q?: string;
|
||||
river?: string;
|
||||
status?: CarbetStatus;
|
||||
accessType?: AccessType;
|
||||
};
|
||||
|
||||
export async function listCarbetsAdmin(filters: AdminCarbetFilters = {}): Promise<AdminCarbetListItem[]> {
|
||||
const where: Parameters<typeof prisma.carbet.findMany>[0]["where"] = {};
|
||||
if (filters.q) {
|
||||
where.OR = [
|
||||
{ title: { contains: filters.q, mode: "insensitive" } },
|
||||
{ slug: { contains: filters.q, mode: "insensitive" } },
|
||||
{ river: { contains: filters.q, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
if (filters.river) where.river = filters.river;
|
||||
if (filters.status) where.status = filters.status;
|
||||
if (filters.accessType) where.accessType = filters.accessType;
|
||||
|
||||
const rows = await prisma.carbet.findMany({
|
||||
where,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 100,
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
river: true,
|
||||
capacity: true,
|
||||
status: true,
|
||||
accessType: true,
|
||||
updatedAt: true,
|
||||
owner: { select: { firstName: true, lastName: true, email: true } },
|
||||
_count: { select: { media: true, bookings: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
slug: r.slug,
|
||||
title: r.title,
|
||||
river: r.river,
|
||||
capacity: r.capacity,
|
||||
status: r.status,
|
||||
accessType: r.accessType,
|
||||
ownerName: `${r.owner.firstName} ${r.owner.lastName}`.trim() || r.owner.email,
|
||||
ownerEmail: r.owner.email,
|
||||
mediaCount: r._count.media,
|
||||
bookingsCount: r._count.bookings,
|
||||
updatedAt: r.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listDistinctRivers(): Promise<string[]> {
|
||||
const rows = await prisma.carbet.findMany({
|
||||
distinct: ["river"],
|
||||
orderBy: { river: "asc" },
|
||||
select: { river: true },
|
||||
});
|
||||
return rows.map((r) => r.river);
|
||||
}
|
||||
|
||||
export async function listOwners() {
|
||||
return await prisma.user.findMany({
|
||||
where: { role: UserRole.OWNER, isActive: true },
|
||||
orderBy: [{ firstName: "asc" }, { lastName: "asc" }],
|
||||
select: { id: true, firstName: true, lastName: true, email: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPirogueProviders() {
|
||||
return await prisma.pirogueProvider.findMany({
|
||||
where: { active: true },
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true, rivers: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCarbetForEdit(id: string) {
|
||||
return await prisma.carbet.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
owner: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
pirogueProvider: { select: { id: true, name: true } },
|
||||
media: { orderBy: { sortOrder: "asc" } },
|
||||
_count: { select: { bookings: true, reviews: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const ACCESS_TYPE_OPTIONS: { value: AccessType; label: string }[] = [
|
||||
{ value: AccessType.ROAD_AND_RIVER, label: "🛣️ Route + fleuve" },
|
||||
{ value: AccessType.RIVER_ONLY, label: "🛶 Expédition fleuve" },
|
||||
];
|
||||
|
||||
export const TRANSPORT_MODE_OPTIONS: { value: TransportMode; label: string }[] = [
|
||||
{ value: TransportMode.SELF_ARRANGE, label: "🗺️ À organiser par le voyageur" },
|
||||
{ value: TransportMode.OWNER_PROVIDES, label: "👤 Le loueur fournit" },
|
||||
{ value: TransportMode.PARTNER_PROVIDER, label: "🤝 Partenaire référencé" },
|
||||
];
|
||||
|
||||
export const STATUS_OPTIONS: { value: CarbetStatus; label: string }[] = [
|
||||
{ value: CarbetStatus.DRAFT, label: "Brouillon" },
|
||||
{ value: CarbetStatus.PUBLISHED, label: "Publié" },
|
||||
{ value: CarbetStatus.ARCHIVED, label: "Archivé" },
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue