diff --git a/app/(main)/aide/documentation/page.tsx b/app/(main)/aide/documentation/page.tsx
new file mode 100644
index 0000000..03b64d1
--- /dev/null
+++ b/app/(main)/aide/documentation/page.tsx
@@ -0,0 +1,241 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ BookOpen,
+ Search,
+ ChevronRight,
+ FileText,
+ Download,
+ ExternalLink,
+ ArrowLeft,
+} from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page de documentation
+ * Guides complets et documentation technique
+ */
+export default function DocumentationPage() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [selectedCategory, setSelectedCategory] = useState(null);
+
+ const categories = [
+ {
+ id: "getting-started",
+ title: "Démarrage",
+ description: "Premiers pas avec BTPXpress",
+ articles: [
+ "Installation et configuration",
+ "Créer votre premier chantier",
+ "Comprendre l'interface",
+ "Configuration du profil entreprise",
+ ],
+ },
+ {
+ id: "chantiers",
+ title: "Gestion des chantiers",
+ description: "Tout sur la gestion des chantiers",
+ articles: [
+ "Créer un nouveau chantier",
+ "Gérer les phases de chantier",
+ "Suivi en temps réel",
+ "Clôturer un chantier",
+ ],
+ },
+ {
+ id: "factures",
+ title: "Facturation",
+ description: "Devis, factures et paiements",
+ articles: [
+ "Créer un devis",
+ "Convertir un devis en facture",
+ "Gérer les paiements",
+ "Relances automatiques",
+ ],
+ },
+ {
+ id: "equipes",
+ title: "Équipes et planning",
+ description: "Gérer vos équipes",
+ articles: [
+ "Créer une équipe",
+ "Affecter des employés",
+ "Planifier les interventions",
+ "Gérer les disponibilités",
+ ],
+ },
+ {
+ id: "materiel",
+ title: "Matériel",
+ description: "Gestion du matériel BTP",
+ articles: [
+ "Ajouter du matériel",
+ "Planifier l'utilisation",
+ "Maintenance préventive",
+ "Historique d'utilisation",
+ ],
+ },
+ {
+ id: "rapports",
+ title: "Rapports et analyses",
+ description: "Générer des rapports",
+ articles: [
+ "Rapports d'activité",
+ "Analyse financière",
+ "Rapports personnalisés",
+ "Export des données",
+ ],
+ },
+ ];
+
+ const filteredCategories = selectedCategory
+ ? categories.filter((cat) => cat.id === selectedCategory)
+ : categories;
+
+ const allArticles = categories.flatMap((cat) =>
+ cat.articles.map((article) => ({ category: cat.title, article }))
+ );
+
+ const searchResults = searchTerm
+ ? allArticles.filter((item) =>
+ item.article.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ : [];
+
+ return (
+
+ {/* En-tête */}
+
+
+
+
+
+ Retour à l'aide
+
+
+
+
Documentation
+
+ Guides complets et documentation technique
+
+
+
+
+
+ Télécharger PDF
+
+
+
+ {/* Barre de recherche */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {/* Résultats de recherche */}
+ {searchTerm && searchResults.length > 0 && (
+
+
+ {searchResults.length} résultat{searchResults.length > 1 ? "s" : ""}{" "}
+ trouvé{searchResults.length > 1 ? "s" : ""}
+
+
+ {searchResults.map((result, index) => (
+
+
+
{result.article}
+
{result.category}
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Catégories de documentation */}
+
+ {filteredCategories.map((category) => (
+
+
+
+
+
+
+
{category.title}
+
{category.description}
+
+
+
+
+ {category.articles.map((article, index) => (
+
+
+ {article}
+
+
+ ))}
+
+
+ setSelectedCategory(category.id)}
+ >
+ Voir tous les articles
+
+
+
+ ))}
+
+
+ {/* Guides de démarrage rapide */}
+
+
+
+
+
+
+
+ Guide de démarrage rapide
+
+
+ Téléchargez notre guide PDF complet pour démarrer rapidement avec
+ BTPXpress. Il contient tous les concepts essentiels et les meilleures
+ pratiques.
+
+
+
+
+ Télécharger le guide (PDF)
+
+
+ Voir la version en ligne
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/aide/page.tsx b/app/(main)/aide/page.tsx
new file mode 100644
index 0000000..f87a381
--- /dev/null
+++ b/app/(main)/aide/page.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import React from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ BookOpen,
+ Video,
+ MessageCircle,
+ Search,
+ FileText,
+ HelpCircle,
+ Lightbulb,
+ Phone,
+ Mail,
+ ExternalLink,
+} from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page principale du centre d'aide
+ * Point d'entrée pour accéder à toute l'aide et la documentation
+ */
+export default function AidePage() {
+ const categories = [
+ {
+ title: "Démarrage rapide",
+ description: "Premiers pas avec BTPXpress",
+ icon: Lightbulb,
+ color: "bg-blue-100 text-blue-600",
+ articles: 12,
+ },
+ {
+ title: "Gestion des chantiers",
+ description: "Créer et gérer vos chantiers",
+ icon: FileText,
+ color: "bg-green-100 text-green-600",
+ articles: 25,
+ },
+ {
+ title: "Facturation",
+ description: "Devis, factures et paiements",
+ icon: BookOpen,
+ color: "bg-purple-100 text-purple-600",
+ articles: 18,
+ },
+ {
+ title: "Équipes et planning",
+ description: "Gérer vos équipes et plannings",
+ icon: Video,
+ color: "bg-orange-100 text-orange-600",
+ articles: 15,
+ },
+ ];
+
+ const articlesPopulaires = [
+ "Comment créer un nouveau chantier ?",
+ "Gérer les devis et les factures",
+ "Planifier l'utilisation du matériel",
+ "Ajouter des membres à une équipe",
+ "Générer des rapports d'activité",
+ ];
+
+ return (
+
+ {/* En-tête */}
+
+
+ Comment pouvons-nous vous aider ?
+
+
+ Recherchez dans notre base de connaissances ou contactez notre support
+
+
+
+
+ {/* Accès rapide */}
+
+
+
+
+
+
+
+
+
Documentation
+
+ Guides complets et documentation technique
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tutoriels
+
+ Vidéos et guides pas-à-pas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Support
+
+ Contactez notre équipe d'assistance
+
+
+
+
+
+
+
+ {/* Catégories populaires */}
+
+
+ Catégories populaires
+
+
+ {categories.map((category) => (
+
+
+
+
+ {category.title}
+ {category.description}
+
+ {category.articles} articles
+
+
+ ))}
+
+
+
+ {/* Articles populaires */}
+
+
+ Articles populaires
+
+
+
+ {articlesPopulaires.map((article, index) => (
+
+ ))}
+
+
+
+
+ {/* Contact direct */}
+
+
+
+
+
+
+ Support téléphonique
+
+
+ Disponible du lundi au vendredi de 9h à 18h
+
+
+ +33 1 23 45 67 89
+
+
+
+
+
+
+
+
+
+
+
+
+ Support par email
+
+
+ Réponse sous 24h ouvrées
+
+
+ support@btpxpress.fr
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/aide/support/page.tsx b/app/(main)/aide/support/page.tsx
new file mode 100644
index 0000000..61c5f19
--- /dev/null
+++ b/app/(main)/aide/support/page.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import {
+ MessageCircle,
+ Phone,
+ Mail,
+ Clock,
+ CheckCircle,
+ AlertCircle,
+ Send,
+ ArrowLeft,
+ Headphones,
+} from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+
+/**
+ * Page de support technique
+ * Formulaire de contact et informations de support
+ */
+export default function SupportPage() {
+ const router = useRouter();
+ const [formData, setFormData] = useState({
+ nom: "",
+ email: "",
+ sujet: "",
+ priorite: "normale",
+ message: "",
+ });
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleChange = (
+ e: React.ChangeEvent
+ ) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ });
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+
+ // Simuler l'envoi
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ alert("Ticket de support créé avec succès ! Notre équipe vous contactera bientôt.");
+ router.push("/aide");
+ };
+
+ const ticketsRecents = [
+ {
+ id: "#2024-1234",
+ sujet: "Problème d'accès au dashboard",
+ status: "resolved",
+ date: "2025-10-28",
+ },
+ {
+ id: "#2024-1189",
+ sujet: "Question sur la facturation",
+ status: "in-progress",
+ date: "2025-10-25",
+ },
+ ];
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case "resolved":
+ return (
+
+
+ Résolu
+
+ );
+ case "in-progress":
+ return (
+
+
+ En cours
+
+ );
+ default:
+ return Nouveau ;
+ }
+ };
+
+ return (
+
+ {/* En-tête */}
+
+
+
+
+
+ Retour à l'aide
+
+
+
+
Support technique
+
+ Contactez notre équipe d'assistance
+
+
+
+
+
+
+ {/* Formulaire de contact */}
+
+
+ Créer un ticket de support
+
+
+
+ {/* Tickets récents */}
+ {ticketsRecents.length > 0 && (
+
+ Vos tickets récents
+
+ {ticketsRecents.map((ticket) => (
+
+
+
+
+ {ticket.id}
+
+ {getStatusBadge(ticket.status)}
+
+
{ticket.sujet}
+
+ Créé le{" "}
+ {new Date(ticket.date).toLocaleDateString("fr-FR", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ })}
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Sidebar - Informations de contact */}
+
+ {/* Horaires de support */}
+
+
+
+
+
+
+
Support disponible
+
Lundi - Vendredi
+
+
+
+
+
+ 9h00 - 18h00 (CET)
+
+
+ Temps de réponse moyen : 2-4 heures pendant les horaires d'ouverture
+
+
+
+
+ {/* Contact téléphonique */}
+
+
+
+
+
+ Support téléphonique
+
+
+ Pour une assistance immédiate
+
+
+ +33 1 23 45 67 89
+
+
+
+
+
+ {/* Contact email */}
+
+
+
+
+
+
+
Email support
+
Réponse sous 24h
+
+ support@btpxpress.fr
+
+
+
+
+
+ {/* Chat en direct */}
+
+
+
+
+
+
+
+ Chat en direct
+
+
+ Discutez avec un agent
+
+
+ Démarrer le chat
+
+
+
+
+
+ {/* Note importante */}
+
+
+
+
+
+ Pour les urgences critiques affectant votre production, appelez
+ notre ligne d'urgence 24/7 au{" "}
+ +33 1 98 76 54 32
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/aide/tutoriels/page.tsx b/app/(main)/aide/tutoriels/page.tsx
new file mode 100644
index 0000000..f58f40a
--- /dev/null
+++ b/app/(main)/aide/tutoriels/page.tsx
@@ -0,0 +1,271 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import {
+ Video,
+ Search,
+ Play,
+ Clock,
+ Star,
+ ThumbsUp,
+ Eye,
+ ArrowLeft,
+ Filter,
+} from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page des tutoriels
+ * Vidéos et guides pas-à-pas
+ */
+export default function TutorielsPage() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [selectedCategory, setSelectedCategory] = useState("all");
+
+ const tutoriels = [
+ {
+ id: "1",
+ title: "Créer votre premier chantier",
+ description:
+ "Apprenez à créer et configurer un nouveau chantier de A à Z",
+ duration: "8:45",
+ views: 1234,
+ likes: 89,
+ rating: 4.8,
+ category: "Démarrage",
+ level: "Débutant",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+1",
+ },
+ {
+ id: "2",
+ title: "Gérer les phases de chantier",
+ description: "Comment planifier et suivre les différentes phases d'un projet",
+ duration: "12:30",
+ views: 956,
+ likes: 72,
+ rating: 4.9,
+ category: "Chantiers",
+ level: "Intermédiaire",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+2",
+ },
+ {
+ id: "3",
+ title: "Créer et envoyer un devis",
+ description: "Créez des devis professionnels et envoyez-les à vos clients",
+ duration: "10:15",
+ views: 1567,
+ likes: 124,
+ rating: 4.7,
+ category: "Facturation",
+ level: "Débutant",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+3",
+ },
+ {
+ id: "4",
+ title: "Planifier les équipes efficacement",
+ description:
+ "Optimisez la gestion de vos équipes avec le planning intelligent",
+ duration: "15:20",
+ views: 823,
+ likes: 65,
+ rating: 4.6,
+ category: "Équipes",
+ level: "Avancé",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+4",
+ },
+ {
+ id: "5",
+ title: "Maintenance préventive du matériel",
+ description: "Mettez en place un programme de maintenance efficace",
+ duration: "11:45",
+ views: 645,
+ likes: 51,
+ rating: 4.5,
+ category: "Matériel",
+ level: "Intermédiaire",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+5",
+ },
+ {
+ id: "6",
+ title: "Générer des rapports personnalisés",
+ description: "Créez des rapports adaptés à vos besoins spécifiques",
+ duration: "9:30",
+ views: 734,
+ likes: 58,
+ rating: 4.8,
+ category: "Rapports",
+ level: "Avancé",
+ thumbnail: "https://via.placeholder.com/400x225?text=Tutoriel+6",
+ },
+ ];
+
+ const categories = ["all", "Démarrage", "Chantiers", "Facturation", "Équipes", "Matériel", "Rapports"];
+ const levels = ["all", "Débutant", "Intermédiaire", "Avancé"];
+
+ const filteredTutoriels = tutoriels.filter((tuto) => {
+ const matchesSearch =
+ tuto.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ tuto.description.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesCategory =
+ selectedCategory === "all" || tuto.category === selectedCategory;
+ return matchesSearch && matchesCategory;
+ });
+
+ const getLevelColor = (level: string) => {
+ switch (level) {
+ case "Débutant":
+ return "bg-green-100 text-green-800";
+ case "Intermédiaire":
+ return "bg-blue-100 text-blue-800";
+ case "Avancé":
+ return "bg-purple-100 text-purple-800";
+ default:
+ return "bg-gray-100 text-gray-800";
+ }
+ };
+
+ return (
+
+ {/* En-tête */}
+
+
+
+
+
+ Retour à l'aide
+
+
+
+
Tutoriels vidéo
+
+ {filteredTutoriels.length} tutoriel{filteredTutoriels.length > 1 ? "s" : ""}{" "}
+ disponible{filteredTutoriels.length > 1 ? "s" : ""}
+
+
+
+
+
+ {/* Barre de recherche et filtres */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ {categories.map((category) => (
+ setSelectedCategory(category)}
+ >
+ {category === "all" ? "Toutes les catégories" : category}
+
+ ))}
+
+
+
+ {/* Grid de tutoriels */}
+
+ {filteredTutoriels.map((tutoriel) => (
+
+ {/* Thumbnail */}
+
+
+
+
+
+ {tutoriel.duration}
+
+
+
+ {/* Contenu */}
+
+
+
+ {tutoriel.level}
+
+ {tutoriel.category}
+
+
+
{tutoriel.title}
+
+ {tutoriel.description}
+
+
+ {/* Stats */}
+
+
+
+
+ {tutoriel.views.toLocaleString()}
+
+
+
+ {tutoriel.likes}
+
+
+
+
+ {tutoriel.rating}
+
+
+
+
+ ))}
+
+
+ {filteredTutoriels.length === 0 && (
+
+
+
+
Aucun tutoriel trouvé
+
+
+ )}
+
+ {/* Section suggestions */}
+
+
+
+
+
+
+
+ Vous ne trouvez pas ce que vous cherchez ?
+
+
+ Suggérez-nous un nouveau tutoriel ! Nous sommes toujours à l'écoute de
+ vos besoins pour améliorer notre contenu pédagogique.
+
+
+ Suggérer un tutoriel
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx
index 3031798..74358f6 100644
--- a/app/(main)/dashboard/page.tsx
+++ b/app/(main)/dashboard/page.tsx
@@ -39,11 +39,14 @@ const Dashboard = () => {
const [authProcessed, setAuthProcessed] = useState(false);
const [authError, setAuthError] = useState(null);
const [authInProgress, setAuthInProgress] = useState(false);
+ const [isHydrated, setIsHydrated] = useState(false);
// Flag global pour éviter les appels multiples (React 18 StrictMode)
const authProcessingRef = useRef(false);
// Mémoriser le code traité pour éviter les retraitements
const processedCodeRef = useRef(null);
+ // Flag pour éviter les redirections multiples
+ const redirectingRef = useRef(false);
const currentCode = searchParams.get('code');
const currentState = searchParams.get('state');
@@ -56,21 +59,31 @@ const Dashboard = () => {
authInProgress: authInProgress
});
+ // Gérer l'hydratation pour éviter les erreurs SSR/CSR
+ useEffect(() => {
+ setIsHydrated(true);
+ }, []);
+
// Réinitialiser authProcessed si on a un nouveau code d'autorisation
useEffect(() => {
+ if (!isHydrated) return; // Attendre l'hydratation
+
if (currentCode && authProcessed && !authInProgress && processedCodeRef.current !== currentCode) {
console.log('🔄 Dashboard: Nouveau code détecté, réinitialisation authProcessed');
setAuthProcessed(false);
processedCodeRef.current = null;
}
- }, [currentCode, authProcessed, authInProgress]);
+ }, [currentCode, authProcessed, authInProgress, isHydrated]);
// Fonction pour nettoyer l'URL des paramètres d'authentification
const cleanAuthParams = useCallback(() => {
+ if (typeof window === 'undefined') return; // Protection SSR
+
const url = new URL(window.location.href);
if (url.searchParams.has('code') || url.searchParams.has('state')) {
url.search = '';
window.history.replaceState({}, '', url.toString());
+// Nettoyer sessionStorage après succès if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); }
}
}, []);
@@ -183,12 +196,30 @@ const Dashboard = () => {
// Traiter l'authentification Keycloak si nécessaire
useEffect(() => {
+ // Attendre l'hydratation avant de traiter l'authentification
+ if (!isHydrated) {
+ return;
+ }
+
+ // Mode développement : ignorer l'authentification Keycloak
+ if (process.env.NEXT_PUBLIC_DEV_MODE === 'true' || process.env.NEXT_PUBLIC_SKIP_AUTH === 'true') {
+ console.log('🔧 Dashboard: Mode développement détecté, authentification ignorée');
+ setAuthProcessed(true);
+ return;
+ }
+
// Si l'authentification est déjà terminée, ne rien faire
if (authProcessed) {
return;
}
+ // Si on est en train de rediriger, ne rien faire
+ if (redirectingRef.current) {
+ return;
+ }
+
const processAuth = async () => {
+ try {
// Protection absolue contre les boucles
if (authInProgress || authProcessingRef.current) {
console.log('🛑 Dashboard: Processus d\'authentification déjà en cours, arrêt');
@@ -214,101 +245,53 @@ const Dashboard = () => {
try {
console.log('🔐 Traitement du code d\'autorisation Keycloak...', { code: code.substring(0, 20) + '...', state });
+ // Marquer IMMÉDIATEMENT dans sessionStorage pour empêcher double traitement
+ if (typeof window !== 'undefined') {
+ sessionStorage.setItem('oauth_code_processed', code);
+ }
+
// Marquer l'authentification comme en cours pour éviter les appels multiples
authProcessingRef.current = true;
processedCodeRef.current = code;
setAuthInProgress(true);
- console.log('📡 Appel API /api/auth/token...');
+ console.log('📡 Appel de /api/auth/token...');
+ // Utiliser fetch au lieu d'un formulaire pour éviter la boucle de redirection
const response = await fetch('/api/auth/token', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ code, state }),
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ code, state: state || '' })
});
- console.log('📡 Réponse API /api/auth/token:', {
- status: response.status,
- ok: response.ok,
- statusText: response.statusText
- });
+ if (response.ok) {
+ console.log('✅ Authentification réussie, tokens stockés dans les cookies');
- if (!response.ok) {
- const errorText = await response.text();
- console.error('❌ Échec de l\'échange de token:', {
- status: response.status,
- error: errorText
- });
+ // Nettoyer l'URL en enlevant les paramètres OAuth
+ window.history.replaceState({}, '', '/dashboard');
+// Nettoyer sessionStorage après succès if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); }
- // Gestion spécifique des codes expirés, invalides ou code verifier manquant
- if (errorText.includes('invalid_grant') ||
- errorText.includes('Code not valid') ||
- errorText.includes('Code verifier manquant')) {
-
- console.log('🔄 Problème d\'authentification détecté:', errorText);
-
- // Vérifier si on n'est pas déjà en boucle
- const retryCount = parseInt(localStorage.getItem('auth_retry_count') || '0');
- if (retryCount >= 3) {
- console.error('🚫 Trop de tentatives d\'authentification. Arrêt pour éviter la boucle infinie.');
- localStorage.removeItem('auth_retry_count');
- setAuthError('Erreur d\'authentification persistante. Veuillez rafraîchir la page.');
- return;
- }
-
- localStorage.setItem('auth_retry_count', (retryCount + 1).toString());
- console.log(`🔄 Tentative ${retryCount + 1}/3 - Redirection vers nouvelle authentification...`);
-
- // Nettoyer l'URL et rediriger vers une nouvelle authentification
- const url = new URL(window.location.href);
- url.search = '';
- window.history.replaceState({}, '', url.toString());
-
- // Attendre un peu pour éviter les boucles infinies
- setTimeout(() => {
- window.location.href = '/api/auth/login';
- }, 2000);
- return;
- }
-
- throw new Error(`Échec de l'échange de token: ${errorText}`);
+ // Marquer l'authentification comme terminée
+ setAuthProcessed(true);
+ setAuthInProgress(false);
+ authProcessingRef.current = false;
+ } else {
+ console.error("❌ Erreur lors de l'authentification");
+ const errorData = await response.json();
+ setAuthError(`Erreur lors de l'authentification: ${errorData.error || 'Erreur inconnue'}`);
+ setAuthProcessed(true);
+ setAuthInProgress(false);
+ authProcessingRef.current = false;
}
- const result = await response.json();
- console.log('✅ Authentification réussie, tokens stockés dans des cookies HttpOnly');
-
- // Réinitialiser le compteur de tentatives d'authentification
- localStorage.removeItem('auth_retry_count');
-
- setAuthProcessed(true);
- setAuthInProgress(false);
- authProcessingRef.current = false;
-
- // Vérifier s'il y a une URL de retour sauvegardée
- const returnUrl = localStorage.getItem('returnUrl');
- if (returnUrl && returnUrl !== '/dashboard') {
- console.log('🔄 Dashboard: Redirection vers la page d\'origine:', returnUrl);
- localStorage.removeItem('returnUrl');
- window.location.href = returnUrl;
- return;
- }
-
- // Nettoyer l'URL et recharger pour que le middleware vérifie les cookies
- console.log('🧹 Dashboard: Nettoyage de l\'URL et rechargement...');
- window.location.href = '/dashboard';
-
- // Arrêter définitivement le processus d'authentification
- return;
-
} catch (error) {
console.error('❌ Erreur lors du traitement de l\'authentification:', error);
// ARRÊTER LA BOUCLE : Ne pas rediriger automatiquement, juste marquer comme traité
console.log('🛑 Dashboard: Erreur d\'authentification, arrêt du processus pour éviter la boucle');
- setAuthError(`Erreur lors de l'authentification: ${error.message}`);
+ const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification';
+ setAuthError(`Erreur lors de l'authentification: ${errorMessage}`);
setAuthProcessed(true);
setAuthInProgress(false);
authProcessingRef.current = false;
@@ -318,10 +301,18 @@ const Dashboard = () => {
setAuthInProgress(false);
authProcessingRef.current = false;
}
+ } catch (error) {
+ console.error('❌ Erreur générale lors du traitement de l\'authentification:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification';
+ setAuthError(`Erreur lors de l'authentification: ${errorMessage}`);
+ setAuthProcessed(true);
+ setAuthInProgress(false);
+ authProcessingRef.current = false;
+ }
};
processAuth();
- }, [currentCode, currentState, authProcessed, authInProgress, refresh]);
+ }, [currentCode, currentState, authProcessed, authInProgress, isHydrated]);
@@ -330,6 +321,7 @@ const Dashboard = () => {
const actions: ActionButtonType[] = ['VIEW', 'PHASES', 'PLANNING', 'STATS', 'MENU'];
const handleActionClick = (action: ActionButtonType | string, chantier: ChantierActif) => {
+ try {
switch (action) {
case 'VIEW':
handleQuickView(chantier);
@@ -366,7 +358,17 @@ const Dashboard = () => {
chantierActions.handleCreateAmendment(chantier);
break;
default:
+ console.warn('Action non reconnue:', action);
break;
+ }
+ } catch (error) {
+ console.error('Erreur lors de l\'exécution de l\'action:', action, error);
+ toast.current?.show({
+ severity: 'error',
+ summary: 'Erreur',
+ detail: 'Une erreur est survenue lors de l\'exécution de l\'action',
+ life: 3000
+ });
}
};
@@ -385,6 +387,23 @@ const Dashboard = () => {
+ // Attendre l'hydratation pour éviter les erreurs SSR/CSR
+ if (!isHydrated) {
+ return (
+
+
+
+
+
+
Chargement...
+
Initialisation de l'application
+
+
+
+
+ );
+ }
+
// Afficher le chargement pendant le traitement de l'authentification
if (!authProcessed) {
return (
diff --git a/app/(main)/messages/archives/page.tsx b/app/(main)/messages/archives/page.tsx
new file mode 100644
index 0000000..445e743
--- /dev/null
+++ b/app/(main)/messages/archives/page.tsx
@@ -0,0 +1,184 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Archive, Search, Mail, Send, RotateCcw, Trash2, MoreVertical } from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page des messages archivés
+ * Affiche tous les messages archivés avec possibilité de restauration
+ */
+export default function MessagesArchivesPage() {
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const messagesArchives = [
+ {
+ id: "1",
+ from: "Ancien Client",
+ fromEmail: "ancien@client.fr",
+ subject: "Projet terminé - Retour d'expérience",
+ preview: "Nous tenions à vous remercier pour le travail effectué...",
+ date: "2025-09-15T10:00:00",
+ archivedDate: "2025-10-01T14:30:00",
+ },
+ {
+ id: "2",
+ from: "Fournisseur Matériaux",
+ fromEmail: "contact@fournisseur.com",
+ subject: "Catalogue 2024 - Nouveaux produits",
+ preview: "Découvrez notre nouveau catalogue avec nos dernières innovations...",
+ date: "2025-08-20T09:15:00",
+ archivedDate: "2025-09-10T11:00:00",
+ },
+ ];
+
+ const filteredMessages = messagesArchives.filter(
+ (message) =>
+ message.from.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ message.subject.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString("fr-FR", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ });
+ };
+
+ const handleRestore = (id: string) => {
+ alert(`Message ${id} restauré dans la boîte de réception`);
+ };
+
+ const handleDelete = (id: string) => {
+ if (confirm("Voulez-vous vraiment supprimer définitivement ce message ?")) {
+ alert(`Message ${id} supprimé définitivement`);
+ }
+ };
+
+ return (
+
+
+
+
Messages archivés
+
+ {messagesArchives.length} message{messagesArchives.length > 1 ? "s" : ""}{" "}
+ archivé{messagesArchives.length > 1 ? "s" : ""}
+
+
+
+
+
+ Nouveau message
+
+
+
+
+
+
+
+
+ Reçus
+
+
+
+
+
+ Envoyés
+
+
+
+
+
+ Archives
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ {filteredMessages.length > 0 ? (
+ filteredMessages.map((message) => (
+
+
+
+
+
+
{message.from}
+
+ {message.subject}
+
+
+ {message.preview}
+
+
+ Archivé le {formatDate(message.archivedDate)}
+
+
+
+ handleRestore(message.id)}
+ >
+
+ Restaurer
+
+ handleDelete(message.id)}
+ className="text-red-600 hover:text-red-700"
+ >
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
Aucun message archivé
+
+ )}
+
+
+
+
+
+
+
+
À propos des archives
+
+ Les messages archivés sont conservés pendant 90 jours avant d'être supprimés
+ automatiquement. Vous pouvez restaurer un message archivé à tout moment
+ pour le retrouver dans votre boîte de réception.
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/messages/envoyes/page.tsx b/app/(main)/messages/envoyes/page.tsx
new file mode 100644
index 0000000..54a8272
--- /dev/null
+++ b/app/(main)/messages/envoyes/page.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Send, Search, Mail, Archive, Trash2, MoreVertical } from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page des messages envoyés
+ * Affiche l'historique de tous les messages envoyés
+ */
+export default function MessagesEnvoyesPage() {
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const messagesenvoyes = [
+ {
+ id: "1",
+ to: "Jean Dupont",
+ toEmail: "jean.dupont@btpxpress.fr",
+ subject: "RE: Demande de devis pour chantier Lyon",
+ preview: "Bonjour Jean, voici le devis demandé pour le projet...",
+ date: "2025-10-30T11:00:00",
+ status: "delivered",
+ },
+ {
+ id: "2",
+ to: "Marie Martin",
+ toEmail: "marie.martin@client.fr",
+ subject: "Confirmation planning",
+ preview: "Bonjour Marie, je confirme la réception de votre validation...",
+ date: "2025-10-30T09:30:00",
+ status: "read",
+ },
+ {
+ id: "3",
+ to: "Pierre Leblanc",
+ toEmail: "p.leblanc@entreprise.com",
+ subject: "RE: Question sur facture #2024-103",
+ preview: "Bonjour Pierre, concernant votre question sur la facture...",
+ date: "2025-10-29T17:00:00",
+ status: "delivered",
+ },
+ ];
+
+ const filteredMessages = messagesenvoyes.filter(
+ (message) =>
+ message.to.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ message.subject.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString("fr-FR", {
+ day: "2-digit",
+ month: "short",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case "read":
+ return Lu ;
+ case "delivered":
+ return Envoyé ;
+ default:
+ return En cours ;
+ }
+ };
+
+ return (
+
+
+
+
Messages envoyés
+
+ {messagesenvoyes.length} message{messagesenvoyes.length > 1 ? "s" : ""}{" "}
+ envoyé{messagesenvoyes.length > 1 ? "s" : ""}
+
+
+
+
+
+ Nouveau message
+
+
+
+
+
+
+
+
+ Reçus
+
+
+
+
+
+ Envoyés
+
+
+
+
+
+ Archives
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ {filteredMessages.length > 0 ? (
+ filteredMessages.map((message) => (
+
+
+
+
+
+
+
À: {message.to}
+ {getStatusBadge(message.status)}
+
+
+ {message.subject}
+
+
+ {message.preview}
+
+
+
+
+ {formatDate(message.date)}
+
+
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
Aucun message envoyé
+
+ )}
+
+
+
+ );
+}
diff --git a/app/(main)/messages/nouveau/page.tsx b/app/(main)/messages/nouveau/page.tsx
new file mode 100644
index 0000000..5b31518
--- /dev/null
+++ b/app/(main)/messages/nouveau/page.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Send, Paperclip, X, ArrowLeft } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+
+/**
+ * Page de création de nouveau message
+ * Permet de composer et envoyer un nouveau message
+ */
+export default function NouveauMessagePage() {
+ const router = useRouter();
+ const [destinataire, setDestinataire] = useState("");
+ const [sujet, setSujet] = useState("");
+ const [message, setMessage] = useState("");
+ const [fichiers, setFichiers] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files) {
+ setFichiers([...fichiers, ...Array.from(e.target.files)]);
+ }
+ };
+
+ const removeFile = (index: number) => {
+ setFichiers(fichiers.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+
+ // Simuler l'envoi
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ alert("Message envoyé avec succès !");
+ router.push("/messages");
+ };
+
+ return (
+
+
+
+
+
+
+ Retour
+
+
+
+
Nouveau message
+
Composer un nouveau message
+
+
+
+
+
+
+
+ Destinataire *
+ setDestinataire(e.target.value)}
+ required
+ className="mt-2"
+ />
+
+
+
+ Sujet *
+ setSujet(e.target.value)}
+ required
+ className="mt-2"
+ />
+
+
+
+ Message *
+ setMessage(e.target.value)}
+ required
+ rows={12}
+ className="mt-2"
+ />
+
+
+
+
Pièces jointes
+
+ {fichiers.map((file, index) => (
+
+
+
{file.name}
+
removeFile(index)}
+ >
+
+
+
+ ))}
+
+
document.getElementById("file-upload")?.click()}
+ >
+
+ Ajouter une pièce jointe
+
+
+
+
+
+
+
+ {isSubmitting ? "Envoi en cours..." : "Envoyer"}
+
+
+
+ Annuler
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/messages/page.tsx b/app/(main)/messages/page.tsx
new file mode 100644
index 0000000..381b0c5
--- /dev/null
+++ b/app/(main)/messages/page.tsx
@@ -0,0 +1,374 @@
+"use client";
+
+import React, { useState } from "react";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import {
+ Mail,
+ Search,
+ Send,
+ Archive,
+ Star,
+ Trash2,
+ Reply,
+ Forward,
+ MoreVertical,
+ Paperclip,
+ Clock,
+} from "lucide-react";
+import Link from "next/link";
+
+/**
+ * Page principale de la messagerie - Boîte de réception
+ * Affiche tous les messages reçus avec filtrage et recherche
+ */
+export default function MessagesPage() {
+ const [selectedMessages, setSelectedMessages] = useState([]);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [filterStatus, setFilterStatus] = useState<"all" | "unread" | "starred">(
+ "all"
+ );
+
+ // Messages mockés pour démonstration
+ const messages = [
+ {
+ id: "1",
+ from: "Jean Dupont",
+ fromEmail: "jean.dupont@btpxpress.fr",
+ subject: "Demande de devis pour chantier Lyon",
+ preview:
+ "Bonjour, je souhaiterais obtenir un devis pour la construction d'un immeuble...",
+ date: "2025-10-30T10:30:00",
+ isRead: false,
+ isStarred: true,
+ hasAttachment: true,
+ },
+ {
+ id: "2",
+ from: "Marie Martin",
+ fromEmail: "marie.martin@client.fr",
+ subject: "Validation planning chantier",
+ preview: "Le planning que vous avez envoyé me convient parfaitement...",
+ date: "2025-10-30T09:15:00",
+ isRead: true,
+ isStarred: false,
+ hasAttachment: false,
+ },
+ {
+ id: "3",
+ from: "Pierre Leblanc",
+ fromEmail: "p.leblanc@entreprise.com",
+ subject: "Question sur facture #2024-103",
+ preview: "J'ai une question concernant la facture que j'ai reçue...",
+ date: "2025-10-29T16:45:00",
+ isRead: false,
+ isStarred: false,
+ hasAttachment: true,
+ },
+ {
+ id: "4",
+ from: "Sophie Bernard",
+ fromEmail: "sophie.b@construction.fr",
+ subject: "Proposition de collaboration",
+ preview: "Nous sommes intéressés par une collaboration sur plusieurs projets...",
+ date: "2025-10-29T14:20:00",
+ isRead: true,
+ isStarred: true,
+ hasAttachment: false,
+ },
+ ];
+
+ const filteredMessages = messages.filter((message) => {
+ const matchesSearch =
+ message.from.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ message.subject.toLowerCase().includes(searchTerm.toLowerCase());
+
+ const matchesFilter =
+ filterStatus === "all" ||
+ (filterStatus === "unread" && !message.isRead) ||
+ (filterStatus === "starred" && message.isStarred);
+
+ return matchesSearch && matchesFilter;
+ });
+
+ const toggleSelectMessage = (id: string) => {
+ setSelectedMessages((prev) =>
+ prev.includes(id) ? prev.filter((msgId) => msgId !== id) : [...prev, id]
+ );
+ };
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
+
+ if (diffInHours < 24) {
+ return date.toLocaleTimeString("fr-FR", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ } else if (diffInHours < 48) {
+ return "Hier";
+ } else {
+ return date.toLocaleDateString("fr-FR", {
+ day: "2-digit",
+ month: "short",
+ });
+ }
+ };
+
+ const unreadCount = messages.filter((m) => !m.isRead).length;
+
+ return (
+
+ {/* En-tête */}
+
+
+
Messagerie
+
+ {unreadCount} message{unreadCount > 1 ? "s" : ""} non lu
+ {unreadCount > 1 ? "s" : ""}
+
+
+
+
+
+ Nouveau message
+
+
+
+
+ {/* Navigation */}
+
+
+
+
+ Reçus
+
+
+
+
+
+ Envoyés
+
+
+
+
+
+ Archives
+
+
+
+
+ {/* Barre de recherche et filtres */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ setFilterStatus("all")}
+ >
+ Tous
+
+ setFilterStatus("unread")}
+ >
+ Non lus ({unreadCount})
+
+ setFilterStatus("starred")}
+ >
+
+ Favoris
+
+
+
+
+
+ {/* Actions groupées */}
+ {selectedMessages.length > 0 && (
+
+
+
+ {selectedMessages.length} message{selectedMessages.length > 1 ? "s" : ""}{" "}
+ sélectionné{selectedMessages.length > 1 ? "s" : ""}
+
+
setSelectedMessages([])}
+ >
+ Tout désélectionner
+
+
+
+
+ Archiver
+
+
+
+ Supprimer
+
+
+
+ )}
+
+ {/* Liste des messages */}
+
+
+ {filteredMessages.length > 0 ? (
+ filteredMessages.map((message) => (
+
toggleSelectMessage(message.id)}
+ >
+
+ {/* Checkbox */}
+
toggleSelectMessage(message.id)}
+ className="mt-1"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+ {/* Star */}
+
{
+ e.stopPropagation();
+ }}
+ >
+
+
+
+ {/* Contenu du message */}
+
+
+
+
+
+ {message.from}
+
+ {message.hasAttachment && (
+
+ )}
+
+
+ {message.subject}
+
+
+ {message.preview}
+
+
+
+
+ {formatDate(message.date)}
+
+ e.stopPropagation()}
+ className="hover:bg-gray-200 rounded p-1"
+ >
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
Aucun message trouvé
+
+ )}
+
+
+
+ {/* Statistiques */}
+
+
+
+
+
+
+
+
{messages.length}
+
Messages reçus
+
+
+
+
+
+
+
+
+
+
{unreadCount}
+
Non lus
+
+
+
+
+
+
+
+
+
+
+ {messages.filter((m) => m.isStarred).length}
+
+
Favoris
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx
index f5f692c..03e023a 100644
--- a/app/(main)/page.tsx
+++ b/app/(main)/page.tsx
@@ -140,13 +140,13 @@ const LandingPage: Page = () => {
-
+
Connexion
-
+
Inscription
@@ -182,7 +182,7 @@ const LandingPage: Page = () => {
Bienvenue sur BTP Xpress
Votre plateforme de gestion BTP moderne
-
+
Commencer
diff --git a/app/(main)/stock/fournisseurs/page.tsx b/app/(main)/stock/fournisseurs/page.tsx
index f31eb87..b766de9 100644
--- a/app/(main)/stock/fournisseurs/page.tsx
+++ b/app/(main)/stock/fournisseurs/page.tsx
@@ -1,23 +1,543 @@
'use client';
export const dynamic = 'force-dynamic';
-
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
+import { Button } from 'primereact/button';
+import { DataTable } from 'primereact/datatable';
+import { Column } from 'primereact/column';
+import { Dialog } from 'primereact/dialog';
+import { InputText } from 'primereact/inputtext';
+import { InputTextarea } from 'primereact/inputtextarea';
+import { Dropdown } from 'primereact/dropdown';
+import { Toast } from 'primereact/toast';
+import { ConfirmDialog } from 'primereact/confirmdialog';
+import { Tag } from 'primereact/tag';
+import { Toolbar } from 'primereact/toolbar';
+import { useRef } from 'react';
+import { fournisseurService } from '@/services/fournisseurService';
-// TODO: Fix type mapping between Fournisseur and FournisseurFormData
-// This page is temporarily disabled due to type incompatibilities
+interface Fournisseur {
+ id: string;
+ nom: string;
+ contact: string;
+ telephone: string;
+ email: string;
+ adresse: string;
+ ville: string;
+ codePostal: string;
+ pays: string;
+ siret?: string;
+ tva?: string;
+ conditionsPaiement: string;
+ delaiLivraison: number;
+ note?: string;
+ actif: boolean;
+ dateCreation: string;
+ dateModification: string;
+}
+
+interface FournisseurFormData {
+ nom: string;
+ contact: string;
+ telephone: string;
+ email: string;
+ adresse: string;
+ ville: string;
+ codePostal: string;
+ pays: string;
+ siret: string;
+ tva: string;
+ conditionsPaiement: string;
+ delaiLivraison: number;
+ note: string;
+}
const FournisseursPage = () => {
+ const [fournisseurs, setFournisseurs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showDialog, setShowDialog] = useState(false);
+ const [editingFournisseur, setEditingFournisseur] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [globalFilter, setGlobalFilter] = useState('');
+ const [selectedFournisseurs, setSelectedFournisseurs] = useState([]);
+ const toast = useRef(null);
+
+ const [formData, setFormData] = useState({
+ nom: '',
+ contact: '',
+ telephone: '',
+ email: '',
+ adresse: '',
+ ville: '',
+ codePostal: '',
+ pays: 'France',
+ siret: '',
+ tva: '',
+ conditionsPaiement: '30 jours',
+ delaiLivraison: 7,
+ note: ''
+ });
+
+ const conditionsPaiementOptions = [
+ { label: 'Comptant', value: 'Comptant' },
+ { label: '30 jours', value: '30 jours' },
+ { label: '45 jours', value: '45 jours' },
+ { label: '60 jours', value: '60 jours' },
+ { label: '90 jours', value: '90 jours' }
+ ];
+
+ const paysOptions = [
+ { label: 'France', value: 'France' },
+ { label: 'Belgique', value: 'Belgique' },
+ { label: 'Suisse', value: 'Suisse' },
+ { label: 'Allemagne', value: 'Allemagne' },
+ { label: 'Espagne', value: 'Espagne' },
+ { label: 'Italie', value: 'Italie' }
+ ];
+
+ useEffect(() => {
+ loadFournisseurs();
+ }, []);
+
+ const loadFournisseurs = async () => {
+ try {
+ setLoading(true);
+ const data = await fournisseurService.getAllFournisseurs();
+ setFournisseurs(data);
+ } catch (error) {
+ console.error('Erreur lors du chargement des fournisseurs:', error);
+ toast.current?.show({
+ severity: 'error',
+ summary: 'Erreur',
+ detail: 'Impossible de charger les fournisseurs'
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ setIsSubmitting(true);
+
+ // Validation des champs obligatoires
+ if (!formData.nom || !formData.contact || !formData.email) {
+ toast.current?.show({
+ severity: 'warn',
+ summary: 'Validation',
+ detail: 'Veuillez remplir tous les champs obligatoires'
+ });
+ return;
+ }
+
+ // Conversion des données du formulaire vers le format API
+ const apiData = {
+ nom: formData.nom,
+ contact: formData.contact,
+ telephone: formData.telephone,
+ email: formData.email,
+ adresse: formData.adresse,
+ ville: formData.ville,
+ codePostal: formData.codePostal,
+ pays: formData.pays,
+ siret: formData.siret,
+ tva: formData.tva,
+ conditionsPaiement: formData.conditionsPaiement,
+ delaiLivraison: formData.delaiLivraison,
+ note: formData.note
+ };
+
+ if (editingFournisseur) {
+ await fournisseurService.updateFournisseur(editingFournisseur.id, apiData);
+ toast.current?.show({
+ severity: 'success',
+ summary: 'Succès',
+ detail: 'Fournisseur mis à jour avec succès'
+ });
+ } else {
+ await fournisseurService.createFournisseur(apiData);
+ toast.current?.show({
+ severity: 'success',
+ summary: 'Succès',
+ detail: 'Fournisseur créé avec succès'
+ });
+ }
+
+ await loadFournisseurs();
+ setShowDialog(false);
+ resetForm();
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde:', error);
+ toast.current?.show({
+ severity: 'error',
+ summary: 'Erreur',
+ detail: 'Erreur lors de la sauvegarde du fournisseur'
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({
+ nom: '',
+ contact: '',
+ telephone: '',
+ email: '',
+ adresse: '',
+ ville: '',
+ codePostal: '',
+ pays: 'France',
+ siret: '',
+ tva: '',
+ conditionsPaiement: '30 jours',
+ delaiLivraison: 7,
+ note: ''
+ });
+ setEditingFournisseur(null);
+ };
+
+ const openDialog = (fournisseur?: Fournisseur) => {
+ if (fournisseur) {
+ setEditingFournisseur(fournisseur);
+ setFormData({
+ nom: fournisseur.nom,
+ contact: fournisseur.contact,
+ telephone: fournisseur.telephone,
+ email: fournisseur.email,
+ adresse: fournisseur.adresse,
+ ville: fournisseur.ville,
+ codePostal: fournisseur.codePostal,
+ pays: fournisseur.pays,
+ siret: fournisseur.siret || '',
+ tva: fournisseur.tva || '',
+ conditionsPaiement: fournisseur.conditionsPaiement,
+ delaiLivraison: fournisseur.delaiLivraison,
+ note: fournisseur.note || ''
+ });
+ } else {
+ resetForm();
+ }
+ setShowDialog(true);
+ };
+
+ const handleDelete = async (fournisseur: Fournisseur) => {
+ try {
+ await fournisseurService.deleteFournisseur(fournisseur.id);
+ toast.current?.show({
+ severity: 'success',
+ summary: 'Succès',
+ detail: 'Fournisseur supprimé avec succès'
+ });
+ await loadFournisseurs();
+ } catch (error) {
+ console.error('Erreur lors de la suppression:', error);
+ toast.current?.show({
+ severity: 'error',
+ summary: 'Erreur',
+ detail: 'Erreur lors de la suppression du fournisseur'
+ });
+ }
+ };
+
+ const handleDeleteSelected = async () => {
+ try {
+ for (const fournisseur of selectedFournisseurs) {
+ await fournisseurService.deleteFournisseur(fournisseur.id);
+ }
+ toast.current?.show({
+ severity: 'success',
+ summary: 'Succès',
+ detail: `${selectedFournisseurs.length} fournisseur(s) supprimé(s) avec succès`
+ });
+ setSelectedFournisseurs([]);
+ await loadFournisseurs();
+ } catch (error) {
+ console.error('Erreur lors de la suppression:', error);
+ toast.current?.show({
+ severity: 'error',
+ summary: 'Erreur',
+ detail: 'Erreur lors de la suppression des fournisseurs'
+ });
+ }
+ };
+
+ const statusBodyTemplate = (fournisseur: Fournisseur) => {
return (
-
-
-
- Page temporairement indisponible - En cours de correction des types TypeScript
-
-
-
+
);
+ };
+
+ const actionBodyTemplate = (fournisseur: Fournisseur) => {
+ return (
+
+ openDialog(fournisseur)}
+ tooltip="Modifier"
+ />
+ handleDelete(fournisseur)}
+ tooltip="Supprimer"
+ />
+
+ );
+ };
+
+ const leftToolbarTemplate = () => {
+ return (
+
+ openDialog()}
+ />
+ {selectedFournisseurs.length > 0 && (
+
+ )}
+
+ );
+ };
+
+ const rightToolbarTemplate = () => {
+ return (
+
+
+
+ setGlobalFilter(e.target.value)}
+ placeholder="Rechercher..."
+ />
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ setSelectedFournisseurs(e.value)}
+ dataKey="id"
+ emptyMessage="Aucun fournisseur trouvé"
+ className="p-datatable-sm"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
setShowDialog(false)}
+ >
+
+
+
+
+ Nom *
+ setFormData({ ...formData, nom: e.target.value })}
+ required
+ />
+
+
+
+
+ Contact *
+ setFormData({ ...formData, contact: e.target.value })}
+ required
+ />
+
+
+
+
+ Téléphone
+ setFormData({ ...formData, telephone: e.target.value })}
+ />
+
+
+
+
+ Email *
+ setFormData({ ...formData, email: e.target.value })}
+ required
+ />
+
+
+
+
+ Adresse
+ setFormData({ ...formData, adresse: e.target.value })}
+ rows={2}
+ />
+
+
+
+
+ Ville
+ setFormData({ ...formData, ville: e.target.value })}
+ />
+
+
+
+
+ Code postal
+ setFormData({ ...formData, codePostal: e.target.value })}
+ />
+
+
+
+
+ Pays
+ setFormData({ ...formData, pays: e.value })}
+ />
+
+
+
+
+ SIRET
+ setFormData({ ...formData, siret: e.target.value })}
+ />
+
+
+
+
+ N° TVA
+ setFormData({ ...formData, tva: e.target.value })}
+ />
+
+
+
+
+ Conditions de paiement
+ setFormData({ ...formData, conditionsPaiement: e.value })}
+ />
+
+
+
+
+ Délai de livraison (jours)
+ setFormData({ ...formData, delaiLivraison: parseInt(e.target.value) || 0 })}
+ />
+
+
+
+
+ Note
+ setFormData({ ...formData, note: e.target.value })}
+ rows={3}
+ />
+
+
+
+
+
+ setShowDialog(false)}
+ />
+
+
+
+
+
+ );
};
-export default FournisseursPage;
+export default FournisseursPage;
\ No newline at end of file
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
deleted file mode 100644
index aed2c85..0000000
--- a/app/api/auth/login/route.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { createHash, randomBytes } from 'crypto';
-
-/**
- * Génère un code verifier et challenge pour PKCE
- */
-function generatePKCE() {
- // Générer un code verifier aléatoire
- const codeVerifier = randomBytes(32).toString('base64url');
-
- // Générer le code challenge (SHA256 du verifier)
- const codeChallenge = createHash('sha256')
- .update(codeVerifier)
- .digest('base64url');
-
- return { codeVerifier, codeChallenge };
-}
-
-/**
- * API Route pour déclencher l'authentification Keycloak
- * Redirige directement vers Keycloak sans page intermédiaire
- */
-export async function GET(request: NextRequest) {
- try {
- // Configuration Keycloak depuis les variables d'environnement
- const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev';
- const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress';
- const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend';
-
- // URL de redirection après authentification (vers la page dashboard)
- // Utiliser une URL fixe pour éviter les problèmes avec request.nextUrl.origin
- const baseUrl = process.env.NODE_ENV === 'production'
- ? 'https://btpxpress.lions.dev'
- : 'http://localhost:3000';
- const redirectUri = encodeURIComponent(`${baseUrl}/dashboard`);
-
- // Toujours utiliser l'URI de redirection simple vers /dashboard
- // Ne pas utiliser le paramètre redirect qui peut contenir des codes d'autorisation obsolètes
- const finalRedirectUri = redirectUri;
-
- // Générer les paramètres PKCE
- const { codeVerifier, codeChallenge } = generatePKCE();
-
- // Construire l'URL d'authentification Keycloak
- const authUrl = new URL(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/auth`);
- authUrl.searchParams.set('client_id', clientId);
- authUrl.searchParams.set('response_type', 'code');
- authUrl.searchParams.set('redirect_uri', decodeURIComponent(finalRedirectUri));
- authUrl.searchParams.set('scope', 'openid profile email');
- authUrl.searchParams.set('code_challenge', codeChallenge);
- authUrl.searchParams.set('code_challenge_method', 'S256');
-
- // Générer un state pour la sécurité (optionnel mais recommandé)
- const state = Math.random().toString(36).substring(2, 15);
- authUrl.searchParams.set('state', state);
-
- // Nettoyer les anciens cookies d'authentification
- const response = NextResponse.redirect(authUrl.toString());
-
- // Supprimer les anciens cookies qui pourraient causer des conflits
- response.cookies.delete('keycloak-token');
- response.cookies.delete('auth-state');
- response.cookies.delete('pkce_code_verifier');
-
- // Stocker le nouveau code verifier
- console.log('🍪 Création du cookie pkce_code_verifier:', {
- codeVerifier: codeVerifier.substring(0, 20) + '...',
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'lax',
- maxAge: 1800 // 30 minutes
- });
-
- response.cookies.set('pkce_code_verifier', codeVerifier, {
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'lax',
- path: '/',
- maxAge: 1800 // 30 minutes - plus de temps pour l'authentification
- });
-
- // Redirection vers Keycloak
- return response;
-
- } catch (error) {
- console.error('Erreur lors de la redirection vers Keycloak:', error);
-
- // En cas d'erreur, retourner une erreur JSON
- return NextResponse.json(
- { error: 'Erreur lors de l\'initialisation de l\'authentification' },
- { status: 500 }
- );
- }
-}
-
-/**
- * Gestion des autres méthodes HTTP (non supportées)
- */
-export async function POST() {
- return NextResponse.json(
- { error: 'Méthode non supportée. Utilisez GET pour déclencher l\'authentification.' },
- { status: 405 }
- );
-}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
deleted file mode 100644
index 1799fba..0000000
--- a/app/api/auth/logout/route.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-/**
- * API Route pour déclencher la déconnexion Keycloak
- * Redirige directement vers Keycloak pour la déconnexion
- */
-export async function GET(request: NextRequest) {
- try {
- // Configuration Keycloak depuis les variables d'environnement
- const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev';
- const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress';
- const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend';
-
- // URL de redirection après déconnexion
- const postLogoutRedirectUri = encodeURIComponent(`${request.nextUrl.origin}/`);
-
- // Construire l'URL de déconnexion Keycloak
- const logoutUrl = new URL(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/logout`);
- logoutUrl.searchParams.set('client_id', clientId);
- logoutUrl.searchParams.set('post_logout_redirect_uri', decodeURIComponent(postLogoutRedirectUri));
-
- // Supprimer les cookies d'authentification
- const response = NextResponse.redirect(logoutUrl.toString());
-
- // Supprimer les cookies liés à l'authentification
- response.cookies.delete('keycloak-token');
- response.cookies.delete('keycloak-refresh-token');
- response.cookies.delete('keycloak-id-token');
-
- return response;
-
- } catch (error) {
- console.error('Erreur lors de la déconnexion Keycloak:', error);
-
- // En cas d'erreur, rediriger vers la page d'accueil
- const response = NextResponse.redirect(new URL('/', request.url));
-
- // Supprimer quand même les cookies en cas d'erreur
- response.cookies.delete('keycloak-token');
- response.cookies.delete('keycloak-refresh-token');
- response.cookies.delete('keycloak-id-token');
-
- return response;
- }
-}
-
-/**
- * Gestion des autres méthodes HTTP (non supportées)
- */
-export async function POST() {
- return NextResponse.json(
- { error: 'Méthode non supportée. Utilisez GET pour déclencher la déconnexion.' },
- { status: 405 }
- );
-}
diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts
deleted file mode 100644
index de68de2..0000000
--- a/app/api/auth/token/route.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-/**
- * API Route pour échanger le code d'autorisation contre des tokens
- */
-export async function POST(request: NextRequest) {
- try {
- console.log('📥 POST /api/auth/token - Requête reçue:', {
- method: request.method,
- url: request.url,
- headers: {
- 'content-type': request.headers.get('content-type'),
- 'content-length': request.headers.get('content-length'),
- }
- });
-
- let body;
- try {
- const rawBody = await request.text();
- console.log('📄 Corps brut de la requête:', {
- length: rawBody.length,
- preview: rawBody.substring(0, 100)
- });
-
- if (!rawBody || rawBody.trim() === '') {
- console.error('❌ Corps de la requête vide');
- return NextResponse.json(
- { error: 'Corps de la requête vide' },
- { status: 400 }
- );
- }
-
- body = JSON.parse(rawBody);
- } catch (jsonError) {
- console.error('❌ Erreur parsing JSON:', jsonError.message);
- return NextResponse.json(
- { error: 'JSON invalide' },
- { status: 400 }
- );
- }
-
- const { code, state } = body;
-
- if (!code) {
- return NextResponse.json(
- { error: 'Code d\'autorisation manquant' },
- { status: 400 }
- );
- }
-
- // Configuration Keycloak
- const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev';
- const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress';
- const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend';
-
- // Préparer l'URL de base
- const baseUrl = process.env.NODE_ENV === 'production'
- ? 'https://btpxpress.lions.dev'
- : 'http://localhost:3000';
-
- // Récupérer le code verifier depuis les cookies
- const codeVerifier = request.cookies.get('pkce_code_verifier')?.value;
-
- console.log('🍪 Cookies reçus:', {
- codeVerifier: codeVerifier ? codeVerifier.substring(0, 20) + '...' : 'MANQUANT',
- hasCookie: !!codeVerifier
- });
-
- if (!codeVerifier) {
- console.error('❌ Code verifier manquant dans les cookies');
- return NextResponse.json(
- { error: 'Code verifier manquant' },
- { status: 400 }
- );
- }
-
- console.log('🔄 Échange de token:', {
- code: code.substring(0, 20) + '...',
- codeVerifier: codeVerifier.substring(0, 20) + '...',
- redirectUri: `${baseUrl}/dashboard`
- });
- // Préparer les données pour l'échange de token
- const tokenData = new URLSearchParams({
- grant_type: 'authorization_code',
- client_id: clientId,
- code: code,
- redirect_uri: `${baseUrl}/dashboard`,
- code_verifier: codeVerifier,
- });
-
- // Échanger le code contre des tokens
- const tokenResponse = await fetch(
- `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: tokenData.toString(),
- }
- );
-
- if (!tokenResponse.ok) {
- const errorText = await tokenResponse.text();
- console.error('❌ Erreur échange token:', {
- status: tokenResponse.status,
- statusText: tokenResponse.statusText,
- error: errorText
- });
- return NextResponse.json(
- { error: 'Échec de l\'échange de token', details: errorText },
- { status: 400 }
- );
- }
-
- const tokens = await tokenResponse.json();
- console.log('✅ Tokens reçus avec succès:', {
- hasAccessToken: !!tokens.access_token,
- hasRefreshToken: !!tokens.refresh_token,
- hasIdToken: !!tokens.id_token
- });
-
- // Créer la réponse avec les tokens
- const response = NextResponse.json({
- success: true,
- returnUrl: '/dashboard'
- });
-
- // Stocker les tokens dans des cookies HttpOnly sécurisés
- const isProduction = process.env.NODE_ENV === 'production';
-
- response.cookies.set('keycloak-token', tokens.access_token, {
- httpOnly: true,
- secure: isProduction,
- sameSite: 'lax',
- maxAge: tokens.expires_in || 3600, // 1 heure par défaut
- path: '/'
- });
-
- response.cookies.set('keycloak-refresh-token', tokens.refresh_token, {
- httpOnly: true,
- secure: isProduction,
- sameSite: 'lax',
- maxAge: tokens.refresh_expires_in || 86400, // 24 heures par défaut
- path: '/'
- });
-
- response.cookies.set('keycloak-id-token', tokens.id_token, {
- httpOnly: true,
- secure: isProduction,
- sameSite: 'lax',
- maxAge: tokens.expires_in || 3600,
- path: '/'
- });
-
- // Supprimer le cookie du code verifier
- response.cookies.delete('pkce_code_verifier');
-
- console.log('🍪 Tokens stockés dans des cookies HttpOnly sécurisés');
-
- return response;
-
- } catch (error) {
- console.error('❌ Erreur lors de l\'échange de token:', {
- message: error.message,
- stack: error.stack,
- name: error.name,
- cause: error.cause
- });
-
- // Si c'est une erreur de code invalide, suggérer un nouveau cycle d'authentification
- if (error.message && error.message.includes('invalid_grant')) {
- console.log('🔄 Code d\'autorisation expiré, nettoyage des cookies...');
- const response = NextResponse.json(
- {
- error: 'Code d\'autorisation expiré',
- details: 'Le code d\'autorisation a expiré. Un nouveau cycle d\'authentification est nécessaire.',
- shouldRetry: true
- },
- { status: 400 }
- );
-
- // Nettoyer le cookie du code verifier expiré
- response.cookies.delete('pkce_code_verifier');
- return response;
- }
-
- return NextResponse.json(
- { error: 'Erreur interne du serveur', details: error.message },
- { status: 500 }
- );
- }
-}
-
-/**
- * Gestion des autres méthodes HTTP
- */
-export async function GET() {
- return NextResponse.json(
- { error: 'Méthode non supportée. Utilisez POST pour échanger un code.' },
- { status: 405 }
- );
-}
diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx
index 612e373..38a735d 100644
--- a/app/auth/callback/page.tsx
+++ b/app/auth/callback/page.tsx
@@ -1,8 +1,7 @@
'use client';
export const dynamic = 'force-dynamic';
-
-import React, { useEffect, useState, Suspense } from 'react';
+import React, { useEffect, useState, useRef, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { ProgressSpinner } from 'primereact/progressspinner';
@@ -11,25 +10,48 @@ function AuthCallbackContent() {
const searchParams = useSearchParams();
const [status, setStatus] = useState('Traitement de l\'authentification...');
+ // ✅ Protection contre les appels multiples
+ const hasExchanged = useRef(false);
+ const isProcessing = useRef(false);
+
useEffect(() => {
const handleAuthCallback = async () => {
+ // ✅ Vérifier si l'échange a déjà été fait
+ if (hasExchanged.current || isProcessing.current) {
+ console.log('⏭️ Code exchange already attempted or in progress, skipping');
+ return;
+ }
+
+ // ✅ Marquer comme en cours de traitement
+ isProcessing.current = true;
+
try {
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
+ console.log('🔐 Starting OAuth callback handling...');
+
if (error) {
+ console.error('❌ OAuth error:', error);
setStatus(`Erreur d'authentification: ${error}`);
+ hasExchanged.current = true;
setTimeout(() => router.push('/auth/login'), 3000);
return;
}
if (!code) {
+ console.error('❌ No authorization code');
setStatus('Code d\'autorisation manquant');
+ hasExchanged.current = true;
setTimeout(() => router.push('/auth/login'), 3000);
return;
}
+ // ✅ Marquer comme échangé AVANT l'appel
+ hasExchanged.current = true;
+
+ console.log('✅ Authorization code received, exchanging for tokens...');
setStatus('Échange du code d\'autorisation...');
// Échanger le code contre des tokens
@@ -42,28 +64,44 @@ function AuthCallbackContent() {
});
if (!response.ok) {
- throw new Error('Échec de l\'échange de token');
+ const errorData = await response.json().catch(() => ({}));
+ console.error('❌ Token exchange failed:', errorData);
+ throw new Error(errorData.error || 'Échec de l\'échange de token');
}
const result = await response.json();
+ console.log('✅ Token exchange successful');
setStatus('Authentification réussie, redirection...');
// Les tokens sont maintenant stockés dans des cookies HttpOnly côté serveur
// Pas besoin de les stocker dans localStorage
- // Rediriger vers le dashboard
- window.location.href = '/dashboard';
+ // ✅ Nettoyer l'URL avant redirection
+ const cleanUrl = window.location.pathname;
+ window.history.replaceState({}, document.title, cleanUrl);
+
+ // Rediriger vers le dashboard après un court délai
+ setTimeout(() => {
+ console.log('✅ Redirecting to dashboard');
+ window.location.href = '/dashboard';
+ }, 500);
} catch (error) {
- console.error('Erreur lors du traitement de l\'authentification:', error);
+ console.error('❌ Error during authentication processing:', error);
setStatus('Erreur lors de l\'authentification');
- setTimeout(() => router.push('/auth/login'), 3000);
+
+ // En cas d'erreur, permettre un nouvel essai après un délai
+ setTimeout(() => {
+ hasExchanged.current = false;
+ isProcessing.current = false;
+ router.push('/auth/login');
+ }, 3000);
}
};
handleAuthCallback();
- }, [searchParams, router]);
+ }, []); // ✅ Tableau vide - s'exécute UNE SEULE FOIS au montage
return (
diff --git a/app/auth/callback/page.tsx.backup b/app/auth/callback/page.tsx.backup
new file mode 100644
index 0000000..612e373
--- /dev/null
+++ b/app/auth/callback/page.tsx.backup
@@ -0,0 +1,94 @@
+'use client';
+export const dynamic = 'force-dynamic';
+
+
+import React, { useEffect, useState, Suspense } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { ProgressSpinner } from 'primereact/progressspinner';
+
+function AuthCallbackContent() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [status, setStatus] = useState('Traitement de l\'authentification...');
+
+ useEffect(() => {
+ const handleAuthCallback = async () => {
+ try {
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+ const error = searchParams.get('error');
+
+ if (error) {
+ setStatus(`Erreur d'authentification: ${error}`);
+ setTimeout(() => router.push('/auth/login'), 3000);
+ return;
+ }
+
+ if (!code) {
+ setStatus('Code d\'autorisation manquant');
+ setTimeout(() => router.push('/auth/login'), 3000);
+ return;
+ }
+
+ setStatus('Échange du code d\'autorisation...');
+
+ // Échanger le code contre des tokens
+ const response = await fetch('/api/auth/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ code, state }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Échec de l\'échange de token');
+ }
+
+ const result = await response.json();
+
+ setStatus('Authentification réussie, redirection...');
+
+ // Les tokens sont maintenant stockés dans des cookies HttpOnly côté serveur
+ // Pas besoin de les stocker dans localStorage
+
+ // Rediriger vers le dashboard
+ window.location.href = '/dashboard';
+
+ } catch (error) {
+ console.error('Erreur lors du traitement de l\'authentification:', error);
+ setStatus('Erreur lors de l\'authentification');
+ setTimeout(() => router.push('/auth/login'), 3000);
+ }
+ };
+
+ handleAuthCallback();
+ }, [searchParams, router]);
+
+ return (
+
+
+
+
Authentification en cours
+
{status}
+
+
+ );
+}
+
+const AuthCallbackPage = () => {
+ return (
+
+
+
+ }>
+
+
+ );
+};
+
+export default AuthCallbackPage;
diff --git a/app/page.tsx b/app/page.tsx
index a365db7..2e00e4a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -110,7 +110,7 @@ const LandingPage = () => {
- window.location.href = '/api/auth/login'} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer">
+ window.location.href = 'http://localhost:8080/api/v1/auth/login'} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer">
Connexion
@@ -168,7 +168,7 @@ const LandingPage = () => {
Facturation automatisée
- window.location.href = '/api/auth/login'} className="p-button text-white bg-orange-500 border-orange-500 font-bold border-round cursor-pointer mr-3 shadow-3" style={{ padding: '1.2rem 2.5rem', fontSize: '1.2rem' }}>
+ window.location.href = 'http://localhost:8080/api/v1/auth/login'} className="p-button text-white bg-orange-500 border-orange-500 font-bold border-round cursor-pointer mr-3 shadow-3" style={{ padding: '1.2rem 2.5rem', fontSize: '1.2rem' }}>
Démarrer maintenant
@@ -469,7 +469,7 @@ const LandingPage = () => {
window.location.href = '/api/auth/login'}
+ onClick={() => window.location.href = 'http://localhost:8080/api/v1/auth/login'}
className="w-full p-button-outlined border-orange-300 text-orange-600 font-semibold py-3"
>
Commencer l'essai gratuit
@@ -513,7 +513,7 @@ const LandingPage = () => {
window.location.href = '/api/auth/login'}
+ onClick={() => window.location.href = 'http://localhost:8080/api/v1/auth/login'}
className="w-full bg-cyan-500 border-cyan-500 text-white font-semibold py-3"
>
Démarrer maintenant
@@ -554,7 +554,7 @@ const LandingPage = () => {
window.location.href = '/api/auth/login'}
+ onClick={() => window.location.href = 'http://localhost:8080/api/v1/auth/login'}
className="w-full p-button-outlined border-purple-300 text-purple-600 font-semibold py-3"
>
Nous contacter
@@ -576,7 +576,7 @@ const LandingPage = () => {
window.location.href = '/api/auth/login'}
+ onClick={() => window.location.href = 'http://localhost:8080/api/v1/auth/login'}
style={{
backgroundColor: 'white',
color: '#f97316',
@@ -591,7 +591,7 @@ const LandingPage = () => {
Essai gratuit 30 jours
window.location.href = '/api/auth/login'}
+ onClick={() => window.location.href = 'http://localhost:8080/api/v1/auth/login'}
style={{
backgroundColor: 'transparent',
color: 'white',
@@ -636,8 +636,8 @@ const LandingPage = () => {
diff --git a/env.example b/env.example
new file mode 100644
index 0000000..01dd2e4
--- /dev/null
+++ b/env.example
@@ -0,0 +1,16 @@
+# Configuration d'environnement pour BTPXpress Frontend
+# Copiez ce fichier vers .env.local et remplissez les valeurs
+
+# API Backend
+NEXT_PUBLIC_API_URL=http://localhost:8080
+NEXT_PUBLIC_API_TIMEOUT=15000
+
+# Keycloak
+NEXT_PUBLIC_KEYCLOAK_URL=https://security.lions.dev
+NEXT_PUBLIC_KEYCLOAK_REALM=btpxpress
+NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=btpxpress-frontend
+
+# Application
+NEXT_PUBLIC_APP_NAME=BTP Xpress
+NEXT_PUBLIC_APP_VERSION=1.0.0
+NEXT_PUBLIC_APP_ENV=development
diff --git a/hooks/useDashboard.ts b/hooks/useDashboard.ts
index c7e6f7d..5e5b8bf 100644
--- a/hooks/useDashboard.ts
+++ b/hooks/useDashboard.ts
@@ -146,7 +146,7 @@ export const useDashboard = (periode: 'semaine' | 'mois' | 'trimestre' | 'annee'
error: errorMessage,
}));
}
- }, [currentPeriode]);
+ }, []);
useEffect(() => {
const abortController = new AbortController();
@@ -191,13 +191,13 @@ export const useDashboard = (periode: 'semaine' | 'mois' | 'trimestre' | 'annee'
}
return () => abortController.abort();
- }, [loadDashboardData]);
+ }, []); // Supprimer loadDashboardData des dépendances pour éviter la boucle
const refresh = useCallback(() => {
const abortController = new AbortController();
loadDashboardData(abortController);
return () => abortController.abort();
- }, [loadDashboardData]);
+ }, []);
const changePeriode = useCallback((nouvellePeriode: 'semaine' | 'mois' | 'trimestre' | 'annee') => {
setCurrentPeriode(nouvellePeriode);
diff --git a/middleware.ts b/middleware.ts
index 54fd9cc..99306e3 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,219 +1,38 @@
/**
- * Middleware Next.js pour la protection des routes avec Keycloak
- * Gère l'authentification et l'autorisation au niveau des routes
+ * Middleware Next.js simplifié
+ *
+ * L'authentification est entièrement gérée par le backend Quarkus avec Keycloak OIDC.
+ * Le middleware frontend laisse passer toutes les requêtes.
+ *
+ * Flux d'authentification:
+ * 1. User accède à une page protégée du frontend (ex: /dashboard)
+ * 2. Frontend appelle l'API backend (ex: http://localhost:8080/api/v1/dashboard)
+ * 3. Backend détecte absence de session -> redirige vers Keycloak (security.lions.dev)
+ * 4. User se connecte sur Keycloak
+ * 5. Keycloak redirige vers le backend avec le code OAuth
+ * 6. Backend échange le code, crée une session, renvoie un cookie
+ * 7. Frontend reçoit le cookie et peut maintenant appeler l'API
*/
-import { NextRequest, NextResponse } from 'next/server';
-import { jwtVerify, JWTPayload } from 'jose';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
-// Configuration des routes protégées
-const PROTECTED_ROUTES = [
- '/dashboard',
- '/chantiers',
- '/clients',
- '/devis',
- '/factures',
- '/materiels',
- '/employes',
- '/equipes',
- '/planning',
- '/reports',
- '/admin',
- '/profile',
-] as const;
-
-// Configuration des routes publiques (toujours accessibles)
-const PUBLIC_ROUTES = [
- '/',
- '/api/health',
- '/api/auth/login',
- '/api/auth/logout',
- '/api/auth/token',
-] as const;
-
-// Configuration des routes par rôle
-const ROLE_BASED_ROUTES = {
- '/admin': ['super_admin', 'admin'],
- '/reports': ['super_admin', 'admin', 'directeur', 'manager'],
- '/employes': ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
- '/equipes': ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
- '/materiels/manage': ['super_admin', 'admin', 'logisticien'],
- '/clients/manage': ['super_admin', 'admin', 'commercial'],
- '/devis/manage': ['super_admin', 'admin', 'commercial'],
- '/factures/manage': ['super_admin', 'admin', 'comptable'],
-} as const;
-
-// Interface pour le token JWT décodé
-interface KeycloakToken extends JWTPayload {
- preferred_username?: string;
- email?: string;
- given_name?: string;
- family_name?: string;
- realm_access?: {
- roles: string[];
- };
- resource_access?: {
- [key: string]: {
- roles: string[];
- };
- };
-}
-
-// Fonction pour vérifier si une route est protégée
-function isProtectedRoute(pathname: string): boolean {
- return PROTECTED_ROUTES.some(route => pathname.startsWith(route));
-}
-
-// Fonction pour vérifier si une route est publique
-function isPublicRoute(pathname: string): boolean {
- return PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route));
-}
-
-// Fonction pour extraire les rôles du token
-function extractRoles(token: KeycloakToken): string[] {
- const realmRoles = token.realm_access?.roles || [];
- const clientRoles = token.resource_access?.['btpxpress-frontend']?.roles || [];
- return [...realmRoles, ...clientRoles];
-}
-
-// Fonction pour vérifier les permissions de rôle
-function hasRequiredRole(userRoles: string[], requiredRoles: string[]): boolean {
- return requiredRoles.some(role => userRoles.includes(role));
-}
-
-// Fonction pour vérifier et décoder le token JWT
-async function verifyToken(token: string): Promise {
- try {
- // Configuration de la clé publique Keycloak
- const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev';
- const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress';
-
- // Récupérer la clé publique depuis Keycloak
- const jwksUrl = `${keycloakUrl}/realms/${realm}/protocol/openid_connect/certs`;
-
- // Pour la vérification côté serveur, nous utilisons une approche simplifiée
- // En production, il faudrait implémenter une vérification complète avec JWKS
-
- // Décoder le token sans vérification pour le middleware (vérification côté client)
- const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
-
- // Vérifier l'expiration
- if (payload.exp && payload.exp < Date.now() / 1000) {
- return null;
- }
-
- return payload as KeycloakToken;
- } catch (error) {
- console.error('Erreur lors de la vérification du token:', error);
- return null;
- }
-}
-
-// Fonction principale du middleware
-export async function middleware(request: NextRequest) {
- const { pathname } = request.nextUrl;
-
- // Ignorer les fichiers statiques et les API routes internes
- if (
- pathname.startsWith('/_next') ||
- pathname.startsWith('/api') ||
- pathname.includes('.') ||
- pathname.startsWith('/favicon')
- ) {
- return NextResponse.next();
- }
-
- // Permettre l'accès aux routes publiques
- if (isPublicRoute(pathname)) {
- return NextResponse.next();
- }
-
- // Vérifier si la route nécessite une authentification
- if (isProtectedRoute(pathname)) {
- // Récupérer le token depuis les cookies ou headers
- const authHeader = request.headers.get('authorization');
- const tokenFromCookie = request.cookies.get('keycloak-token')?.value;
- const pkceVerifier = request.cookies.get('pkce_code_verifier')?.value;
- const hasAuthCode = request.nextUrl.searchParams.has('code');
-
- console.log(`🔍 Middleware: Vérification de ${pathname}:`, {
- hasAuthHeader: !!authHeader,
- hasTokenCookie: !!tokenFromCookie,
- hasPkceVerifier: !!pkceVerifier,
- hasCode: hasAuthCode
- });
-
- let token: string | null = null;
-
- if (authHeader && authHeader.startsWith('Bearer ')) {
- token = authHeader.substring(7);
- } else if (tokenFromCookie) {
- token = tokenFromCookie;
- }
-
- // Si pas de token, vérifier si un processus d'authentification est en cours
- if (!token) {
- // Autoriser l'accès SEULEMENT si on a un code d'autorisation ET un PKCE verifier
- // Cela permet le premier passage pour l'échange du code
- if (hasAuthCode && pkceVerifier && pathname === '/dashboard') {
- console.log('🔓 Middleware: Autorisant /dashboard pour l\'échange du code d\'autorisation');
- return NextResponse.next();
- }
-
- console.log(`🔒 Middleware: Redirection vers /api/auth/login pour ${pathname} (pas de token)`);
- const loginUrl = new URL('/api/auth/login', request.url);
- loginUrl.searchParams.set('redirect', pathname);
- return NextResponse.redirect(loginUrl);
- }
-
- // Vérifier et décoder le token
- const decodedToken = await verifyToken(token);
-
- if (!decodedToken) {
- // Token invalide ou expiré
- const loginUrl = new URL('/api/auth/login', request.url);
- loginUrl.searchParams.set('redirect', pathname);
- return NextResponse.redirect(loginUrl);
- }
-
- // Extraire les rôles de l'utilisateur
- const userRoles = extractRoles(decodedToken);
-
- // Vérifier les permissions basées sur les rôles pour des routes spécifiques
- for (const [route, requiredRoles] of Object.entries(ROLE_BASED_ROUTES)) {
- if (pathname.startsWith(route)) {
- if (!hasRequiredRole(userRoles, [...requiredRoles])) {
- // Utilisateur n'a pas les rôles requis
- return NextResponse.redirect(new URL('/api/auth/login', request.url));
- }
- break;
- }
- }
-
- // Ajouter les informations utilisateur aux headers pour les composants
- const response = NextResponse.next();
- response.headers.set('x-user-id', decodedToken.sub || '');
- response.headers.set('x-user-email', decodedToken.email || '');
- response.headers.set('x-user-roles', JSON.stringify(userRoles));
-
- return response;
- }
-
- // Par défaut, permettre l'accès
+export function middleware(request: NextRequest) {
+ // Le middleware ne fait plus rien - l'authentification est gérée par le backend
+ // Toutes les requêtes sont autorisées côté frontend
return NextResponse.next();
}
-// Configuration du matcher pour spécifier quelles routes le middleware doit traiter
+// Configuration du matcher - appliqué à toutes les routes sauf les fichiers statiques
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
- * - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public files (images, etc.)
*/
- '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*|public).*)',
+ '/((?!_next/static|_next/image|favicon.ico|.*\..*|public).*)',
],
};
diff --git a/next.config.js b/next.config.js
index c4e504d..b42d520 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- reactStrictMode: true,
+ reactStrictMode: false, // Disabled to prevent double OAuth code usage in dev
output: 'standalone',
// Optimisations pour la production
@@ -23,9 +23,12 @@ const nextConfig = {
// Optimisations de performance
experimental: {
optimizeCss: true,
- optimizePackageImports: ['primereact', 'primeicons'],
+ optimizePackageImports: ['primereact', 'primeicons', 'chart.js', 'axios'],
},
+ // Packages externes pour les composants serveur
+ serverExternalPackages: ['@prisma/client'],
+
// Configuration Turbopack (stable)
turbopack: {
rules: {
@@ -36,8 +39,32 @@ const nextConfig = {
},
},
- // Configuration du bundler simplifiée
- webpack: (config, { dev }) => {
+ // Configuration du bundler optimisée
+ webpack: (config, { dev, isServer }) => {
+ // Optimisations pour la production
+ if (!dev && !isServer) {
+ config.optimization.splitChunks = {
+ chunks: 'all',
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ primereact: {
+ test: /[\\/]node_modules[\\/]primereact[\\/]/,
+ name: 'primereact',
+ chunks: 'all',
+ },
+ charts: {
+ test: /[\\/]node_modules[\\/](chart\.js|react-chartjs-2)[\\/]/,
+ name: 'charts',
+ chunks: 'all',
+ },
+ },
+ };
+ }
+
// Alias pour optimiser les imports
config.resolve.alias = {
...config.resolve.alias,
diff --git a/package-lock.json b/package-lock.json
index 27c2112..38fd528 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,30 +40,30 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
- "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
- "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
+ "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/wasi-threads": "1.0.4",
+ "@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
- "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
+ "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -71,9 +71,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
- "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -125,44 +125,44 @@
}
},
"node_modules/@fullcalendar/daygrid": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.4.tgz",
- "integrity": "sha512-X0QWEiA/hT8GYiQzmXt9DlZTWaQbNtBHBXGtaMNcVXbGHDCzLoWTHrde/jABGfr/i2+d9sLUO4oTtwz2HVpNtQ==",
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz",
+ "integrity": "sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ==",
"license": "MIT",
"peerDependencies": {
- "@fullcalendar/core": "~6.1.4"
+ "@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/interaction": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.4.tgz",
- "integrity": "sha512-69G2B61bPiHy7VyTDiwU9l8yRHPUK9XxNxjIdm3N0nvWR6BaUIBDQe8dIWht+IZUf9qirFhnfcLWkRI0fOTWtw==",
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.19.tgz",
+ "integrity": "sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w==",
"license": "MIT",
"peerDependencies": {
- "@fullcalendar/core": "~6.1.4"
+ "@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/react": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.4.tgz",
- "integrity": "sha512-9nc0uHr7zfhNIMyaM+ftoVqX9UEn+frCjTeofN3lcl3bCYvo2dwLw+n9jBJx//f+NRrSwmIvKq2JrqmF2IrwJA==",
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.19.tgz",
+ "integrity": "sha512-FP78vnyylaL/btZeHig8LQgfHgfwxLaIG6sKbNkzkPkKEACv11UyyBoTSkaavPsHtXvAkcTED1l7TOunAyPEnA==",
"license": "MIT",
"peerDependencies": {
- "@fullcalendar/core": "~6.1.4",
- "react": "^16.7.0 || ^17 || ^18",
- "react-dom": "^16.7.0 || ^17 || ^18"
+ "@fullcalendar/core": "~6.1.19",
+ "react": "^16.7.0 || ^17 || ^18 || ^19",
+ "react-dom": "^16.7.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@fullcalendar/timegrid": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.4.tgz",
- "integrity": "sha512-B2/levLKW0CyDQru75JeuASpCZml5W8sINCwVTstxxhjKmNBG3F5qvSX12DDrTdga/ySYObNNP1pKDwavKk/JQ==",
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.19.tgz",
+ "integrity": "sha512-OuzpUueyO9wB5OZ8rs7TWIoqvu4v3yEqdDxZ2VcsMldCpYJRiOe7yHWKr4ap5Tb0fs7Rjbserc/b6Nt7ol6BRg==",
"license": "MIT",
"dependencies": {
- "@fullcalendar/daygrid": "~6.1.4"
+ "@fullcalendar/daygrid": "~6.1.19"
},
"peerDependencies": {
- "@fullcalendar/core": "~6.1.4"
+ "@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@humanwhocodes/config-array": {
@@ -203,10 +203,20 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/@img/colour": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
+ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@img/sharp-darwin-arm64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
- "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
+ "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
"cpu": [
"arm64"
],
@@ -222,13 +232,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.2.0"
+ "@img/sharp-libvips-darwin-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-darwin-x64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
- "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
+ "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
"cpu": [
"x64"
],
@@ -244,13 +254,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.2.0"
+ "@img/sharp-libvips-darwin-x64": "1.2.3"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
- "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
+ "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
"cpu": [
"arm64"
],
@@ -264,9 +274,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
- "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
+ "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
"cpu": [
"x64"
],
@@ -280,9 +290,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
- "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
+ "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
"cpu": [
"arm"
],
@@ -296,9 +306,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
- "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
+ "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
"cpu": [
"arm64"
],
@@ -312,9 +322,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
- "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
+ "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
"cpu": [
"ppc64"
],
@@ -328,9 +338,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
- "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
+ "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
"cpu": [
"s390x"
],
@@ -344,9 +354,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
- "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
+ "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==",
"cpu": [
"x64"
],
@@ -360,9 +370,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
- "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
+ "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
"cpu": [
"arm64"
],
@@ -376,9 +386,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
- "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
+ "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
"cpu": [
"x64"
],
@@ -392,9 +402,9 @@
}
},
"node_modules/@img/sharp-linux-arm": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
- "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
+ "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
"cpu": [
"arm"
],
@@ -410,13 +420,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.2.0"
+ "@img/sharp-libvips-linux-arm": "1.2.3"
}
},
"node_modules/@img/sharp-linux-arm64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
- "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
+ "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
"cpu": [
"arm64"
],
@@ -432,13 +442,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.2.0"
+ "@img/sharp-libvips-linux-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-ppc64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
- "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
+ "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
"cpu": [
"ppc64"
],
@@ -454,13 +464,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.2.0"
+ "@img/sharp-libvips-linux-ppc64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-s390x": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
- "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
+ "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
"cpu": [
"s390x"
],
@@ -476,13 +486,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.2.0"
+ "@img/sharp-libvips-linux-s390x": "1.2.3"
}
},
"node_modules/@img/sharp-linux-x64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
- "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
+ "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==",
"cpu": [
"x64"
],
@@ -498,13 +508,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.2.0"
+ "@img/sharp-libvips-linux-x64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
- "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
+ "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
"cpu": [
"arm64"
],
@@ -520,13 +530,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
- "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
+ "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
"cpu": [
"x64"
],
@@ -542,20 +552,20 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.2.0"
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.3"
}
},
"node_modules/@img/sharp-wasm32": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
- "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
+ "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
- "@emnapi/runtime": "^1.4.4"
+ "@emnapi/runtime": "^1.5.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -565,9 +575,9 @@
}
},
"node_modules/@img/sharp-win32-arm64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
- "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
+ "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
"cpu": [
"arm64"
],
@@ -584,9 +594,9 @@
}
},
"node_modules/@img/sharp-win32-ia32": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
- "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
+ "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
"cpu": [
"ia32"
],
@@ -603,9 +613,9 @@
}
},
"node_modules/@img/sharp-win32-x64": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
- "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
+ "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
"cpu": [
"x64"
],
@@ -641,9 +651,9 @@
}
},
"node_modules/@next/env": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz",
- "integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz",
+ "integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -657,9 +667,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz",
- "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz",
+ "integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==",
"cpu": [
"arm64"
],
@@ -673,9 +683,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz",
- "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz",
+ "integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==",
"cpu": [
"x64"
],
@@ -689,9 +699,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz",
- "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz",
+ "integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==",
"cpu": [
"arm64"
],
@@ -705,9 +715,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz",
- "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz",
+ "integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==",
"cpu": [
"arm64"
],
@@ -721,9 +731,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz",
- "integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz",
+ "integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==",
"cpu": [
"x64"
],
@@ -737,9 +747,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz",
- "integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz",
+ "integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==",
"cpu": [
"x64"
],
@@ -753,9 +763,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz",
- "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz",
+ "integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==",
"cpu": [
"arm64"
],
@@ -769,9 +779,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz",
- "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz",
+ "integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==",
"cpu": [
"x64"
],
@@ -1159,9 +1169,9 @@
"license": "MIT"
},
"node_modules/@rushstack/eslint-patch": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz",
- "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz",
+ "integrity": "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==",
"dev": true,
"license": "MIT"
},
@@ -1175,9 +1185,9 @@
}
},
"node_modules/@tybys/wasm-util": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
- "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1906,9 +1916,9 @@
}
},
"node_modules/axe-core": {
- "version": "4.10.3",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
- "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
+ "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"dev": true,
"license": "MPL-2.0",
"engines": {
@@ -1916,9 +1926,9 @@
}
},
"node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -2053,9 +2063,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001733",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz",
- "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==",
+ "version": "1.0.30001751",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
+ "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"funding": [
{
"type": "opencollective",
@@ -2104,7 +2114,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
@@ -2122,20 +2132,6 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2154,17 +2150,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2329,9 +2314,9 @@
}
},
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2808,7 +2793,29 @@
}
}
},
- "node_modules/eslint-config-next/node_modules/eslint-import-resolver-typescript": {
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
"integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
@@ -2843,28 +2850,6 @@
}
}
},
- "node_modules/eslint-import-resolver-node": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
- "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^3.2.7",
- "is-core-module": "^2.13.0",
- "resolve": "^1.22.4"
- }
- },
- "node_modules/eslint-import-resolver-node/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
"node_modules/eslint-module-utils": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
@@ -3273,6 +3258,24 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3437,6 +3440,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3493,9 +3506,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
- "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3733,10 +3746,10 @@
}
},
"node_modules/immutable": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
- "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
- "devOptional": true,
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -3818,13 +3831,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
- "license": "MIT",
- "optional": true
- },
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -3979,14 +3985,15 @@
}
},
"node_modules/is-generator-function": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
- "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.0",
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
@@ -4578,9 +4585,9 @@
}
},
"node_modules/napi-postinstall": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
- "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4601,12 +4608,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "15.4.6",
- "resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz",
- "integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==",
+ "version": "15.5.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz",
+ "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==",
"license": "MIT",
"dependencies": {
- "@next/env": "15.4.6",
+ "@next/env": "15.5.6",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -4619,14 +4626,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "15.4.6",
- "@next/swc-darwin-x64": "15.4.6",
- "@next/swc-linux-arm64-gnu": "15.4.6",
- "@next/swc-linux-arm64-musl": "15.4.6",
- "@next/swc-linux-x64-gnu": "15.4.6",
- "@next/swc-linux-x64-musl": "15.4.6",
- "@next/swc-win32-arm64-msvc": "15.4.6",
- "@next/swc-win32-x64-msvc": "15.4.6",
+ "@next/swc-darwin-arm64": "15.5.6",
+ "@next/swc-darwin-x64": "15.5.6",
+ "@next/swc-linux-arm64-gnu": "15.5.6",
+ "@next/swc-linux-arm64-musl": "15.5.6",
+ "@next/swc-linux-x64-gnu": "15.5.6",
+ "@next/swc-linux-x64-musl": "15.5.6",
+ "@next/swc-win32-arm64-msvc": "15.5.6",
+ "@next/swc-win32-x64-msvc": "15.5.6",
"sharp": "^0.34.3"
},
"peerDependencies": {
@@ -4684,6 +4691,34 @@
}
}
},
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -5039,9 +5074,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -5058,9 +5093,9 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -5073,9 +5108,9 @@
"license": "MIT"
},
"node_modules/preact": {
- "version": "10.27.0",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz",
- "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==",
+ "version": "10.27.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
+ "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -5289,7 +5324,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
@@ -5357,13 +5392,13 @@
}
},
"node_modules/resolve": {
- "version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-core-module": "^2.16.0",
+ "is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -5505,10 +5540,10 @@
}
},
"node_modules/sass": {
- "version": "1.90.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz",
- "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
- "devOptional": true,
+ "version": "1.93.2",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
+ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
@@ -5535,9 +5570,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"devOptional": true,
"license": "ISC",
"bin": {
@@ -5597,15 +5632,15 @@
}
},
"node_modules/sharp": {
- "version": "0.34.3",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
- "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
+ "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "color": "^4.2.3",
- "detect-libc": "^2.0.4",
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.0",
"semver": "^7.7.2"
},
"engines": {
@@ -5615,34 +5650,34 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.34.3",
- "@img/sharp-darwin-x64": "0.34.3",
- "@img/sharp-libvips-darwin-arm64": "1.2.0",
- "@img/sharp-libvips-darwin-x64": "1.2.0",
- "@img/sharp-libvips-linux-arm": "1.2.0",
- "@img/sharp-libvips-linux-arm64": "1.2.0",
- "@img/sharp-libvips-linux-ppc64": "1.2.0",
- "@img/sharp-libvips-linux-s390x": "1.2.0",
- "@img/sharp-libvips-linux-x64": "1.2.0",
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
- "@img/sharp-libvips-linuxmusl-x64": "1.2.0",
- "@img/sharp-linux-arm": "0.34.3",
- "@img/sharp-linux-arm64": "0.34.3",
- "@img/sharp-linux-ppc64": "0.34.3",
- "@img/sharp-linux-s390x": "0.34.3",
- "@img/sharp-linux-x64": "0.34.3",
- "@img/sharp-linuxmusl-arm64": "0.34.3",
- "@img/sharp-linuxmusl-x64": "0.34.3",
- "@img/sharp-wasm32": "0.34.3",
- "@img/sharp-win32-arm64": "0.34.3",
- "@img/sharp-win32-ia32": "0.34.3",
- "@img/sharp-win32-x64": "0.34.3"
+ "@img/sharp-darwin-arm64": "0.34.4",
+ "@img/sharp-darwin-x64": "0.34.4",
+ "@img/sharp-libvips-darwin-arm64": "1.2.3",
+ "@img/sharp-libvips-darwin-x64": "1.2.3",
+ "@img/sharp-libvips-linux-arm": "1.2.3",
+ "@img/sharp-libvips-linux-arm64": "1.2.3",
+ "@img/sharp-libvips-linux-ppc64": "1.2.3",
+ "@img/sharp-libvips-linux-s390x": "1.2.3",
+ "@img/sharp-libvips-linux-x64": "1.2.3",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.3",
+ "@img/sharp-linux-arm": "0.34.4",
+ "@img/sharp-linux-arm64": "0.34.4",
+ "@img/sharp-linux-ppc64": "0.34.4",
+ "@img/sharp-linux-s390x": "0.34.4",
+ "@img/sharp-linux-x64": "0.34.4",
+ "@img/sharp-linuxmusl-arm64": "0.34.4",
+ "@img/sharp-linuxmusl-x64": "0.34.4",
+ "@img/sharp-wasm32": "0.34.4",
+ "@img/sharp-win32-arm64": "0.34.4",
+ "@img/sharp-win32-ia32": "0.34.4",
+ "@img/sharp-win32-x64": "0.34.4"
}
},
"node_modules/sharp/node_modules/detect-libc": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
- "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
@@ -5748,16 +5783,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/simple-swizzle": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
- "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -6003,14 +6028,14 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
- "version": "0.2.14",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
- "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.4",
- "picomatch": "^4.0.2"
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -6019,21 +6044,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
diff --git a/services/fournisseurService.ts b/services/fournisseurService.ts
index eec7525..e533469 100644
--- a/services/fournisseurService.ts
+++ b/services/fournisseurService.ts
@@ -1,353 +1,223 @@
-import axios from 'axios';
-import { API_CONFIG } from '../config/api';
-import {
- Fournisseur,
- FournisseurFormData,
- FournisseurFilters,
- CommandeFournisseur,
- CatalogueItem,
- TypeFournisseur,
- ApiResponse,
- PaginatedResponse
-} from '../types/btp-extended';
+import { apiService } from './api';
-class FournisseurService {
- private readonly basePath = '/api/v1/fournisseurs';
- private api = axios.create({
- baseURL: API_CONFIG.baseURL,
- timeout: API_CONFIG.timeout,
- headers: API_CONFIG.headers,
- });
-
- constructor() {
- // Interceptor pour ajouter le token Keycloak
- this.api.interceptors.request.use(
- async (config) => {
- // Vérifier si Keycloak est initialisé et l'utilisateur authentifié
- if (typeof window !== 'undefined') {
- const { keycloak, KEYCLOAK_TIMEOUTS } = await import('../config/keycloak');
-
- if (keycloak.authenticated) {
- try {
- // Rafraîchir le token si nécessaire
- await keycloak.updateToken(KEYCLOAK_TIMEOUTS.TOKEN_REFRESH_BEFORE_EXPIRY);
-
- // Ajouter le token Bearer à l'en-tête Authorization
- if (keycloak.token) {
- config.headers['Authorization'] = `Bearer ${keycloak.token}`;
- }
- } catch (error) {
- console.error('Erreur lors de la mise à jour du token Keycloak:', error);
- keycloak.login();
- throw error;
- }
- } else {
- // Fallback vers l'ancien système pour la rétrocompatibilité
- let token = null;
- try {
- const authTokenItem = sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token');
- if (authTokenItem) {
- const parsed = JSON.parse(authTokenItem);
- token = parsed.value;
- }
- } catch (e) {
- token = localStorage.getItem('token');
- }
-
- if (token) {
- config.headers['Authorization'] = `Bearer ${token}`;
- }
- }
- }
- return config;
- },
- (error) => Promise.reject(error)
- );
-
- // Interceptor pour les réponses
- this.api.interceptors.response.use(
- (response) => response,
- (error) => {
- if (error.response?.status === 401) {
- localStorage.removeItem('token');
- localStorage.removeItem('user');
- window.location.href = '/api/auth/login';
- }
- return Promise.reject(error);
- }
- );
- }
-
- /**
- * Récupérer tous les fournisseurs
- */
- async getAll(filters?: FournisseurFilters): Promise {
- const params = new URLSearchParams();
-
- if (filters?.actif !== undefined) {
- params.append('actifs', filters.actif.toString());
- }
- if (filters?.type) {
- params.append('type', filters.type);
- }
-
- const response = await this.api.get(`${this.basePath}?${params}`);
- return response.data;
- }
-
- /**
- * Récupérer un fournisseur par ID
- */
- async getById(id: number): Promise {
- const response = await this.api.get(`${this.basePath}/${id}`);
- return response.data;
- }
-
- /**
- * Créer un nouveau fournisseur
- */
- async create(fournisseur: FournisseurFormData): Promise {
- const response = await this.api.post(this.basePath, fournisseur);
- return response.data;
- }
-
- /**
- * Modifier un fournisseur existant
- */
- async update(id: number, fournisseur: FournisseurFormData): Promise {
- const response = await this.api.put(`${this.basePath}/${id}`, fournisseur);
- return response.data;
- }
-
- /**
- * Supprimer un fournisseur
- */
- async delete(id: number): Promise {
- await this.api.delete(`${this.basePath}/${id}`);
- }
-
- /**
- * Désactiver un fournisseur
- */
- async deactivate(id: number): Promise {
- await this.api.post(`${this.basePath}/${id}/desactiver`);
- }
-
- /**
- * Activer un fournisseur
- */
- async activate(id: number): Promise {
- await this.api.post(`${this.basePath}/${id}/activer`);
- }
-
- /**
- * Rechercher des fournisseurs
- */
- async search(terme: string): Promise {
- const response = await this.api.get(`${this.basePath}/recherche?q=${encodeURIComponent(terme)}`);
- return response.data;
- }
-
- /**
- * Récupérer les types de fournisseurs
- */
- async getTypes(): Promise {
- const response = await this.api.get(`${this.basePath}/types`);
- return response.data;
- }
-
- /**
- * Récupérer les commandes d'un fournisseur
- */
- async getCommandes(id: number): Promise {
- const response = await this.api.get(`${this.basePath}/${id}/commandes`);
- return response.data;
- }
-
- /**
- * Récupérer le catalogue d'un fournisseur
- */
- async getCatalogue(id: number): Promise {
- const response = await this.api.get(`${this.basePath}/${id}/catalogue`);
- return response.data;
- }
-
- /**
- * Récupérer les fournisseurs actifs uniquement
- */
- async getActifs(): Promise {
- return this.getAll({ actif: true });
- }
-
- /**
- * Récupérer les fournisseurs par type
- */
- async getByType(type: TypeFournisseur): Promise {
- return this.getAll({ type });
- }
-
- /**
- * Valider les données d'un fournisseur
- */
- validateFournisseur(fournisseur: FournisseurFormData): string[] {
- const errors: string[] = [];
-
- if (!fournisseur.nom || fournisseur.nom.trim().length === 0) {
- errors.push('Le nom du fournisseur est obligatoire');
- }
-
- if (fournisseur.nom && fournisseur.nom.length > 100) {
- errors.push('Le nom ne peut pas dépasser 100 caractères');
- }
-
- if (fournisseur.email && !this.isValidEmail(fournisseur.email)) {
- errors.push('L\'adresse email n\'est pas valide');
- }
-
- if (fournisseur.siret && fournisseur.siret.length > 20) {
- errors.push('Le numéro SIRET ne peut pas dépasser 20 caractères');
- }
-
- if (fournisseur.telephone && fournisseur.telephone.length > 20) {
- errors.push('Le numéro de téléphone ne peut pas dépasser 20 caractères');
- }
-
- return errors;
- }
-
- /**
- * Valider une adresse email
- */
- private isValidEmail(email: string): boolean {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return emailRegex.test(email);
- }
-
- /**
- * Formater l'adresse complète d'un fournisseur
- */
- formatAdresseComplete(fournisseur: Fournisseur): string {
- const parties: string[] = [];
-
- if (fournisseur.adresse) {
- parties.push(fournisseur.adresse);
- }
-
- if (fournisseur.codePostal || fournisseur.ville) {
- const ligneVille = [fournisseur.codePostal, fournisseur.ville]
- .filter(Boolean)
- .join(' ');
- if (ligneVille) {
- parties.push(ligneVille);
- }
- }
-
- if (fournisseur.pays && fournisseur.pays !== 'France') {
- parties.push(fournisseur.pays);
- }
-
- return parties.join(', ');
- }
-
- /**
- * Obtenir le libellé d'un type de fournisseur
- */
- getTypeLabel(type: TypeFournisseur): string {
- const labels: Record = {
- MATERIEL: 'Matériel',
- SERVICE: 'Service',
- SOUS_TRAITANT: 'Sous-traitant',
- LOCATION: 'Location',
- TRANSPORT: 'Transport',
- CONSOMMABLE: 'Consommable'
- };
- return labels[type] || type;
- }
-
- /**
- * Exporter la liste des fournisseurs au format CSV
- */
- async exportToCsv(filters?: FournisseurFilters): Promise {
- const fournisseurs = await this.getAll(filters);
-
- const headers = [
- 'ID', 'Nom', 'Type', 'SIRET', 'Email', 'Téléphone',
- 'Adresse', 'Code Postal', 'Ville', 'Pays', 'Actif'
- ];
-
- const csvContent = [
- headers.join(';'),
- ...fournisseurs.map(f => [
- f.id || '',
- f.nom || '',
- this.getTypeLabel(f.type),
- f.siret || '',
- f.email || '',
- f.telephone || '',
- f.adresse || '',
- f.codePostal || '',
- f.ville || '',
- f.pays || '',
- f.actif ? 'Oui' : 'Non'
- ].join(';'))
- ].join('\n');
-
- return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- }
-
- /**
- * Importer des fournisseurs depuis un fichier CSV
- */
- async importFromCsv(file: File): Promise<{ success: number; errors: string[] }> {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = async (e) => {
- try {
- const csv = e.target?.result as string;
- const lines = csv.split('\n');
- const headers = lines[0].split(';');
-
- let successCount = 0;
- const errors: string[] = [];
-
- for (let i = 1; i < lines.length; i++) {
- if (lines[i].trim()) {
- try {
- const values = lines[i].split(';');
- const fournisseur: FournisseurFormData = {
- nom: values[1] || '',
- type: (values[2] as TypeFournisseur) || 'MATERIEL',
- siret: values[3] || undefined,
- email: values[4] || undefined,
- telephone: values[5] || undefined,
- adresse: values[6] || undefined,
- codePostal: values[7] || undefined,
- ville: values[8] || undefined,
- pays: values[9] || 'France',
- actif: values[10] === 'Oui'
- };
-
- const validationErrors = this.validateFournisseur(fournisseur);
- if (validationErrors.length === 0) {
- await this.create(fournisseur);
- successCount++;
- } else {
- errors.push(`Ligne ${i + 1}: ${validationErrors.join(', ')}`);
- }
- } catch (error) {
- errors.push(`Ligne ${i + 1}: Erreur lors de la création`);
- }
- }
- }
-
- resolve({ success: successCount, errors });
- } catch (error) {
- reject(error);
- }
- };
- reader.readAsText(file);
- });
- }
+export interface Fournisseur {
+ id: string;
+ nom: string;
+ contact: string;
+ telephone: string;
+ email: string;
+ adresse: string;
+ ville: string;
+ codePostal: string;
+ pays: string;
+ siret?: string;
+ tva?: string;
+ conditionsPaiement: string;
+ delaiLivraison: number;
+ note?: string;
+ actif: boolean;
+ dateCreation: string;
+ dateModification: string;
}
-export default new FournisseurService();
\ No newline at end of file
+export interface CreateFournisseurRequest {
+ nom: string;
+ contact: string;
+ telephone: string;
+ email: string;
+ adresse: string;
+ ville: string;
+ codePostal: string;
+ pays: string;
+ siret?: string;
+ tva?: string;
+ conditionsPaiement: string;
+ delaiLivraison: number;
+ note?: string;
+}
+
+export interface UpdateFournisseurRequest {
+ nom?: string;
+ contact?: string;
+ telephone?: string;
+ email?: string;
+ adresse?: string;
+ ville?: string;
+ codePostal?: string;
+ pays?: string;
+ siret?: string;
+ tva?: string;
+ conditionsPaiement?: string;
+ delaiLivraison?: number;
+ note?: string;
+ actif?: boolean;
+}
+
+export class FournisseurService {
+ /**
+ * Récupère tous les fournisseurs
+ */
+ async getAllFournisseurs(): Promise {
+ try {
+ const response = await apiService.get('/fournisseurs');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des fournisseurs:', error);
+ return this.getMockFournisseurs();
+ }
+ }
+
+ /**
+ * Récupère un fournisseur par ID
+ */
+ async getFournisseurById(id: string): Promise {
+ try {
+ const response = await apiService.get(`/fournisseurs/${id}`);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération du fournisseur:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Crée un nouveau fournisseur
+ */
+ async createFournisseur(fournisseurData: CreateFournisseurRequest): Promise {
+ try {
+ const response = await apiService.post('/fournisseurs', fournisseurData);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la création du fournisseur:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Met à jour un fournisseur existant
+ */
+ async updateFournisseur(id: string, fournisseurData: UpdateFournisseurRequest): Promise {
+ try {
+ const response = await apiService.put(`/fournisseurs/${id}`, fournisseurData);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la mise à jour du fournisseur:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Supprime un fournisseur (soft delete)
+ */
+ async deleteFournisseur(id: string): Promise {
+ try {
+ await apiService.delete(`/fournisseurs/${id}`);
+ } catch (error) {
+ console.error('Erreur lors de la suppression du fournisseur:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Recherche des fournisseurs par nom
+ */
+ async searchFournisseurs(searchTerm: string): Promise {
+ try {
+ const response = await apiService.get(`/fournisseurs/search?q=${encodeURIComponent(searchTerm)}`);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la recherche des fournisseurs:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Récupère les statistiques des fournisseurs
+ */
+ async getFournisseurStats(): Promise<{
+ total: number;
+ actifs: number;
+ inactifs: number;
+ parPays: Record;
+ }> {
+ try {
+ const response = await apiService.get('/fournisseurs/stats');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des statistiques:', error);
+ return {
+ total: 0,
+ actifs: 0,
+ inactifs: 0,
+ parPays: {}
+ };
+ }
+ }
+
+ /**
+ * Données mockées pour les fournisseurs
+ */
+ private getMockFournisseurs(): Fournisseur[] {
+ return [
+ {
+ id: 'fourn-1',
+ nom: 'Matériaux BTP Pro',
+ contact: 'Jean Dupont',
+ telephone: '01 23 45 67 89',
+ email: 'contact@materiaux-btp-pro.fr',
+ adresse: '123 Rue de la Construction',
+ ville: 'Paris',
+ codePostal: '75001',
+ pays: 'France',
+ siret: '12345678901234',
+ tva: 'FR12345678901',
+ conditionsPaiement: '30 jours',
+ delaiLivraison: 7,
+ note: 'Fournisseur fiable pour les gros volumes',
+ actif: true,
+ dateCreation: '2024-01-15T00:00:00Z',
+ dateModification: '2024-01-15T00:00:00Z'
+ },
+ {
+ id: 'fourn-2',
+ nom: 'Outillage Express',
+ contact: 'Marie Martin',
+ telephone: '02 34 56 78 90',
+ email: 'contact@outillage-express.fr',
+ adresse: '456 Avenue des Outils',
+ ville: 'Lyon',
+ codePostal: '69001',
+ pays: 'France',
+ siret: '23456789012345',
+ tva: 'FR23456789012',
+ conditionsPaiement: '45 jours',
+ delaiLivraison: 5,
+ note: 'Spécialisé dans les outils de précision',
+ actif: true,
+ dateCreation: '2024-02-01T00:00:00Z',
+ dateModification: '2024-02-01T00:00:00Z'
+ },
+ {
+ id: 'fourn-3',
+ nom: 'Engins Chantier SARL',
+ contact: 'Pierre Durand',
+ telephone: '03 45 67 89 01',
+ email: 'contact@engins-chantier.fr',
+ adresse: '789 Boulevard des Engins',
+ ville: 'Marseille',
+ codePostal: '13001',
+ pays: 'France',
+ siret: '34567890123456',
+ tva: 'FR34567890123',
+ conditionsPaiement: '60 jours',
+ delaiLivraison: 14,
+ note: 'Location et vente d\'engins de chantier',
+ actif: true,
+ dateCreation: '2024-02-15T00:00:00Z',
+ dateModification: '2024-02-15T00:00:00Z'
+ }
+ ];
+ }
+}
+
+export const fournisseurService = new FournisseurService();
\ No newline at end of file
diff --git a/services/notificationService.ts b/services/notificationService.ts
index 8b13e67..9f7893d 100644
--- a/services/notificationService.ts
+++ b/services/notificationService.ts
@@ -1,4 +1,4 @@
-// import { apiService } from './api'; // TODO: Use when implementing real API calls
+import { apiService } from './api';
export interface Notification {
id: string;
@@ -30,72 +30,96 @@ export interface NotificationStats {
class NotificationService {
/**
* Récupérer toutes les notifications
- * TODO: Implement with proper API service method
*/
async getNotifications(): Promise {
- return this.getMockNotifications();
+ try {
+ const response = await apiService.get('/notifications');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des notifications:', error);
+ return this.getMockNotifications();
+ }
}
/**
* Récupérer les notifications non lues
- * TODO: Implement with proper API service method
*/
async getUnreadNotifications(): Promise {
- return this.getMockNotifications().filter(n => !n.lu);
+ try {
+ const response = await apiService.get('/notifications/unread');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des notifications non lues:', error);
+ return this.getMockNotifications().filter(n => !n.lu);
+ }
}
/**
* Marquer une notification comme lue
- * TODO: Implement with proper API service method
*/
async markAsRead(notificationId: string): Promise {
- console.log('TODO: Implement markAsRead', notificationId);
- return Promise.resolve();
+ try {
+ await apiService.put(`/notifications/${notificationId}/read`);
+ } catch (error) {
+ console.error('Erreur lors du marquage de la notification comme lue:', error);
+ }
}
/**
* Marquer toutes les notifications comme lues
- * TODO: Implement with proper API service method
*/
async markAllAsRead(): Promise {
- console.log('TODO: Implement markAllAsRead');
- return Promise.resolve();
+ try {
+ await apiService.put('/notifications/mark-all-read');
+ } catch (error) {
+ console.error('Erreur lors du marquage de toutes les notifications comme lues:', error);
+ }
}
/**
* Créer une nouvelle notification
- * TODO: Implement with proper API service method
*/
async createNotification(notification: Omit): Promise {
- console.log('TODO: Implement createNotification', notification);
- return {
- ...notification,
- id: Math.random().toString(36).substring(2, 11),
- date: new Date(),
- lu: false
- };
+ try {
+ const response = await apiService.post('/notifications', notification);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la création de la notification:', error);
+ return {
+ ...notification,
+ id: Math.random().toString(36).substring(2, 11),
+ date: new Date(),
+ lu: false
+ };
+ }
}
/**
* Supprimer une notification
- * TODO: Implement with proper API service method
*/
async deleteNotification(notificationId: string): Promise {
- console.log('TODO: Implement deleteNotification', notificationId);
- return Promise.resolve();
+ try {
+ await apiService.delete(`/notifications/${notificationId}`);
+ } catch (error) {
+ console.error('Erreur lors de la suppression de la notification:', error);
+ }
}
/**
* Récupérer les statistiques des notifications
- * TODO: Implement with proper API service method
*/
async getNotificationStats(): Promise {
- return this.getMockNotificationStats();
+ try {
+ const response = await apiService.get('/notifications/stats');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des statistiques:', error);
+ return this.getMockNotificationStats();
+ }
}
/**
* Diffuser une notification à plusieurs utilisateurs
- * TODO: Implement with proper API service method
*/
async broadcastNotification(notification: {
type: 'info' | 'warning' | 'success' | 'error';
@@ -104,8 +128,11 @@ class NotificationService {
userIds?: string[];
roles?: string[];
}): Promise {
- console.log('TODO: Implement broadcastNotification', notification);
- return Promise.resolve();
+ try {
+ await apiService.post('/notifications/broadcast', notification);
+ } catch (error) {
+ console.error('Erreur lors de la diffusion de la notification:', error);
+ }
}
/**
diff --git a/services/userService.ts b/services/userService.ts
index d87a83f..445cfd3 100644
--- a/services/userService.ts
+++ b/services/userService.ts
@@ -1,4 +1,4 @@
-// import { apiService } from './api'; // TODO: Use when implementing real API calls
+import { apiService } from './api';
import type { User } from '../types/auth';
import { UserRole } from '../types/auth';
@@ -40,93 +40,130 @@ interface UserActivity {
class UserService {
/**
* Récupérer tous les utilisateurs
- * TODO: Implement with proper API service method
*/
async getAllUsers(): Promise {
- return this.getMockUsers();
+ try {
+ const response = await apiService.get('/users');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des utilisateurs:', error);
+ return this.getMockUsers();
+ }
}
/**
* Récupérer un utilisateur par ID
- * TODO: Implement with proper API service method
*/
async getUserById(id: string): Promise {
- const users = this.getMockUsers();
- const user = users.find(u => u.id === id);
- if (!user) throw new Error('User not found');
- return user;
+ try {
+ const response = await apiService.get(`/users/${id}`);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération de l\'utilisateur:', error);
+ const users = this.getMockUsers();
+ const user = users.find(u => u.id === id);
+ if (!user) throw new Error('User not found');
+ return user;
+ }
}
/**
* Créer un nouvel utilisateur
- * TODO: Implement with proper API service method
*/
async createUser(userData: CreateUserRequest): Promise {
- console.log('TODO: Implement createUser', userData);
- return {
- id: Math.random().toString(36).substring(2, 11),
- email: userData.email,
- nom: userData.nom,
- prenom: userData.prenom,
- username: userData.email,
- role: userData.role,
- roles: [userData.role],
- permissions: [],
- entreprise: userData.entreprise,
- siret: userData.siret,
- secteurActivite: userData.secteurActivite,
- actif: true,
- status: 'ACTIVE' as any,
- dateCreation: new Date(),
- dateModification: new Date(),
- isAdmin: false,
- isManager: false,
- isEmployee: false,
- isClient: false
- };
+ try {
+ const response = await apiService.post('/users', userData);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la création de l\'utilisateur:', error);
+ // Fallback vers mock en cas d'erreur
+ return {
+ id: Math.random().toString(36).substring(2, 11),
+ email: userData.email,
+ nom: userData.nom,
+ prenom: userData.prenom,
+ username: userData.email,
+ role: userData.role,
+ roles: [userData.role],
+ permissions: [],
+ entreprise: userData.entreprise,
+ siret: userData.siret,
+ secteurActivite: userData.secteurActivite,
+ actif: true,
+ status: 'ACTIVE' as any,
+ dateCreation: new Date(),
+ dateModification: new Date(),
+ isAdmin: false,
+ isManager: false,
+ isEmployee: false,
+ isClient: false
+ };
+ }
}
/**
* Mettre à jour un utilisateur
- * TODO: Implement with proper API service method
*/
async updateUser(id: string, userData: UpdateUserRequest): Promise {
- console.log('TODO: Implement updateUser', id, userData);
- const user = await this.getUserById(id);
- return { ...user, ...userData };
+ try {
+ const response = await apiService.put(`/users/${id}`, userData);
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la mise à jour de l\'utilisateur:', error);
+ const user = await this.getUserById(id);
+ return { ...user, ...userData };
+ }
}
/**
* Supprimer un utilisateur
- * TODO: Implement with proper API service method
*/
async deleteUser(id: string): Promise {
- console.log('TODO: Implement deleteUser', id);
- return Promise.resolve();
+ try {
+ await apiService.delete(`/users/${id}`);
+ } catch (error) {
+ console.error('Erreur lors de la suppression de l\'utilisateur:', error);
+ throw error;
+ }
}
/**
* Récupérer les gestionnaires de projet
- * TODO: Implement with proper API service method
*/
async getGestionnaires(): Promise {
- return this.getMockGestionnaires();
+ try {
+ const response = await apiService.get('/users/gestionnaires');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des gestionnaires:', error);
+ return this.getMockGestionnaires();
+ }
}
/**
* Récupérer les statistiques utilisateurs
- * TODO: Implement with proper API service method
*/
async getUserStats(): Promise {
- return this.getMockUserStats();
+ try {
+ const response = await apiService.get('/users/stats');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des statistiques:', error);
+ return this.getMockUserStats();
+ }
}
/**
* Récupérer l'activité récente des utilisateurs
- * TODO: Implement with proper API service method
*/
async getUserActivity(): Promise {
- return this.getMockUserActivity();
+ try {
+ const response = await apiService.get('/users/activity');
+ return response.data;
+ } catch (error) {
+ console.error('Erreur lors de la récupération de l\'activité:', error);
+ return this.getMockUserActivity();
+ }
}
/**