Aller au contenu principal

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 interne QUOTES_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 selon sessionId)
  • useUpdateQuote() - Mettre à jour une quote
  • useDeleteQuote() - Supprimer une quote (nécessite sessionId)
  • usePreviewQuoteEmail() - Prévisualiser l'email d'une quote
  • useSendQuoteEmail() - 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 selon sessionId)
  • useUpdateInvoice() - Mettre à jour une invoice
  • useDeleteInvoice() - Supprimer une invoice (nécessite sessionId)
  • usePreviewInvoiceEmail() - Prévisualiser l'email d'une invoice
  • useSendInvoiceEmail() - 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 sessionId fourni : invalide ["quotes", sessionId] et ["quotes", sessionId, quoteId] si quoteId fourni
  • Si contactId fourni : invalide ["quotes", "contact", contactId] et la clé detail si quoteId fourni
  • 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

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 });

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

  1. Endpoints : useQuote et useInvoice utilisent 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
  2. Cache : Les invalidations sont complètes et gèrent tous les contextes simultanément (nouvelles clés uniformisées + rétrocompatibilité)
  3. Contact : Pas d'endpoint backend dédié par contact, le filtrage se fait côté client avec filterQuotesByContact / filterInvoicesByContact
  4. Navigation : Chaque entité a son propre scope de navigation, indépendant du contexte session/contact
  5. Exports CSV/PDF : Les exports utilisent fetch direct (pas React Query) pour éviter de polluer le cache
  6. Code deprecated : Les hooks deprecated (useQuotes, useInvoices) ont été supprimés. Utilisez toujours les hooks recommandés (useAllQuotes, useSessionQuotes, useAllInvoices, useSessionInvoices)