)}
diff --git a/apps/frontend/app/[locale]/dashboard/search-advanced/page.tsx b/apps/frontend/app/[locale]/dashboard/search-advanced/page.tsx
index 50e18bb..f7fdcf5 100644
--- a/apps/frontend/app/[locale]/dashboard/search-advanced/page.tsx
+++ b/apps/frontend/app/[locale]/dashboard/search-advanced/page.tsx
@@ -3,10 +3,12 @@
import { useState, useEffect } from 'react';
import { useRouter } from '@/i18n/navigation';
import { useSearchParams } from 'next/navigation';
-import { Search, Loader2 } from 'lucide-react';
+import { Search, Loader2, Lock, Sparkles } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { getAvailableOrigins, getAvailableDestinations, RoutePortInfo } from '@/lib/api/rates';
+import { useReservationQuota } from '@/hooks/useReservationQuota';
+import { Link } from '@/i18n/navigation';
import dynamic from 'next/dynamic';
const PortRouteMapLoader = () => {
@@ -69,8 +71,10 @@ const OPTION_KEYS: (keyof SearchForm)[] = [
export default function AdvancedSearchPage() {
const t = useTranslations('dashboard.rateSearch');
+ const tLimit = useTranslations('dashboard.bookingsList.limit');
const router = useRouter();
const searchParams = useSearchParams();
+ const quota = useReservationQuota();
const [editBookingId, setEditBookingId] = useState(null);
const [searchForm, setSearchForm] = useState({
origin: '',
@@ -817,6 +821,39 @@ export default function AdvancedSearchPage() {
);
+ // Block starting a NEW reservation when the free-plan yearly limit is reached.
+ // Editing an existing unpaid booking (editBookingId) does not create a new
+ // shipment, so it is never blocked.
+ const isEditing = !!searchParams.get('editBookingId');
+ if (quota.blocked && !isEditing && !quota.loading) {
+ return (
+
diff --git a/apps/frontend/messages/en.json b/apps/frontend/messages/en.json
index c8bd9eb..c1947ed 100644
--- a/apps/frontend/messages/en.json
+++ b/apps/frontend/messages/en.json
@@ -398,6 +398,13 @@
"title": "Bookings",
"description": "Manage and track your shipments",
"new": "New Booking",
+ "limit": {
+ "title": "Reservation limit reached",
+ "message": "Your free plan is limited to {max} reservations per year. Upgrade your plan to keep booking.",
+ "upgrade": "Upgrade my plan",
+ "upgradeShort": "Upgrade",
+ "back": "Back to bookings"
+ },
"transferBanner": {
"title": "Transfer declared",
"message": "Your transfer has been recorded. An administrator will verify receipt and activate your booking. You will be notified once validated."
diff --git a/apps/frontend/messages/fr.json b/apps/frontend/messages/fr.json
index 51d7eda..556b82b 100644
--- a/apps/frontend/messages/fr.json
+++ b/apps/frontend/messages/fr.json
@@ -398,6 +398,13 @@
"title": "Réservations",
"description": "Gérez et suivez vos envois",
"new": "Nouvelle Réservation",
+ "limit": {
+ "title": "Limite de réservations atteinte",
+ "message": "Votre abonnement gratuit est limité à {max} réservations par an. Passez à un abonnement supérieur pour continuer à réserver.",
+ "upgrade": "Améliorer mon abonnement",
+ "upgradeShort": "Améliorer",
+ "back": "Retour aux réservations"
+ },
"transferBanner": {
"title": "Virement déclaré",
"message": "Votre virement a été enregistré. Un administrateur va vérifier la réception et activer votre booking. Vous serez notifié dès la validation."
diff --git a/apps/frontend/src/hooks/useReservationQuota.ts b/apps/frontend/src/hooks/useReservationQuota.ts
new file mode 100644
index 0000000..e7e0922
--- /dev/null
+++ b/apps/frontend/src/hooks/useReservationQuota.ts
@@ -0,0 +1,46 @@
+'use client';
+
+import { useQuery } from '@tanstack/react-query';
+import { listCsvBookings } from '@/lib/api';
+import { useSubscription } from '@/lib/context/subscription-context';
+
+/**
+ * Reservation (shipment) quota for the current plan.
+ *
+ * The entry (free) plan is limited to a number of reservations per calendar
+ * year (`maxShipmentsPerYear`, e.g. 5). Higher plans are unlimited (-1). This
+ * hook lets the UI proactively prompt an upgrade once the limit is reached,
+ * instead of letting the user go through the whole flow only to be blocked.
+ */
+export function useReservationQuota() {
+ const { subscription, loading: subLoading } = useSubscription();
+
+ const { data, isLoading } = useQuery({
+ // Same query key as the bookings list so the request is shared/cached.
+ queryKey: ['csv-bookings'],
+ queryFn: () => listCsvBookings({ page: 1, limit: 1000 }),
+ });
+
+ const max = subscription?.planDetails?.maxShipmentsPerYear ?? -1;
+ const unlimited = max === -1;
+
+ const currentYear = new Date().getFullYear();
+ const bookings = data?.bookings ?? [];
+ const used = bookings.filter(
+ b => b.createdAt && new Date(b.createdAt).getFullYear() === currentYear
+ ).length;
+
+ const limitReached = !unlimited && used >= max;
+ const blocked = limitReached;
+
+ return {
+ loading: subLoading || isLoading,
+ plan: subscription?.plan ?? null,
+ max,
+ used,
+ remaining: unlimited ? Infinity : Math.max(0, max - used),
+ unlimited,
+ limitReached,
+ blocked,
+ };
+}