karbe/src/lib/ce-dashboard.ts
Ubuntu 63a29d9ade
All checks were successful
CI / test (pull_request) Successful in 2m33s
feat(ce): Sprint H — signup CE public + /espace-ce shell
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>
2026-06-02 23:12:46 +00:00

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);
}