All checks were successful
CI / test (pull_request) Successful in 2m33s
src/lib/ce-access.ts (NEW) :
- requireCeManagerSession (redirect connexion ou / si rôle insuffisant)
- getCurrentCeOrganization (CE_MANAGER → son org via organizationId,
ADMIN → org ciblée par paramètre ou null)
- canManageCarbetForCe (owner direct OU membre d'une org liée)
- requireApprovedOrg (redirect /espace-ce?pending=1 si non validée)
Emails best-effort :
- sendNewCeRequest → admin (contact@karbe) avec lien filtré
/admin/organizations?status=pending
- sendCeApproved → CE_MANAGERs actifs de l'org après validation
- Branchement dans approveOrganizationAction : envoie le mail à tous
les CE_MANAGERs actifs de l'org en best-effort.
Signup CE public :
- SignupForm 4e tuile « Comité d'Entreprise » avec champ orgName.
Layout grid 4 colonnes sur lg, 2 sur sm.
- /api/signup étendu :
- zod accepte CE_MANAGER + orgName
- transaction $tx atomique : Organization (approved=false, slug
auto-unique via slugify + suffix) + User (role=CE_MANAGER,
organizationId lié)
- sendNewCeRequest best-effort
- réponse étendue avec organizationId
- Pattern slug : retry avec suffix -2, -3… jusqu'à libre
Dashboard /espace-ce :
- layout.tsx : requirePluginOr404("ce-management") +
requireCeManagerSession
- page.tsx : 4 KPIs (carbets co-gérés, items rental, bookings 30j,
revenu 30j), bannière « En attente de validation » si pending,
2 ActionCards (Mes carbets, Matériel rental) marquées « Bientôt »
jusqu'aux sprints I et J
- ce-dashboard.ts : getCeOrgKpis (agrège bookings carbets via
membership + rentalBookings via provider.organizationId) +
listCeCarbets pour Sprint I
SiteHeader : lien « Espace CE » conditionné par role + plugin
(mirror du lien Espace prestataire).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
import "server-only";
|
|
|
|
import {
|
|
BookingStatus,
|
|
RentalBookingStatus,
|
|
} from "@/generated/prisma/enums";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
/**
|
|
* KPIs agrégés à l'échelle d'une organisation CE.
|
|
* - carbets : nombre de carbets co-gérés via OrganizationCarbetMembership
|
|
* - rentalItems : items des providers liés à l'org
|
|
* - bookings30d : bookings confirmées sur les carbets de l'org (30 derniers jours)
|
|
* - rentalBookings30d : RentalBooking confirmées sur les providers de l'org
|
|
* - revenue30d : somme des amounts (booking + rental) sur 30j
|
|
*/
|
|
export async function getCeOrgKpis(organizationId: string) {
|
|
const since = new Date(Date.now() - 30 * 86_400_000);
|
|
|
|
const [carbetsCount, providers, bookings30d, rentalBookings30d] = await Promise.all([
|
|
prisma.organizationCarbetMembership.count({ where: { organizationId } }),
|
|
prisma.rentalProvider.findMany({
|
|
where: { organizationId },
|
|
select: {
|
|
id: true,
|
|
approved: true,
|
|
active: true,
|
|
_count: { select: { items: true } },
|
|
},
|
|
}),
|
|
prisma.booking.findMany({
|
|
where: {
|
|
status: BookingStatus.CONFIRMED,
|
|
createdAt: { gte: since },
|
|
carbet: { organizations: { some: { organizationId } } },
|
|
},
|
|
select: { amount: true, currency: true },
|
|
}),
|
|
prisma.rentalBooking.findMany({
|
|
where: {
|
|
status: RentalBookingStatus.CONFIRMED,
|
|
createdAt: { gte: since },
|
|
provider: { organizationId },
|
|
},
|
|
select: { amount: true, currency: true },
|
|
}),
|
|
]);
|
|
|
|
const itemsCount = providers.reduce((s, p) => s + p._count.items, 0);
|
|
const revenue30d = [
|
|
...bookings30d.map((b) => Number(b.amount)),
|
|
...rentalBookings30d.map((r) => Number(r.amount)),
|
|
].reduce((s, n) => s + n, 0);
|
|
|
|
return {
|
|
carbetsCount,
|
|
providersCount: providers.length,
|
|
rentalItemsCount: itemsCount,
|
|
rentalProviderApproved: providers.every((p) => p.approved),
|
|
bookings30dCount: bookings30d.length,
|
|
rentalBookings30dCount: rentalBookings30d.length,
|
|
revenue30d,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Liste les carbets co-gérés par une org (joinés via membership).
|
|
*/
|
|
export async function listCeCarbets(organizationId: string) {
|
|
const memberships = await prisma.organizationCarbetMembership.findMany({
|
|
where: { organizationId },
|
|
orderBy: { addedAt: "desc" },
|
|
select: {
|
|
carbet: {
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
title: true,
|
|
river: true,
|
|
status: true,
|
|
capacity: true,
|
|
nightlyPrice: true,
|
|
ownerId: true,
|
|
owner: { select: { firstName: true, lastName: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return memberships.map((m) => m.carbet);
|
|
}
|