diff --git a/package-lock.json b/package-lock.json index 7d8475d..eb5b2bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,23 +10,15 @@ "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.1056.0", - "@aws-sdk/s3-request-presigner": "^3.1058.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", - "@types/leaflet": "^1.9.21", "bcryptjs": "^3.0.3", - "leaflet": "^1.9.4", "next": "16.2.6", "next-auth": "^5.0.0-beta.31", "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4", - "react-leaflet": "^5.0.0", "resend": "^4.8.0", - "sharp": "^0.34.5", "stripe": "^18.3.0" }, "devDependencies": { @@ -514,23 +506,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1058.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1058.0.tgz", - "integrity": "sha512-IRgNfn8U3zfsZ0JkpmwjS59R/XyHMHxpuwW6HVuJhik+FsbClhNkujEO0w1WqJvXrF4FX+7qIAwUrvlwNvaZ7Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.15", - "@aws-sdk/signature-v4-multi-region": "^3.996.30", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.5", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.30", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", @@ -861,59 +836,6 @@ "node": ">=18" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", - "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.1.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@electric-sql/pglite": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", @@ -1647,6 +1569,7 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=18" } @@ -2834,17 +2757,6 @@ "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, - "node_modules/@react-leaflet/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", - "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", - "license": "Hippocratic-2.1", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", @@ -3697,12 +3609,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3717,15 +3623,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/leaflet": { - "version": "1.9.21", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", - "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/node": { "version": "20.19.41", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", @@ -5398,6 +5295,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7699,12 +7597,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9162,20 +9054,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-leaflet": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", - "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", - "license": "Hippocratic-2.1", - "dependencies": { - "@react-leaflet/core": "^3.0.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -9573,6 +9451,7 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -9616,6 +9495,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", + "optional": true, "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index e0a10f1..6a33258 100644 --- a/package.json +++ b/package.json @@ -14,23 +14,15 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1056.0", - "@aws-sdk/s3-request-presigner": "^3.1058.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", - "@types/leaflet": "^1.9.21", "bcryptjs": "^3.0.3", - "leaflet": "^1.9.4", "next": "16.2.6", "next-auth": "^5.0.0-beta.31", "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4", - "react-leaflet": "^5.0.0", "resend": "^4.8.0", - "sharp": "^0.34.5", "stripe": "^18.3.0" }, "devDependencies": { @@ -47,4 +39,4 @@ "typescript": "^5.9.3", "vitest": "^3.2.4" } -} +} \ No newline at end of file diff --git a/prisma/migrations/20260601060000_password_reset_token/migration.sql b/prisma/migrations/20260601060000_password_reset_token/migration.sql deleted file mode 100644 index 50033de..0000000 --- a/prisma/migrations/20260601060000_password_reset_token/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE "PasswordResetToken" ( - "tokenHash" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expiresAt" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("tokenHash") -); -CREATE INDEX "PasswordResetToken_userId_idx" ON "PasswordResetToken"("userId"); -CREATE INDEX "PasswordResetToken_expiresAt_idx" ON "PasswordResetToken"("expiresAt"); diff --git a/prisma/migrations/20260602030000_operational_criteria/migration.sql b/prisma/migrations/20260602030000_operational_criteria/migration.sql deleted file mode 100644 index 5bdca5f..0000000 --- a/prisma/migrations/20260602030000_operational_criteria/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TYPE "RoadAccess" AS ENUM ('NONE', 'DRY_SEASON_ONLY', 'ALL_YEAR'); -CREATE TYPE "Electricity" AS ENUM ('NONE', 'SOLAR', 'GENERATOR_READY', 'EDF'); - -ALTER TABLE "Carbet" ADD COLUMN "roadAccess" "RoadAccess"; -ALTER TABLE "Carbet" ADD COLUMN "electricity" "Electricity"; -ALTER TABLE "Carbet" ADD COLUMN "gsmAtCarbet" BOOLEAN NOT NULL DEFAULT false; -ALTER TABLE "Carbet" ADD COLUMN "gsmExitDistanceKm" DECIMAL(4,2); - --- Seed des 6 carbets démo avec valeurs réalistes -UPDATE "Carbet" SET "roadAccess" = 'NONE', "electricity" = 'SOLAR', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 1.5 WHERE id = 'demo-carbet-awara'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-kourou'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-mahury'; -UPDATE "Carbet" SET "roadAccess" = 'NONE', "electricity" = 'GENERATOR_READY', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 4.0 WHERE id = 'demo-carbet-maripa'; -UPDATE "Carbet" SET "roadAccess" = 'DRY_SEASON_ONLY', "electricity" = 'SOLAR', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 0.5 WHERE id = 'demo-carbet-paripou'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-wapa'; diff --git a/prisma/migrations/20260602100000_favorite/migration.sql b/prisma/migrations/20260602100000_favorite/migration.sql deleted file mode 100644 index 8abf012..0000000 --- a/prisma/migrations/20260602100000_favorite/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE "Favorite" ( - "userId" TEXT NOT NULL, - "carbetId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Favorite_pkey" PRIMARY KEY ("userId", "carbetId") -); -CREATE INDEX "Favorite_userId_idx" ON "Favorite"("userId"); -CREATE INDEX "Favorite_carbetId_idx" ON "Favorite"("carbetId"); diff --git a/prisma/migrations/20260603000000_rental_marketplace/migration.sql b/prisma/migrations/20260603000000_rental_marketplace/migration.sql deleted file mode 100644 index 65b4eb1..0000000 --- a/prisma/migrations/20260603000000_rental_marketplace/migration.sql +++ /dev/null @@ -1,112 +0,0 @@ --- UserRole : ajouter RENTAL_PROVIDER -ALTER TYPE "UserRole" ADD VALUE IF NOT EXISTS 'RENTAL_PROVIDER'; - --- Enums dédiés -CREATE TYPE "RentalCategory" AS ENUM ('SLEEP', 'NAVIGATION', 'FISHING', 'COOKING', 'SAFETY'); -CREATE TYPE "RentalBookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'HANDED_OVER', 'RETURNED', 'CANCELLED'); - --- RentalProvider -CREATE TABLE "RentalProvider" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "isSystemD" BOOLEAN NOT NULL DEFAULT false, - "managedByUserId" TEXT, - "contactEmail" TEXT, - "contactPhone" TEXT, - "rivers" TEXT[] DEFAULT ARRAY[]::TEXT[], - "description" TEXT, - "commissionPct" DECIMAL(5,2) NOT NULL DEFAULT 0, - "active" BOOLEAN NOT NULL DEFAULT true, - "approved" BOOLEAN NOT NULL DEFAULT false, - "approvedAt" TIMESTAMP(3), - "approvedBy" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - CONSTRAINT "RentalProvider_pkey" PRIMARY KEY ("id"), - CONSTRAINT "RentalProvider_managedByUserId_fkey" FOREIGN KEY ("managedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE -); -CREATE INDEX "RentalProvider_active_approved_idx" ON "RentalProvider"("active", "approved"); -CREATE INDEX "RentalProvider_managedByUserId_idx" ON "RentalProvider"("managedByUserId"); - --- RentalItem -CREATE TABLE "RentalItem" ( - "id" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "category" "RentalCategory" NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "imageUrl" TEXT, - "pricePerDay" DECIMAL(8,2) NOT NULL, - "pricePerWeek" DECIMAL(8,2), - "deposit" DECIMAL(8,2) NOT NULL DEFAULT 0, - "totalQty" INTEGER NOT NULL DEFAULT 1, - "withMotor" BOOLEAN NOT NULL DEFAULT false, - "fuelIncluded" BOOLEAN NOT NULL DEFAULT false, - "requiresLicense" BOOLEAN NOT NULL DEFAULT false, - "active" BOOLEAN NOT NULL DEFAULT true, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - CONSTRAINT "RentalItem_pkey" PRIMARY KEY ("id"), - CONSTRAINT "RentalItem_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "RentalProvider"("id") ON DELETE CASCADE ON UPDATE CASCADE -); -CREATE INDEX "RentalItem_providerId_idx" ON "RentalItem"("providerId"); -CREATE INDEX "RentalItem_category_active_idx" ON "RentalItem"("category", "active"); - --- RentalItemAvailability -CREATE TABLE "RentalItemAvailability" ( - "id" TEXT NOT NULL, - "itemId" TEXT NOT NULL, - "startDate" TIMESTAMP(3) NOT NULL, - "endDate" TIMESTAMP(3) NOT NULL, - "qty" INTEGER NOT NULL, - "reason" TEXT NOT NULL, - "rentalBookingId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "RentalItemAvailability_pkey" PRIMARY KEY ("id"), - CONSTRAINT "RentalItemAvailability_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "RentalItem"("id") ON DELETE CASCADE ON UPDATE CASCADE -); -CREATE INDEX "RentalItemAvailability_itemId_startDate_endDate_idx" ON "RentalItemAvailability"("itemId", "startDate", "endDate"); -CREATE INDEX "RentalItemAvailability_rentalBookingId_idx" ON "RentalItemAvailability"("rentalBookingId"); - --- RentalBooking -CREATE TABLE "RentalBooking" ( - "id" TEXT NOT NULL, - "bookingId" TEXT, - "tenantId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "startDate" TIMESTAMP(3) NOT NULL, - "endDate" TIMESTAMP(3) NOT NULL, - "status" "RentalBookingStatus" NOT NULL DEFAULT 'PENDING', - "paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'PENDING', - "itemsTotal" DECIMAL(10,2) NOT NULL, - "depositTotal" DECIMAL(10,2) NOT NULL, - "commissionAmount" DECIMAL(10,2) NOT NULL DEFAULT 0, - "amount" DECIMAL(10,2) NOT NULL, - "currency" TEXT NOT NULL DEFAULT 'EUR', - "stripeSessionId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - CONSTRAINT "RentalBooking_pkey" PRIMARY KEY ("id"), - CONSTRAINT "RentalBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE SET NULL ON UPDATE CASCADE, - CONSTRAINT "RentalBooking_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "RentalBooking_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "RentalProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE -); -CREATE INDEX "RentalBooking_tenantId_status_idx" ON "RentalBooking"("tenantId", "status"); -CREATE INDEX "RentalBooking_providerId_status_idx" ON "RentalBooking"("providerId", "status"); -CREATE INDEX "RentalBooking_bookingId_idx" ON "RentalBooking"("bookingId"); -CREATE INDEX "RentalBooking_startDate_endDate_idx" ON "RentalBooking"("startDate", "endDate"); - --- RentalLine -CREATE TABLE "RentalLine" ( - "id" TEXT NOT NULL, - "rentalBookingId" TEXT NOT NULL, - "itemId" TEXT NOT NULL, - "qty" INTEGER NOT NULL, - "pricePerDay" DECIMAL(8,2) NOT NULL, - "deposit" DECIMAL(8,2) NOT NULL DEFAULT 0, - "lineTotal" DECIMAL(10,2) NOT NULL, - CONSTRAINT "RentalLine_pkey" PRIMARY KEY ("id"), - CONSTRAINT "RentalLine_rentalBookingId_fkey" FOREIGN KEY ("rentalBookingId") REFERENCES "RentalBooking"("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "RentalLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "RentalItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE -); -CREATE INDEX "RentalLine_rentalBookingId_idx" ON "RentalLine"("rentalBookingId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7580413..e9aaf6d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,6 @@ enum UserRole { CE_MEMBER TOURIST ADMIN - RENTAL_PROVIDER } enum CarbetStatus { @@ -98,13 +97,11 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) - carbets Carbet[] @relation("CarbetOwner") - bookings Booking[] @relation("BookingTenant") - reviews Review[] @relation("ReviewAuthor") - subscriptions Subscription[] - rentalProviders RentalProvider[] - rentalBookings RentalBooking[] @relation("RentalBookingTenant") + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) + carbets Carbet[] @relation("CarbetOwner") + bookings Booking[] @relation("BookingTenant") + reviews Review[] @relation("ReviewAuthor") + subscriptions Subscription[] @@index([organizationId]) @@index([role]) @@ -127,11 +124,6 @@ model Carbet { // Détails d'accès route pour ROAD_AND_RIVER (GPS, distance, type de piste). roadAccessNote String? capacity Int - // 4 critères opérationnels dealbreakers (dispo en filtres + badges UI) - roadAccess RoadAccess? - electricity Electricity? - gsmAtCarbet Boolean @default(false) - gsmExitDistanceKm Decimal? @db.Decimal(4, 2) // Prix par nuit pour le carbet entier (toute capacité). En euros. nightlyPrice Decimal @db.Decimal(10, 2) @default(0) // Contraintes séjour (plugin min-stay). null = pas de contrainte. @@ -252,8 +244,7 @@ model Booking { carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict) tenant User @relation("BookingTenant", fields: [tenantId], references: [id], onDelete: Restrict) - review Review? - rentalBookings RentalBooking[] + review Review? @@index([carbetId]) @@index([tenantId]) @@ -370,163 +361,3 @@ model Translation { @@id([key, lang]) @@index([lang]) } - -model PasswordResetToken { - tokenHash String @id - userId String - expiresAt DateTime - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([expiresAt]) -} - -model Favorite { - userId String - carbetId String - createdAt DateTime @default(now()) - - @@id([userId, carbetId]) - @@index([userId]) - @@index([carbetId]) -} - -enum RoadAccess { - NONE - DRY_SEASON_ONLY - ALL_YEAR -} - -enum Electricity { - NONE - SOLAR - GENERATOR_READY - EDF -} - -enum RentalCategory { - SLEEP - NAVIGATION - FISHING - COOKING - SAFETY -} - -enum RentalBookingStatus { - PENDING - CONFIRMED - HANDED_OVER - RETURNED - CANCELLED -} - -model RentalProvider { - id String @id @default(cuid()) - name String - isSystemD Boolean @default(false) - managedByUserId String? - contactEmail String? - contactPhone String? - rivers String[] @default([]) - description String? - commissionPct Decimal @db.Decimal(5, 2) @default(0) - active Boolean @default(true) - approved Boolean @default(false) - approvedAt DateTime? - approvedBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - manager User? @relation(fields: [managedByUserId], references: [id], onDelete: SetNull) - items RentalItem[] - rentalBookings RentalBooking[] - - @@index([active, approved]) - @@index([managedByUserId]) -} - -model RentalItem { - id String @id @default(cuid()) - providerId String - category RentalCategory - name String - description String? - imageUrl String? - pricePerDay Decimal @db.Decimal(8, 2) - pricePerWeek Decimal? @db.Decimal(8, 2) - deposit Decimal @db.Decimal(8, 2) @default(0) - totalQty Int @default(1) - withMotor Boolean @default(false) - fuelIncluded Boolean @default(false) - requiresLicense Boolean @default(false) - active Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - provider RentalProvider @relation(fields: [providerId], references: [id], onDelete: Cascade) - availabilities RentalItemAvailability[] - lines RentalLine[] - - @@index([providerId]) - @@index([category, active]) -} - -model RentalItemAvailability { - id String @id @default(cuid()) - itemId String - startDate DateTime - endDate DateTime - qty Int - reason String - rentalBookingId String? - createdAt DateTime @default(now()) - - item RentalItem @relation(fields: [itemId], references: [id], onDelete: Cascade) - - @@index([itemId, startDate, endDate]) - @@index([rentalBookingId]) -} - -model RentalBooking { - id String @id @default(cuid()) - bookingId String? - tenantId String - providerId String - startDate DateTime - endDate DateTime - status RentalBookingStatus @default(PENDING) - paymentStatus PaymentStatus @default(PENDING) - itemsTotal Decimal @db.Decimal(10, 2) - depositTotal Decimal @db.Decimal(10, 2) - commissionAmount Decimal @db.Decimal(10, 2) @default(0) - amount Decimal @db.Decimal(10, 2) - currency String @default("EUR") - stripeSessionId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - booking Booking? @relation(fields: [bookingId], references: [id], onDelete: SetNull) - tenant User @relation("RentalBookingTenant", fields: [tenantId], references: [id], onDelete: Restrict) - provider RentalProvider @relation(fields: [providerId], references: [id], onDelete: Restrict) - lines RentalLine[] - - @@index([tenantId, status]) - @@index([providerId, status]) - @@index([bookingId]) - @@index([startDate, endDate]) -} - -model RentalLine { - id String @id @default(cuid()) - rentalBookingId String - itemId String - qty Int - pricePerDay Decimal @db.Decimal(8, 2) - deposit Decimal @db.Decimal(8, 2) @default(0) - lineTotal Decimal @db.Decimal(10, 2) - - rentalBooking RentalBooking @relation(fields: [rentalBookingId], references: [id], onDelete: Cascade) - item RentalItem @relation(fields: [itemId], references: [id], onDelete: Restrict) - - @@index([rentalBookingId]) -} diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png deleted file mode 100644 index a185b67..0000000 Binary files a/public/icons/apple-touch-icon.png and /dev/null differ diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png deleted file mode 100644 index c062acf..0000000 Binary files a/public/icons/favicon-32.png and /dev/null differ diff --git a/public/icons/icon-192-maskable.png b/public/icons/icon-192-maskable.png deleted file mode 100644 index e80f811..0000000 Binary files a/public/icons/icon-192-maskable.png and /dev/null differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png deleted file mode 100644 index cb0fd13..0000000 Binary files a/public/icons/icon-192.png and /dev/null differ diff --git a/public/icons/icon-512-maskable.png b/public/icons/icon-512-maskable.png deleted file mode 100644 index 5041e00..0000000 Binary files a/public/icons/icon-512-maskable.png and /dev/null differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png deleted file mode 100644 index abb04bf..0000000 Binary files a/public/icons/icon-512.png and /dev/null differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest deleted file mode 100644 index 2f32e8d..0000000 --- a/public/manifest.webmanifest +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "Karbé — carbets fluviaux de Guyane", - "short_name": "Karbé", - "description": "Au fil de l'eau : louez des carbets le long des fleuves de Guyane.", - "start_url": "/decouvrir", - "id": "/decouvrir", - "scope": "/", - "display": "standalone", - "orientation": "portrait", - "background_color": "#000000", - "theme_color": "#059669", - "lang": "fr", - "categories": ["travel", "lifestyle"], - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/icons/icon-192-maskable.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/icon-512-maskable.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "shortcuts": [ - { - "name": "Au fil de l'eau", - "short_name": "Découvrir", - "url": "/decouvrir", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - }, - { - "name": "Mes favoris", - "short_name": "Favoris", - "url": "/mes-favoris", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - }, - { - "name": "Mon compte", - "short_name": "Compte", - "url": "/mon-compte", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - } - ] -} diff --git a/scripts/backup-postgres.sh b/scripts/backup-postgres.sh deleted file mode 100755 index abe63d4..0000000 --- a/scripts/backup-postgres.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# -# Backup nightly du PostgreSQL Karbé vers MinIO. -# Lancé par un systemd timer (karbe-backup.timer). -# -# Rétention 30 jours côté MinIO (s'appuyer sur une lifecycle policy ou un -# nettoyage côté `mc rm` planifié — TODO si on veut être propre). - -set -euo pipefail - -STAMP=$(date -u +%Y%m%d-%H%M%S) -DUMP_DIR=/tmp/karbe-backup -DUMP_FILE="$DUMP_DIR/karbe-${STAMP}.sql.gz" -BUCKET_DEST="karbe-backups/postgres/karbe-${STAMP}.sql.gz" - -mkdir -p "$DUMP_DIR" - -# Dump compressé depuis le conteneur postgres -docker compose -f /home/ubuntu/karbe/docker-compose.prod.yml \ - -f /home/ubuntu/karbe/docker-compose.override.yml \ - exec -T postgres pg_dump -U karbe -d karbe \ - | gzip > "$DUMP_FILE" - -SIZE=$(stat -c %s "$DUMP_FILE") -echo "[$(date -u +%FT%TZ)] dump created size=${SIZE}B path=${DUMP_FILE}" - -# Push vers MinIO via mc Docker -docker run --rm --network karbe-net \ - --entrypoint /bin/sh \ - -v "$DUMP_DIR:/dump" \ - -e MINIO_ROOT_USER \ - -e MINIO_ROOT_PASSWORD \ - minio/mc:latest -c " - mc alias set karbe http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" >/dev/null 2>&1 && \ - mc mb karbe/karbe-backups --ignore-existing >/dev/null 2>&1 && \ - mc cp /dump/karbe-${STAMP}.sql.gz karbe/${BUCKET_DEST} - " - -echo "[$(date -u +%FT%TZ)] uploaded to karbe/${BUCKET_DEST}" - -# Nettoyage local -rm -f "$DUMP_FILE" - -# Rétention : supprime les backups > 30 jours dans MinIO -docker run --rm --network karbe-net \ - --entrypoint /bin/sh \ - -e MINIO_ROOT_USER \ - -e MINIO_ROOT_PASSWORD \ - minio/mc:latest -c " - mc alias set karbe http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" >/dev/null 2>&1 && \ - mc rm --recursive --force --older-than 30d karbe/karbe-backups/ 2>/dev/null || true - " - -echo "[$(date -u +%FT%TZ)] retention sweep done (>30d removed)" diff --git a/src/app/accueil/page.tsx b/src/app/accueil/page.tsx deleted file mode 100644 index 513e1ac..0000000 --- a/src/app/accueil/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Link from "next/link"; -import { IfPluginEnabled } from "@/components/IfPluginEnabled"; -import { HeroSection } from "@/components/landing/HeroSection"; -import { ExperiencesSection } from "@/components/landing/ExperiencesSection"; -import { HowItWorksSection } from "@/components/landing/HowItWorksSection"; -import { CESection } from "@/components/landing/CESection"; -import { TestimonialsSection } from "@/components/landing/TestimonialsSection"; -import { LandingFooter } from "@/components/landing/Footer"; - -export const metadata = { title: "Accueil — Karbé" }; - -/** - * Landing « marketing » historique (hero + sections + footer riche). Conservée - * à /accueil après la promotion de /decouvrir comme nouvelle page d'index. - */ -export default function LandingPage() { - return ( - <> - -
-

- Karbé — carbets fluviaux de Guyane -

-

- La marketplace pour louer des carbets le long des fleuves de Guyane. -

-
- - Au fil de l'eau - - - Catalogue - -
-
- - } - > - -
- - - - - - - - - - ); -} diff --git a/src/app/admin/carbets/[id]/page.tsx b/src/app/admin/carbets/[id]/page.tsx index bf7a972..02c0d80 100644 --- a/src/app/admin/carbets/[id]/page.tsx +++ b/src/app/admin/carbets/[id]/page.tsx @@ -7,7 +7,7 @@ import { } from "@/lib/admin/carbets"; import { CarbetForm } from "../_components/CarbetForm"; import { StatusBadge } from "@/components/admin/StatusBadge"; -import { MediaUploader } from "@/components/MediaUploader"; +import { MediaManager } from "./_components/MediaManager"; import { StatusActions } from "./_components/StatusActions"; import { updateCarbetAction } from "../actions"; @@ -61,21 +61,16 @@ export default async function EditCarbetPage({ params }: PageProps) { -
-

- Médias -

- ({ - id: m.id, - type: m.type, - s3Key: m.s3Key, - s3Url: m.s3Url, - sortOrder: m.sortOrder, - }))} - /> -
+ ({ + id: m.id, + type: m.type, + s3Key: m.s3Key, + s3Url: m.s3Url, + sortOrder: m.sortOrder, + }))} + /> - {/* Critères opérationnels */} -
-

