Layout admin : - src/app/admin/layout.tsx : route protégée requireRole(ADMIN), sidebar + topbar + breadcrumbs, data-admin sur racine pour theme sobre indépendant du theme public - Sidebar : 12 sections groupées (Vue d'ensemble, Catalogue, Activité, Membres, Contenu, Système), highlight de la route courante - TopBar : prompt ⌘K, lien vers site public, email admin - Breadcrumbs : auto depuis pathname - CommandPalette : ⌘K / Ctrl K, navigation ↑↓ + Entrée, recherche live debounced 150ms Dashboard : - 7 KPI cards avec tone neutral/ok/warn/info (réservations semaine, confirmées 30j, revenus reversés, occupation, nouveaux users, carbets publiés, avis à modérer) - Section raccourcis fréquents Theme admin : - globals.css : [data-admin] override le background+font, neutralise les borders sépia/papier teinté du theme aquarelle, garantit lisibilité permanente Recherche globale : - lib/admin/search.ts : query parallèle sur Carbet, User, Booking, ContentPage, PirogueProvider (5 résultats par catégorie, LIKE insensitive) - api/admin/search?q=… route handler avec requireRole KPI : - lib/admin/kpis.ts : 7 métriques live (cache 0), Promise.all, helper formatEur Pas de dépendance externe ajoutée (cmdk, shadcn) — composants custom Tailwind pour rester léger.
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
/**
|
|
* Recherche globale ⌘K — server function.
|
|
*
|
|
* Recherche transversale sur carbets / utilisateurs / réservations /
|
|
* pages éditoriales / prestataires pirogue. Renvoie au max 5 résultats
|
|
* par catégorie pour garder la palette lisible.
|
|
*/
|
|
|
|
import "server-only";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export type SearchHit = {
|
|
type: "carbet" | "user" | "booking" | "page" | "provider";
|
|
id: string;
|
|
title: string;
|
|
subtitle?: string;
|
|
href: string;
|
|
};
|
|
|
|
export async function adminSearch(query: string): Promise<SearchHit[]> {
|
|
const q = query.trim();
|
|
if (q.length < 2) return [];
|
|
const ci = { contains: q, mode: "insensitive" as const };
|
|
|
|
const [carbets, users, bookings, pages, providers] = await Promise.all([
|
|
prisma.carbet.findMany({
|
|
where: {
|
|
OR: [{ slug: ci }, { title: ci }, { river: ci }],
|
|
},
|
|
take: 5,
|
|
select: { id: true, slug: true, title: true, river: true, status: true },
|
|
}),
|
|
prisma.user.findMany({
|
|
where: {
|
|
OR: [{ email: ci }, { firstName: ci }, { lastName: ci }],
|
|
},
|
|
take: 5,
|
|
select: { id: true, email: true, firstName: true, lastName: true, role: true },
|
|
}),
|
|
prisma.booking.findMany({
|
|
where: { id: ci },
|
|
take: 5,
|
|
select: { id: true, status: true, startDate: true, endDate: true },
|
|
}),
|
|
prisma.contentPage.findMany({
|
|
where: {
|
|
OR: [{ slug: ci }, { title: ci }],
|
|
lang: "fr",
|
|
},
|
|
take: 5,
|
|
select: { slug: true, title: true, category: true, lang: true },
|
|
}),
|
|
prisma.pirogueProvider.findMany({
|
|
where: { OR: [{ name: ci }] },
|
|
take: 5,
|
|
select: { id: true, name: true, rivers: true },
|
|
}),
|
|
]);
|
|
|
|
const hits: SearchHit[] = [];
|
|
|
|
for (const c of carbets) {
|
|
hits.push({
|
|
type: "carbet",
|
|
id: c.id,
|
|
title: c.title,
|
|
subtitle: `${c.river} · ${c.status}`,
|
|
href: `/admin/carbets/${c.id}`,
|
|
});
|
|
}
|
|
for (const u of users) {
|
|
hits.push({
|
|
type: "user",
|
|
id: u.id,
|
|
title: `${u.firstName} ${u.lastName}`.trim() || u.email,
|
|
subtitle: `${u.email} · ${u.role}`,
|
|
href: `/admin/users/${u.id}`,
|
|
});
|
|
}
|
|
for (const b of bookings) {
|
|
hits.push({
|
|
type: "booking",
|
|
id: b.id,
|
|
title: `Réservation ${b.id.slice(0, 8)}`,
|
|
subtitle: `${b.status} · ${b.startDate.toISOString().slice(0, 10)} → ${b.endDate.toISOString().slice(0, 10)}`,
|
|
href: `/admin/bookings/${b.id}`,
|
|
});
|
|
}
|
|
for (const p of pages) {
|
|
hits.push({
|
|
type: "page",
|
|
id: p.slug,
|
|
title: p.title,
|
|
subtitle: `/${p.slug} · ${p.category} · ${p.lang}`,
|
|
href: `/admin/content-pages/${encodeURIComponent(p.slug)}`,
|
|
});
|
|
}
|
|
for (const p of providers) {
|
|
hits.push({
|
|
type: "provider",
|
|
id: p.id,
|
|
title: p.name,
|
|
subtitle: p.rivers.join(" · "),
|
|
href: `/admin/pirogue-providers/${p.id}`,
|
|
});
|
|
}
|
|
|
|
return hits;
|
|
}
|