Hooks Quotes/Invoices - Documentation Technique
Date de mise à jour : 2026-01-27
Statut : ✅ Implémenté et testé
Note : Ce document décrit l'architecture actuelle des hooks quotes/invoices après le refactoring complet.
Pour l'historique du refactoring, voir le document interneQUOTES_INVOICES_HOOKS_REFACTORING.md(non publié sur le site de documentation).
Vue d'ensemble
Les hooks useQuotesInvoices gèrent les devis (quotes) et factures (invoices) avec support de trois contextes :
- Global : Tous les documents de l'utilisateur (
/quotes,/invoices) - Session : Documents liés à une session (
/sessions/:sessionId/quotes-invoices/...) - Contact : Documents liés directement à un contact (filtrés côté client)
Architecture
Fichier principal
frontend/src/client/quotes-invoices/useQuotesInvoices.ts
Conventions de clés React Query
// Liste globale
["quotes", "all"]
["invoices", "all"]
// Liste paginée
["quotes", "all", "paginated", params]
["invoices", "all", "paginated", params]
// Par session
["quotes", sessionId]
["invoices", sessionId]
// Par contact (pour invalidations)
["quotes", "contact", contactId]
["invoices", "contact", contactId]
// Document spécifique (toujours global maintenant)
["quotes", "global", id]
["invoices", "global", id]
Hooks disponibles
Quotes
Lecture - Contexte Global
useAllQuotes()- Toutes les quotes de l'utilisateur (endpoint/quotes)useAllQuotesPaginated(params)- Quotes paginées (endpoint/quotes?page=...)useQuote(id)- Une quote spécifique (endpoint global/quotes/:id, fonctionne pour toutes les quotes)
Lecture - Contexte Session
useSessionQuotes(sessionId)- Quotes d'une session spécifique (endpoint/sessions/:sessionId/quotes-invoices/quotes)
Mutations
useCreateQuote()- Créer une quote (session ou global selonsessionId)useUpdateQuote()- Mettre à jour une quoteuseDeleteQuote()- Supprimer une quote (nécessitesessionId)usePreviewQuoteEmail()- Prévisualiser l'email d'une quoteuseSendQuoteEmail()- Envoyer une quote par email
Invoices
Lecture - Contexte Global
useAllInvoices()- Toutes les invoices de l'utilisateur (endpoint/invoices)useAllInvoicesPaginated(params)- Invoices paginées (endpoint/invoices?page=...)useInvoice(id)- Une invoice spécifique (endpoint global/invoices/:id, fonctionne pour toutes les invoices)
Lecture - Contexte Session
useSessionInvoices(sessionId)- Invoices d'une session spécifique (endpoint/sessions/:sessionId/quotes-invoices/invoices)
Mutations
useCreateInvoice()- Créer une invoice (session ou global selonsessionId)useUpdateInvoice()- Mettre à jour une invoiceuseDeleteInvoice()- Supprimer une invoice (nécessitesessionId)usePreviewInvoiceEmail()- Prévisualiser l'email d'une invoiceuseSendInvoiceEmail()- Envoyer une invoice par email
Fonctions utilitaires
// Filtrer par contact (côté client)
filterQuotesByContact(quotes: Quote[], contactId: string): Quote[]
filterInvoicesByContact(invoices: Invoice[], contactId: string): Invoice[]
// Invalidations
invalidateQuotes(queryClient: QueryClient, context?: { quoteId?: string; sessionId?: string; contactId?: string })
invalidateInvoices(queryClient: QueryClient, context?: { invoiceId?: string; sessionId?: string; contactId?: string })
Invalidations
Les fonctions d'invalidation gèrent automatiquement tous les contextes :
// Invalide les queries globales, par session, et par contact si nécessaire
invalidateQuoteQueries(queryClient, {
quoteId?: string,
sessionId?: string | null,
contactId?: string | null
})
Comportement :
- Toujours invalide les clés globales (
["quotes"],["quotes", "all"],["quotes", "all", "paginated"]) - Si
sessionIdfourni : invalide["quotes", sessionId]et["quotes", sessionId, quoteId]siquoteIdfourni - Si
contactIdfourni : invalide["quotes", "contact", contactId]et la clé detail siquoteIdfourni - Même logique pour
invalidateInvoiceQueries
Exemples d'utilisation
Créer une quote liée à un contact
const createQuote = useCreateQuote();
createQuote.mutate({
data: {
title: "Devis personnalisé",
amount: 1000,
contact_id: "contact-123", // Pas de session_id
},
// Pas de sessionId, utilise l'endpoint global /quotes
});
Créer une quote liée à une session
const createQuote = useCreateQuote();
createQuote.mutate({
data: { title: "Devis session", amount: 500 },
sessionId: "session-123", // Utilise /sessions/:sessionId/quotes-invoices/quotes
});
Filtrer les quotes d'un contact
// Utiliser le hook global et filtrer côté client
const { data: allQuotes = [] } = useAllQuotes();
const relatedQuotes = useMemo(
() => filterQuotesByContact(allQuotes, contactId),
[allQuotes, contactId]
);
Utiliser les hooks de session
// Dans un onglet de session
const { data: sessionQuotes = [], isLoading } = useSessionQuotes(sessionId);
const { data: sessionInvoices = [] } = useSessionInvoices(sessionId);
Récupérer une quote/invoice spécifique
// Utilisation simplifiée - plus besoin de passer sessionId/contactId
const { data: quote } = useQuote(quoteId);
// Les données de session et contact sont dans la quote elle-même
const sessionId = quote?.session_id;
const contactId = quote?.contact_id;
Invalider après une mise à jour
const updateQuote = useUpdateQuote();
// La mutation invalide automatiquement le cache en fonction du contexte
// (sessionId et contactId sont extraits de la réponse de la quote)
updateQuote.mutate({
id: "quote-123",
sessionId: "session-123", // ou undefined pour global
data: { title: "Nouveau titre" },
});
Tests
Tests unitaires disponibles dans : frontend/src/__tests__/quotes-invoices.hooks.spec.ts
Couverture :
- ✅ Appels API (GET, POST, PUT, DELETE) pour quotes et invoices
- ✅ Fonctions de filtrage par contact
- ✅ Fonctions d'invalidation dans tous les contextes
Navigation et Routes
Principe de navigation par entité
Depuis janvier 2026, chaque entité (quotes, invoices, contracts) a son propre scope de navigation indépendant :
- Routes de vue :
/quotes/:id,/invoices/:id,/contracts/:id- Pas de paramètres de recherche - Routes d'édition :
/quotes/:id/edit,/invoices/:id/edit,/contracts/:id/edit- Pas de paramètres de recherche - Routes de création :
/quotes/new?sessionId=...&contactId=...- Paramètres de recherche pour pré-remplir les formulaires uniquement
Récupération des données
Les données de session et contact sont récupérées depuis l'entité elle-même :
// Dans QuoteViewPage ou InvoiceViewPage
const { data: quote } = useQuote(quoteId);
const sessionId = quote?.session_id || null;
const contactId = quote?.contact_id || null;
// Charger session et contact depuis les données de l'entité
const { data: session } = useSession(sessionId || "", { enabled: !!sessionId });
const { data: contact } = useContact(contactId || "", { enabled: !!contactId });
Breadcrumb
Le breadcrumb pointe toujours vers la liste de l'entité, pas vers le contexte parent :
/quotes/:id→ Breadcrumb : Dashboard > Quotes > [Quote Title]/invoices/:id→ Breadcrumb : Dashboard > Invoices > [Invoice Title]/contracts/:id→ Breadcrumb : Dashboard > Contracts > [Contract Title]
Le bouton "back" retourne toujours vers la liste de l'entité (/quotes, /invoices, /contracts).
Notes techniques
- Endpoints :
useQuoteetuseInvoiceutilisent toujours les endpoints globaux (/quotes/:id,/invoices/:id) qui fonctionnent pour toutes les entités, qu'elles soient liées à une session ou à un contact - Cache : Les invalidations sont complètes et gèrent tous les contextes simultanément (nouvelles clés uniformisées + rétrocompatibilité)
- Contact : Pas d'endpoint backend dédié par contact, le filtrage se fait côté client avec
filterQuotesByContact/filterInvoicesByContact - Navigation : Chaque entité a son propre scope de navigation, indépendant du contexte session/contact
- Exports CSV/PDF : Les exports utilisent
fetchdirect (pas React Query) pour éviter de polluer le cache - Code deprecated : Les hooks deprecated (
useQuotes,useInvoices) ont été supprimés. Utilisez toujours les hooks recommandés (useAllQuotes,useSessionQuotes,useAllInvoices,useSessionInvoices)