This commit is contained in:
David 2026-01-25 16:00:22 +01:00
parent 301409624b
commit 10b45599ae
3 changed files with 263 additions and 105 deletions

View File

@ -27,14 +27,15 @@ export default function OrganizationSettingsPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState<TabType>('information'); const [activeTab, setActiveTab] = useState<TabType>('information');
// Auto-switch to subscription tab if coming back from Stripe // Auto-switch to subscription tab if coming back from Stripe (only for ADMIN/MANAGER)
useEffect(() => { useEffect(() => {
const isSuccess = searchParams.get('success') === 'true'; const isSuccess = searchParams.get('success') === 'true';
const isCanceled = searchParams.get('canceled') === 'true'; const isCanceled = searchParams.get('canceled') === 'true';
if (isSuccess || isCanceled) { const canAccessBilling = user?.role === 'ADMIN' || user?.role === 'MANAGER';
if ((isSuccess || isCanceled) && canAccessBilling) {
setActiveTab('subscription'); setActiveTab('subscription');
} }
}, [searchParams]); }, [searchParams, user?.role]);
const [organization, setOrganization] = useState<OrganizationResponse | null>(null); const [organization, setOrganization] = useState<OrganizationResponse | null>(null);
const [formData, setFormData] = useState<OrganizationForm>({ const [formData, setFormData] = useState<OrganizationForm>({
name: '', name: '',
@ -165,6 +166,9 @@ export default function OrganizationSettingsPage() {
); );
} }
// Check if user can view subscription and licenses (only ADMIN and MANAGER)
const canViewBilling = user?.role === 'ADMIN' || user?.role === 'MANAGER';
const tabs = [ const tabs = [
{ {
id: 'information' as TabType, id: 'information' as TabType,
@ -185,24 +189,27 @@ export default function OrganizationSettingsPage() {
</svg> </svg>
), ),
}, },
{ // Only show subscription and licenses tabs for ADMIN and MANAGER roles
id: 'subscription' as TabType, ...(canViewBilling ? [
label: 'Abonnement', {
icon: ( id: 'subscription' as TabType,
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> label: 'Abonnement',
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /> icon: (
</svg> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
), <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
}, </svg>
{ ),
id: 'licenses' as TabType, },
label: 'Licences', {
icon: ( id: 'licenses' as TabType,
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> label: 'Licences',
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> icon: (
</svg> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
), <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
}, </svg>
),
},
] : []),
]; ];
return ( return (
@ -432,9 +439,9 @@ export default function OrganizationSettingsPage() {
</div> </div>
)} )}
{activeTab === 'subscription' && <SubscriptionTab />} {activeTab === 'subscription' && canViewBilling && <SubscriptionTab />}
{activeTab === 'licenses' && <LicensesTab />} {activeTab === 'licenses' && canViewBilling && <LicensesTab />}
</div> </div>
{/* Actions (only for information and address tabs) */} {/* Actions (only for information and address tabs) */}

View File

