diff --git a/prisma/migrations/20260531200000_add_pirogue_providers/migration.sql b/prisma/migrations/20260531200000_add_pirogue_providers/migration.sql
new file mode 100644
index 0000000..3f25550
--- /dev/null
+++ b/prisma/migrations/20260531200000_add_pirogue_providers/migration.sql
@@ -0,0 +1,29 @@
+-- Plugin pirogue-providers : modèle PirogueProvider + transportMode sur Carbet
+
+CREATE TYPE "TransportMode" AS ENUM ('OWNER_PROVIDES', 'SELF_ARRANGE', 'PARTNER_PROVIDER');
+
+CREATE TABLE "PirogueProvider" (
+ "id" TEXT PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "contactEmail" TEXT,
+ "contactPhone" TEXT,
+ "rivers" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
+ "pricingNote" TEXT,
+ "description" TEXT,
+ "active" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL
+);
+
+CREATE INDEX "PirogueProvider_active_idx" ON "PirogueProvider" ("active");
+
+ALTER TABLE "Carbet"
+ ADD COLUMN "transportMode" "TransportMode",
+ ADD COLUMN "pirogueProviderId" TEXT;
+
+ALTER TABLE "Carbet"
+ ADD CONSTRAINT "Carbet_pirogueProviderId_fkey"
+ FOREIGN KEY ("pirogueProviderId") REFERENCES "PirogueProvider"("id")
+ ON DELETE SET NULL;
+
+CREATE INDEX "Carbet_pirogueProviderId_idx" ON "Carbet" ("pirogueProviderId");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 4fd694e..5a05f4f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -64,6 +64,12 @@ enum AccessType {
RIVER_ONLY
}
+enum TransportMode {
+ OWNER_PROVIDES
+ SELF_ARRANGE
+ PARTNER_PROVIDER
+}
+
model Organization {
id String @id @default(cuid())
name String
@@ -125,23 +131,45 @@ model Carbet {
// Contraintes saisonnières (plugin seasonality). JSON libre, schéma type :
// { closedInLowWater: bool, closedSeasons: ["WET"|"DRY"|"LOW_WATER"][], note: string }
seasonalConstraints Json?
+ // Plugin pirogue-providers : qui organise le transport ?
+ transportMode TransportMode?
+ pirogueProviderId String?
status CarbetStatus @default(DRAFT)
lastBookedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict)
- amenities CarbetAmenity[]
- media Media[]
- availabilities Availability[]
- bookings Booking[]
- reviews Review[]
+ owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict)
+ pirogueProvider PirogueProvider? @relation(fields: [pirogueProviderId], references: [id], onDelete: SetNull)
+ amenities CarbetAmenity[]
+ media Media[]
+ availabilities Availability[]
+ bookings Booking[]
+ reviews Review[]
subscriptions Subscription[]
@@index([ownerId])
@@index([status])
@@index([river])
@@index([accessType])
+ @@index([pirogueProviderId])
+}
+
+model PirogueProvider {
+ id String @id @default(cuid())
+ name String
+ contactEmail String?
+ contactPhone String?
+ rivers String[] @default([])
+ pricingNote String?
+ description String?
+ active Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ carbets Carbet[]
+
+ @@index([active])
}
model Amenity {
diff --git a/src/app/carbets/[slug]/page.tsx b/src/app/carbets/[slug]/page.tsx
index 227b305..9a3e51c 100644
--- a/src/app/carbets/[slug]/page.tsx
+++ b/src/app/carbets/[slug]/page.tsx
@@ -17,6 +17,7 @@ import { ReviewsSection } from "../_components/reviews-section";
import { StarRating } from "../_components/star-rating";
import { AccessTypeBadge } from "@/components/AccessTypeBadge";
import { StayConstraints } from "@/components/StayConstraints";
+import { PirogueTransportBlock } from "@/components/PirogueTransportBlock";
type PageProps = {
params: Promise<{ slug: string }>;
@@ -137,6 +138,11 @@ export default async function PublicCarbetPage({ params }: PageProps) {
+
+
{carbet.amenities.length > 0 ? (
diff --git a/src/app/partenaires-pirogue/page.tsx b/src/app/partenaires-pirogue/page.tsx
new file mode 100644
index 0000000..812be29
--- /dev/null
+++ b/src/app/partenaires-pirogue/page.tsx
@@ -0,0 +1,88 @@
+import { notFound } from "next/navigation";
+import { isPluginEnabled } from "@/lib/plugins/server";
+import { listActiveProviders } from "@/lib/pirogue-providers";
+
+export const dynamic = "force-dynamic";
+
+export async function generateMetadata() {
+ return { title: "Partenaires pirogue" };
+}
+
+export default async function ProvidersPage() {
+ if (!(await isPluginEnabled("pirogue-providers"))) notFound();
+ const providers = await listActiveProviders();
+
+ return (
+
+
+
+ Transport
+
+
+ Nos partenaires pirogue
+
+
+ Pour les carbets accessibles uniquement par le fleuve, on travaille avec des piroguiers
+ locaux référencés. Tarifs estimatifs ci-dessous ; le détail de votre trajet est calé
+ directement avec le partenaire après réservation.
+
+
+
+ {providers.length === 0 ? (
+
+ Aucun partenaire référencé pour le moment.
+
+ ) : (
+
+ {providers.map((p) => (
+ -
+
+
{p.name}
+
+ {p.rivers.map((r) => (
+
+ {r}
+
+ ))}
+
+
+ {p.description ? (
+ {p.description}
+ ) : null}
+ {p.pricingNote ? (
+ {p.pricingNote}
+ ) : null}
+
+ {p.contactEmail ? (
+
+ ) : null}
+ {p.contactPhone ? (
+
+
- Tél. ·
+ - {p.contactPhone}
+
+ ) : null}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/PirogueTransportBlock.tsx b/src/components/PirogueTransportBlock.tsx
new file mode 100644
index 0000000..149d75e
--- /dev/null
+++ b/src/components/PirogueTransportBlock.tsx
@@ -0,0 +1,80 @@
+import { isPluginEnabled } from "@/lib/plugins/server";
+import {
+ TRANSPORT_MODE_EMOJI,
+ TRANSPORT_MODE_LABEL,
+ type PirogueProvider,
+} from "@/lib/pirogue-providers";
+
+/**
+ * Bloc transport pirogue sur la fiche carbet (server component).
+ * Gated par le plugin `pirogue-providers`. Sans le plugin, retourne null.
+ */
+export async function PirogueTransportBlock({
+ transportMode,
+ provider,
+}: {
+ transportMode: string | null;
+ provider: PirogueProvider | null;
+}) {
+ if (!(await isPluginEnabled("pirogue-providers"))) return null;
+ if (!transportMode) return null;
+
+ const emoji = TRANSPORT_MODE_EMOJI[transportMode] ?? "🛶";
+ const label = TRANSPORT_MODE_LABEL[transportMode] ?? "Transport pirogue";
+
+ return (
+
+
+ {emoji}
+
+ Transport pirogue — {label}
+
+
+
+ {transportMode === "PARTNER_PROVIDER" && provider ? (
+
+
+ Ce carbet travaille avec un partenaire référencé :{" "}
+ {provider.name}
+
+ {provider.description ?
{provider.description}
: null}
+ {provider.pricingNote ? (
+
{provider.pricingNote}
+ ) : null}
+
+ {provider.contactEmail ? (
+
+ ) : null}
+ {provider.contactPhone ? (
+
+
- Tél. ·
+ - {provider.contactPhone}
+
+ ) : null}
+
+
+ ) : transportMode === "OWNER_PROVIDES" ? (
+
+ Le loueur s'occupe du transport : il vous récupère au point d'embarquement et
+ vous ramène en fin de séjour. Détails de l'heure et du point de rendez-vous transmis
+ par e-mail après réservation.
+
+ ) : (
+
+ Le transport est à votre charge. Renseignez-vous auprès des piroguiers locaux du dégrad
+ d'embarquement, ou prévenez-nous : on peut vous orienter vers un partenaire.
+
+ )}
+
+ );
+}
diff --git a/src/lib/carbet-public.ts b/src/lib/carbet-public.ts
index dc32e1f..82ed693 100644
--- a/src/lib/carbet-public.ts
+++ b/src/lib/carbet-public.ts
@@ -2,12 +2,13 @@ import { cache } from "react";
import { prisma } from "@/lib/prisma";
import { amenityLabel } from "@/lib/amenities";
-import { AccessType, CarbetStatus, MediaType } from "@/generated/prisma/enums";
+import { AccessType, CarbetStatus, MediaType, TransportMode } from "@/generated/prisma/enums";
import type { CarbetReview, CarbetReviewStats } from "@/lib/reviews";
import {
getCarbetReviewStats,
listCarbetReviews,
} from "@/lib/reviews-server";
+import type { PirogueProvider } from "@/lib/pirogue-providers";
export type PublicCarbetMedia = {
id: string;
@@ -30,6 +31,8 @@ export type PublicCarbetDetail = {
maxStayNights: number | null;
minCapacity: number | null;
seasonalConstraints: unknown;
+ transportMode: TransportMode | null;
+ pirogueProvider: PirogueProvider | null;
latitude: string;
longitude: string;
ownerId: string;
@@ -61,10 +64,24 @@ export const getPublicCarbet = cache(
maxStayNights: true,
minCapacity: true,
seasonalConstraints: true,
+ transportMode: true,
+ pirogueProviderId: true,
latitude: true,
longitude: true,
ownerId: true,
owner: { select: { firstName: true } },
+ pirogueProvider: {
+ select: {
+ id: true,
+ name: true,
+ contactEmail: true,
+ contactPhone: true,
+ rivers: true,
+ pricingNote: true,
+ description: true,
+ active: true,
+ },
+ },
media: {
orderBy: { sortOrder: "asc" },
select: { id: true, type: true, s3Url: true },
@@ -97,6 +114,19 @@ export const getPublicCarbet = cache(
maxStayNights: carbet.maxStayNights,
minCapacity: carbet.minCapacity,
seasonalConstraints: carbet.seasonalConstraints,
+ transportMode: carbet.transportMode,
+ pirogueProvider: carbet.pirogueProvider
+ ? {
+ id: carbet.pirogueProvider.id,
+ name: carbet.pirogueProvider.name,
+ contactEmail: carbet.pirogueProvider.contactEmail,
+ contactPhone: carbet.pirogueProvider.contactPhone,
+ rivers: carbet.pirogueProvider.rivers,
+ pricingNote: carbet.pirogueProvider.pricingNote,
+ description: carbet.pirogueProvider.description,
+ active: carbet.pirogueProvider.active,
+ }
+ : null,
latitude: carbet.latitude.toString(),
longitude: carbet.longitude.toString(),
ownerId: carbet.ownerId,
diff --git a/src/lib/pirogue-providers.ts b/src/lib/pirogue-providers.ts
new file mode 100644
index 0000000..923cdd0
--- /dev/null
+++ b/src/lib/pirogue-providers.ts
@@ -0,0 +1,50 @@
+/**
+ * Helpers Plugin pirogue-providers.
+ */
+
+import { prisma } from "@/lib/prisma";
+
+export type PirogueProvider = {
+ id: string;
+ name: string;
+ contactEmail: string | null;
+ contactPhone: string | null;
+ rivers: string[];
+ pricingNote: string | null;
+ description: string | null;
+ active: boolean;
+};
+
+export async function listActiveProviders(): Promise {
+ const rows = await prisma.pirogueProvider.findMany({
+ where: { active: true },
+ orderBy: { name: "asc" },
+ });
+ return rows.map((r) => ({
+ id: r.id,
+ name: r.name,
+ contactEmail: r.contactEmail,
+ contactPhone: r.contactPhone,
+ rivers: r.rivers,
+ pricingNote: r.pricingNote,
+ description: r.description,
+ active: r.active,
+ }));
+}
+
+export async function listProvidersForRiver(river: string): Promise {
+ const all = await listActiveProviders();
+ return all.filter((p) => p.rivers.some((r) => r.toLowerCase() === river.toLowerCase()));
+}
+
+export const TRANSPORT_MODE_LABEL: Record = {
+ OWNER_PROVIDES: "Le loueur fournit le passeur",
+ SELF_ARRANGE: "À organiser par le voyageur",
+ PARTNER_PROVIDER: "Partenaire référencé",
+};
+
+export const TRANSPORT_MODE_EMOJI: Record = {
+ OWNER_PROVIDES: "👤",
+ SELF_ARRANGE: "🗺️",
+ PARTNER_PROVIDER: "🤝",
+};
diff --git a/src/lib/plugins/hooks.ts b/src/lib/plugins/hooks.ts
index 53c5bc3..d9cccb7 100644
--- a/src/lib/plugins/hooks.ts
+++ b/src/lib/plugins/hooks.ts
@@ -25,6 +25,10 @@ import {
seedLegalPages,
unpublishLegalPages,
} from "./seeds/legal-pages-default";
+import {
+ deactivatePirogueProviders,
+ seedPirogueProviders,
+} from "./seeds/pirogue-providers-default";
export const pluginHooks: Record = {
"demo-carbets-seed": {
@@ -67,4 +71,16 @@ export const pluginHooks: Record = {
console.log(`[plugin legal-pages] disable: ${unpub} pages dépubliées`);
},
},
+ "pirogue-providers": {
+ onEnable: async () => {
+ const { providers, carbets } = await seedPirogueProviders();
+ console.log(
+ `[plugin pirogue-providers] seed: ${providers} partenaires, ${carbets} carbets attachés`,
+ );
+ },
+ onDisable: async () => {
+ const count = await deactivatePirogueProviders();
+ console.log(`[plugin pirogue-providers] disable: ${count} partenaires désactivés`);
+ },
+ },
};
diff --git a/src/lib/plugins/seeds/pirogue-providers-default.ts b/src/lib/plugins/seeds/pirogue-providers-default.ts
new file mode 100644
index 0000000..9360325
--- /dev/null
+++ b/src/lib/plugins/seeds/pirogue-providers-default.ts
@@ -0,0 +1,104 @@
+/**
+ * Seed du plugin `pirogue-providers` : 3 prestataires partenaires fictifs
+ * réalistes pour la démo, attachés à 4 fleuves majeurs de Guyane.
+ */
+
+import { prisma } from "@/lib/prisma";
+
+const PROVIDERS = [
+ {
+ id: "demo-provider-maroni",
+ name: "Pirogues du Maroni",
+ contactEmail: "contact@pirogues-maroni.demo",
+ contactPhone: "+594-694-100200",
+ rivers: ["Maroni", "Lawa"],
+ pricingNote: "≈ 150 € aller-retour depuis Apatou (selon distance carbet)",
+ description:
+ "Coopérative de piroguiers bushinengués. Aller-retour vers les carbets en aval d'Apatou, départs matin. Capacité 6-8 passagers, vestes de pluie fournies.",
+ },
+ {
+ id: "demo-provider-approuague",
+ name: "Approuague Aventures",
+ contactEmail: "info@approuague-aventures.demo",
+ contactPhone: "+594-694-300400",
+ rivers: ["Approuague"],
+ pricingNote: "≈ 250 € aller-retour Régina, ≈ 320 € jusqu'au saut Mapaou",
+ description:
+ "Prestataire historique de l'Approuague, basé à Régina. Pirogues 4 passagers + matériel. Possibilité de jour blanc (pêche, observation) en option.",
+ },
+ {
+ id: "demo-provider-oyapock",
+ name: "Oyapock Frontière",
+ contactEmail: "contact@oyapock-frontiere.demo",
+ contactPhone: "+594-694-500600",
+ rivers: ["Oyapock"],
+ pricingNote: "≈ 300 € aller-retour Saint-Georges (haute eau), tarif majoré en étiage",
+ description:
+ "Trajet vers les carbets côté Guyane et côté Brésilien (Vila Brasil). En étiage (oct-nov), prévoir une marge horaire — fond du fleuve parfois imprévisible.",
+ },
+] as const;
+
+const CARBET_PROVIDER_LINKS = [
+ { slug: "demo-karbe-awara-maroni", providerId: "demo-provider-maroni", mode: "PARTNER_PROVIDER" as const },
+ { slug: "demo-karbe-maripa-approuague", providerId: "demo-provider-approuague", mode: "PARTNER_PROVIDER" as const },
+ { slug: "demo-karbe-paripou-oyapock", providerId: "demo-provider-oyapock", mode: "PARTNER_PROVIDER" as const },
+ { slug: "demo-karbe-wapa-comte", providerId: null, mode: "OWNER_PROVIDES" as const },
+ { slug: "demo-karbe-mahury-ce-hopital", providerId: null, mode: "OWNER_PROVIDES" as const },
+ { slug: "demo-karbe-kourou-couleuvre", providerId: null, mode: "SELF_ARRANGE" as const },
+];
+
+export async function seedPirogueProviders(): Promise<{ providers: number; carbets: number }> {
+ let providers = 0;
+ for (const p of PROVIDERS) {
+ await prisma.pirogueProvider.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ contactEmail: p.contactEmail,
+ contactPhone: p.contactPhone,
+ rivers: [...p.rivers],
+ pricingNote: p.pricingNote,
+ description: p.description,
+ active: true,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ contactEmail: p.contactEmail,
+ contactPhone: p.contactPhone,
+ rivers: [...p.rivers],
+ pricingNote: p.pricingNote,
+ description: p.description,
+ active: true,
+ },
+ });
+ providers += 1;
+ }
+
+ let carbets = 0;
+ for (const link of CARBET_PROVIDER_LINKS) {
+ const updated = await prisma.carbet.updateMany({
+ where: { slug: link.slug },
+ data: {
+ pirogueProviderId: link.providerId,
+ transportMode: link.mode,
+ },
+ });
+ carbets += updated.count;
+ }
+
+ return { providers, carbets };
+}
+
+export async function deactivatePirogueProviders(): Promise {
+ const result = await prisma.pirogueProvider.updateMany({
+ where: { id: { startsWith: "demo-provider-" } },
+ data: { active: false },
+ });
+ // Détache aussi les carbets démo
+ await prisma.carbet.updateMany({
+ where: { pirogueProviderId: { startsWith: "demo-provider-" } },
+ data: { pirogueProviderId: null, transportMode: null },
+ });
+ return result.count;
+}