- Critères opérationnels -

-

- Les 4 dealbreakers d'un séjour en carbet guyanais. Indispensable pour les filtres recherche. -

-
- - - - - - - - - - - - - - - -
-
- {/* Séjour & tarif */}

Séjour & tarif

diff --git a/src/app/admin/carbets/actions.ts b/src/app/admin/carbets/actions.ts index 2004bd8..9e2fbff 100644 --- a/src/app/admin/carbets/actions.ts +++ b/src/app/admin/carbets/actions.ts @@ -10,9 +10,7 @@ import { prisma } from "@/lib/prisma"; import { AccessType, CarbetStatus, - Electricity, MediaType, - RoadAccess, TransportMode, UserRole, } from "@/generated/prisma/enums"; @@ -31,16 +29,6 @@ const baseCarbetSchema = z.object({ capacity: z.coerce.number().int().min(1).max(100), nightlyPrice: z.coerce.number().min(0).max(100000), accessType: z.enum([AccessType.ROAD_AND_RIVER, AccessType.RIVER_ONLY]), - roadAccess: z - .enum([RoadAccess.NONE, RoadAccess.DRY_SEASON_ONLY, RoadAccess.ALL_YEAR]) - .optional() - .nullable(), - electricity: z - .enum([Electricity.NONE, Electricity.SOLAR, Electricity.GENERATOR_READY, Electricity.EDF]) - .optional() - .nullable(), - gsmAtCarbet: z.preprocess((v) => v === "yes" || v === true, z.boolean()), - gsmExitDistanceKm: z.coerce.number().min(0).max(50).optional().nullable(), roadAccessNote: z.string().trim().max(1000).optional().nullable(), pirogueDurationMin: z.coerce.number().int().min(0).max(1440).optional().nullable(), minStayNights: z.coerce.number().int().min(1).max(365).optional().nullable(), @@ -65,11 +53,9 @@ function parseFromFormData(fd: FormData) { if (typeof v === "string") obj[k] = v; } // Normalise les champs optionnels nullables - ["roadAccessNote", "pirogueDurationMin", "minStayNights", "maxStayNights", "minCapacity", "transportMode", "pirogueProviderId", "roadAccess", "electricity", "gsmExitDistanceKm"].forEach( + ["roadAccessNote", "pirogueDurationMin", "minStayNights", "maxStayNights", "minCapacity", "transportMode", "pirogueProviderId"].forEach( (k) => (obj[k] = normalizeNullable(obj[k] as string | null | undefined)), ); - // gsmAtCarbet : si pas posté, on garde la valeur (sera traité par preprocess Zod) - if (!("gsmAtCarbet" in obj)) obj.gsmAtCarbet = "no"; return obj; } diff --git a/src/app/admin/rental-items/[id]/_components/ItemInlineActions.tsx b/src/app/admin/rental-items/[id]/_components/ItemInlineActions.tsx deleted file mode 100644 index 8a6a00f..0000000 --- a/src/app/admin/rental-items/[id]/_components/ItemInlineActions.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; - -type Props = { - active: boolean; - toggleActiveAction: (active: boolean) => Promise<{ ok: true } | { ok: false; error: string } | undefined>; - deleteAction: () => Promise<{ ok: true } | { ok: false; error: string } | undefined | void>; -}; - -export function ItemInlineActions({ active, toggleActiveAction, deleteAction }: Props) { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - const [confirmDelete, setConfirmDelete] = useState(false); - const [error, setError] = useState(null); - - function toggle() { - setError(null); - startTransition(async () => { - const res = await toggleActiveAction(!active); - if (res && (res as { ok?: boolean }).ok === false) setError((res as { error: string }).error); - router.refresh(); - }); - } - function del() { - setError(null); - startTransition(async () => { - const res = await deleteAction(); - if (res && (res as { ok?: boolean }).ok === false) { - setError((res as { error: string }).error); - setConfirmDelete(false); - } - }); - } - - return ( -
-
- - {confirmDelete ? ( -
- Supprimer ? - - -
- ) : ( - - )} -
- {error ?
{error}
: null} -
- ); -} diff --git a/src/app/admin/rental-items/[id]/page.tsx b/src/app/admin/rental-items/[id]/page.tsx deleted file mode 100644 index 8f4dd4a..0000000 --- a/src/app/admin/rental-items/[id]/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { notFound } from "next/navigation"; -import Link from "next/link"; - -import { StatusBadge } from "@/components/admin/StatusBadge"; -import { getRentalItemForAdmin, listProvidersForSelect, RENTAL_CATEGORY_LABEL } from "@/lib/admin/rental-items"; - -import { ItemForm } from "../_components/ItemForm"; -import { ItemInlineActions } from "./_components/ItemInlineActions"; -import { deleteRentalItemAction, toggleRentalItemActiveAction, updateRentalItemAction } from "../actions"; - -export const dynamic = "force-dynamic"; - -type PageProps = { params: Promise<{ id: string }> }; - -export default async function EditRentalItemPage({ params }: PageProps) { - const { id } = await params; - const [item, providers] = await Promise.all([getRentalItemForAdmin(id), listProvidersForSelect()]); - if (!item) notFound(); - - const updateThis = async (fd: FormData) => { - "use server"; - return await updateRentalItemAction(id, fd); - }; - const toggleActiveThis = async (active: boolean) => { - "use server"; - return await toggleRentalItemActiveAction(id, active); - }; - const deleteThis = async () => { - "use server"; - return await deleteRentalItemAction(id); - }; - - return ( -
-
-
- - ← Tous les items - -

- {item.name} - -

-

- {RENTAL_CATEGORY_LABEL[item.category]} ·{" "} - - {item.provider.name} - - {item.provider.isSystemD ? " (System D)" : ""} -

-
- -
- -
- -
-
- ); -} diff --git a/src/app/admin/rental-items/_components/ItemForm.tsx b/src/app/admin/rental-items/_components/ItemForm.tsx deleted file mode 100644 index 27ad4b2..0000000 --- a/src/app/admin/rental-items/_components/ItemForm.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { FormField, inputCls, selectCls, textareaCls } from "@/components/admin/FormField"; -import { RENTAL_CATEGORY_LABEL, RENTAL_CATEGORIES } from "@/lib/rental-category-labels"; - -type Props = { - providers: { id: string; name: string; isSystemD: boolean }[]; - initial?: { - providerId?: string; - category?: string; - name?: string; - description?: string | null; - imageUrl?: string | null; - pricePerDay?: string | number; - pricePerWeek?: string | number | null; - deposit?: string | number; - totalQty?: number; - withMotor?: boolean; - fuelIncluded?: boolean; - requiresLicense?: boolean; - active?: boolean; - }; - action: (fd: FormData) => Promise<{ ok: false; error: string } | { ok: true } | undefined>; - submitLabel?: string; -}; - -export function ItemForm({ providers, initial = {}, action, submitLabel = "Enregistrer" }: Props) { - const [pending, startTransition] = useTransition(); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - - function onSubmit(fd: FormData) { - setError(null); - setSuccess(null); - startTransition(async () => { - const res = await action(fd); - if (res && res.ok === false) setError(res.error); - else if (res && res.ok === true) setSuccess("Enregistré."); - }); - } - - return ( -
-
-
- - - - - - - - - - -