@ -22,6 +22,12 @@ import {
Container, Container,
FileText, FileText,
LayoutDashboard, LayoutDashboard,
Bell,
BookOpen,
Users,
Building2,
Check,
X,
} from 'lucide-react'; } from 'lucide-react';
import { useAuth } from '@/lib/context/auth-context'; import { useAuth } from '@/lib/context/auth-context';
@ -50,6 +56,7 @@ export default function LandingPage() {
const featuresRef = useRef(null); const featuresRef = useRef(null);
const statsRef = useRef(null); const statsRef = useRef(null);
const toolsRef = useRef(null); const toolsRef = useRef(null);
const pricingRef = useRef(null);
const testimonialsRef = useRef(null); const testimonialsRef = useRef(null);
const ctaRef = useRef(null); const ctaRef = useRef(null);
@ -57,6 +64,7 @@ export default function LandingPage() {
const isFeaturesInView = useInView(featuresRef, { once: true }); const isFeaturesInView = useInView(featuresRef, { once: true });
const isStatsInView = useInView(statsRef, { once: true }); const isStatsInView = useInView(statsRef, { once: true });
const isToolsInView = useInView(toolsRef, { once: true }); const isToolsInView = useInView(toolsRef, { once: true });
const isPricingInView = useInView(pricingRef, { once: true });
const isTestimonialsInView = useInView(testimonialsRef, { once: true }); const isTestimonialsInView = useInView(testimonialsRef, { once: true });
const isCtaInView = useInView(ctaRef, { once: true }); const isCtaInView = useInView(ctaRef, { once: true });
@ -72,84 +80,90 @@ export default function LandingPage() {
}, []); }, []);
const features = [ const features = [
{
icon: Search,
title: 'Recherche Intelligente',
description:
'Comparez instantanément les tarifs de plus de 50 compagnies maritimes en temps réel.',
color: 'from-blue-500 to-cyan-500',
},
{
icon: Zap,
title: 'Réservation Rapide',
description: 'Réservez vos containers LCL/FCL en quelques clics avec confirmation immédiate.',
color: 'from-purple-500 to-pink-500',
},
{ {
icon: BarChart3, icon: BarChart3,
title: 'Tableau de Bord', title: 'Dashboard Analytics',
description: 'Suivez tous vos envois en temps réel avec des KPIs détaillés et des analytics.', description:
'Suivez tous vos KPIs en temps réel : bookings, volumes, revenus et alertes personnalisées.',
color: 'from-blue-500 to-cyan-500',
link: '/dashboard',
},
{
icon: Package,
title: 'Gestion des Bookings',
description: 'Créez, gérez et suivez vos réservations maritimes LCL/FCL avec un historique complet.',
color: 'from-purple-500 to-pink-500',
link: '/dashboard/bookings',
},
{
icon: FileText,
title: 'Documents Maritimes',
description: 'Centralisez tous vos documents : B/L, factures, certificats et documents douaniers.',
color: 'from-orange-500 to-red-500', color: 'from-orange-500 to-red-500',
link: '/dashboard/documents',
}, },
{ {
icon: Globe, icon: Search,
title: '10 000+ Ports', title: 'Track & Trace',
description: description:
'Accédez à un réseau mondial de ports avec des données actualisées quotidiennement.', 'Suivez vos conteneurs en temps réel auprès de 10+ transporteurs majeurs (Maersk, MSC, CMA CGM...).',
color: 'from-green-500 to-emerald-500', color: 'from-green-500 to-emerald-500',
link: '/dashboard/track-trace',
}, },
{ {
icon: TrendingUp, icon: BookOpen,
title: 'Meilleurs Prix', title: 'Wiki Maritime',
description: description:
'Optimisation automatique des tarifs pour vous garantir les prix les plus compétitifs.', 'Base de connaissances complète : Incoterms, documents, procédures douanières et plus encore.',
color: 'from-yellow-500 to-orange-500', color: 'from-yellow-500 to-orange-500',
link: '/dashboard/wiki',
}, },
{ {
icon: Shield, icon: Bell,
title: 'Sécurisé', title: 'Notifications Temps Réel',
description: description:
'Plateforme conforme aux standards internationaux avec chiffrement de bout en bout.', 'Restez informé avec des alertes instantanées sur vos bookings, documents et mises à jour.',
color: 'from-indigo-500 to-purple-500', color: 'from-indigo-500 to-purple-500',
link: '/dashboard',
}, },
]; ];
const tools = [ const tools = [
{ {
icon: Calculator, icon: LayoutDashboard,
title: 'Calculateur de Fret', title: 'Dashboard',
description: 'Estimez vos coûts de transport en temps réel', description: 'Vue d\'ensemble de votre activité maritime',
link: '/tools/calculator', link: '/dashboard',
},
{
icon: MapPin,
title: 'Distance & Temps',
description: 'Calculez la distance et le temps entre ports',
link: '/tools/distance',
}, },
{ {
icon: Package, icon: Package,
title: 'Optimiseur de Chargement', title: 'Mes Bookings',
description: "Maximisez l'utilisation de vos containers", description: 'Gérez toutes vos réservations en un seul endroit',
link: '/tools/load-optimizer', link: '/dashboard/bookings',
},
{
icon: Ship,
title: 'Suivi en Temps Réel',
description: 'Trackez vos envois partout dans le monde',
link: '/tracking',
}, },
{ {
icon: FileText, icon: FileText,
title: 'Documents Maritimes', title: 'Documents',
description: 'Générez automatiquement vos documents', description: 'Accédez à tous vos documents maritimes',
link: '/tools/documents', link: '/dashboard/documents',
}, },
{ {
icon: TrendingUp, icon: Search,
title: 'Index des Tarifs', title: 'Track & Trace',
description: 'Suivez les tendances du marché maritime', description: 'Suivez vos conteneurs en temps réel',
link: '/tools/freight-index', link: '/dashboard/track-trace',
},
{
icon: BookOpen,
title: 'Wiki Maritime',
description: 'Base de connaissances du fret maritime',
link: '/dashboard/wiki',
},
{
icon: Users,
title: 'Mon Profil',
description: 'Gérez vos informations personnelles',
link: '/dashboard/profile',
}, },
]; ];
@ -160,6 +174,63 @@ export default function LandingPage() {
{ value: '99.5%', label: 'Disponibilité', icon: CheckCircle2 }, { value: '99.5%', label: 'Disponibilité', icon: CheckCircle2 },
]; ];
const pricingPlans = [
{
name: 'Starter',
price: 'Gratuit',
period: '',
description: 'Idéal pour découvrir la plateforme',
features: [
{ text: 'Jusqu\'à 5 bookings/mois', included: true },
{ text: 'Track & Trace illimité', included: true },
{ text: 'Wiki maritime complet', included: true },
{ text: 'Dashboard basique', included: true },
{ text: 'Support par email', included: true },
{ text: 'Gestion des documents', included: false },
{ text: 'Notifications temps réel', included: false },
{ text: 'API access', included: false },
],
cta: 'Commencer gratuitement',
highlighted: false,
},
{
name: 'Professional',
price: '99€',
period: '/mois',
description: 'Pour les transitaires en croissance',
features: [
{ text: 'Bookings illimités', included: true },
{ text: 'Track & Trace illimité', included: true },
{ text: 'Wiki maritime complet', included: true },
{ text: 'Dashboard avancé + KPIs', included: true },
{ text: 'Support prioritaire', included: true },
{ text: 'Gestion des documents', included: true },
{ text: 'Notifications temps réel', included: true },
{ text: 'API access', included: false },
],
cta: 'Essai gratuit 14 jours',
highlighted: true,
},
{
name: 'Enterprise',
price: 'Sur mesure',
period: '',
description: 'Pour les grandes entreprises',
features: [
{ text: 'Tout Professional +', included: true },
{ text: 'API access complet', included: true },
{ text: 'Intégrations personnalisées', included: true },
{ text: 'Account manager dédié', included: true },
{ text: 'SLA garanti 99.9%', included: true },
{ text: 'Formation sur site', included: true },
{ text: 'Multi-organisations', included: true },
{ text: 'Audit & conformité', included: true },
],
cta: 'Contactez-nous',
highlighted: false,
},
];
const testimonials = [ const testimonials = [
{ {
quote: quote:
@ -474,15 +545,23 @@ export default function LandingPage() {
key={index} key={index}
variants={itemVariants} variants={itemVariants}
whileHover={{ scale: 1.05, y: -10 }} whileHover={{ scale: 1.05, y: -10 }}
className="group bg-white p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all border border-gray-100 cursor-pointer"
> >
<div <Link
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`} href={feature.link}
className="group block bg-white p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all border border-gray-100"
> >
<IconComponent className="w-7 h-7 text-white" /> <div
</div> className={`w-14 h-14 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}
<h3 className="text-2xl font-bold text-brand-navy mb-3">{feature.title}</h3> >
<p className="text-gray-600 leading-relaxed">{feature.description}</p> <IconComponent className="w-7 h-7 text-white" />
</div>
<h3 className="text-2xl font-bold text-brand-navy mb-3 group-hover:text-brand-turquoise transition-colors">{feature.title}</h3>
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
<div className="mt-4 flex items-center text-brand-turquoise font-medium opacity-0 group-hover:opacity-100 transition-opacity">
<span>Découvrir</span>
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</div>
</Link>
</motion.div> </motion.div>
); );
})} })}
@ -603,6 +682,103 @@ export default function LandingPage() {
</div> </div>
</section> </section>
{/* Pricing Section */}
<section
ref={pricingRef}
id="pricing"
className="py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white"
>
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4">
Tarifs simples et transparents
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Choisissez le plan adapté à vos besoins. Évoluez à tout moment.
</p>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
animate={isPricingInView ? 'visible' : 'hidden'}
className="grid grid-cols-1 md:grid-cols-3 gap-8"
>
{pricingPlans.map((plan, index) => (
<motion.div
key={index}
variants={itemVariants}
whileHover={{ y: -10 }}
className={`relative bg-white rounded-2xl shadow-lg border-2 transition-all ${
plan.highlighted
? 'border-brand-turquoise shadow-2xl scale-105'
: 'border-gray-200 hover:border-brand-turquoise/50'
}`}
>
{plan.highlighted && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<span className="bg-brand-turquoise text-white text-sm font-bold px-4 py-1 rounded-full">
Populaire
</span>
</div>
)}
<div className="p-8">
<h3 className="text-2xl font-bold text-brand-navy mb-2">{plan.name}</h3>
<p className="text-gray-600 mb-6">{plan.description}</p>
<div className="mb-6">
<span className="text-5xl font-bold text-brand-navy">{plan.price}</span>
<span className="text-gray-500">{plan.period}</span>
</div>
<ul className="space-y-3 mb-8">
{plan.features.map((feature, featureIndex) => (
<li key={featureIndex} className="flex items-center">
{feature.included ? (
<Check className="w-5 h-5 text-brand-green mr-3 flex-shrink-0" />
) : (
<X className="w-5 h-5 text-gray-300 mr-3 flex-shrink-0" />
)}
<span className={feature.included ? 'text-gray-700' : 'text-gray-400'}>
{feature.text}
</span>
</li>
))}
</ul>
<Link
href={plan.name === 'Enterprise' ? '/contact' : '/register'}
className={`block w-full text-center py-3 px-6 rounded-lg font-semibold transition-all ${
plan.highlighted
? 'bg-brand-turquoise text-white hover:bg-brand-turquoise/90 shadow-lg hover:shadow-xl'
: 'bg-gray-100 text-brand-navy hover:bg-gray-200'
}`}
>
{plan.cta}
</Link>
</div>
</motion.div>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }}
className="mt-12 text-center"
>
<p className="text-gray-600">
Tous les plans incluent un essai gratuit de 14 jours. Aucune carte bancaire requise.
</p>
<p className="text-sm text-gray-500 mt-2">
Des questions ? <Link href="/contact" className="text-brand-turquoise hover:underline">Contactez notre équipe commerciale</Link>
</p>
</motion.div>
</div>
</section>
{/* How It Works Section */} {/* How It Works Section */}
<section className="py-20 lg:py-32 bg-gradient-to-br from-brand-navy to-brand-navy/95 relative overflow-hidden"> <section className="py-20 lg:py-32 bg-gradient-to-br from-brand-navy to-brand-navy/95 relative overflow-hidden">
<div className="absolute inset-0 opacity-10"> <div className="absolute inset-0 opacity-10">

View File

@ -274,9 +274,6 @@ export default function SubscriptionTab() {
: `Plus que ${subscription.availableLicenses} licence${subscription.availableLicenses === 1 ? '' : 's'} disponible${subscription.availableLicenses === 1 ? '' : 's'}.`} : `Plus que ${subscription.availableLicenses} licence${subscription.availableLicenses === 1 ? '' : 's'} disponible${subscription.availableLicenses === 1 ? '' : 's'}.`}
</p> </p>
)} )}
<p className="mt-2 text-xs text-gray-400">
Les administrateurs (ADMIN) ont des licences illimitées et ne sont pas comptés.
</p>
</div> </div>
{/* Billing Period */} {/* Billing Period */}
@ -416,28 +413,6 @@ export default function SubscriptionTab() {
))} ))}
</div> </div>
</div> </div>
{/* Info about webhooks in development */}
{process.env.NODE_ENV === 'development' && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Mode développement</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
Pour que les webhooks Stripe fonctionnent en local, exécutez :{' '}
<code className="bg-yellow-100 px-1 rounded">stripe listen --forward-to localhost:4000/api/v1/subscriptions/webhook</code>
</p>
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
} }