Permet à l'user de répondre aux check-ins directement dans l'app, sans passer par les liens email. Au mount du layout `_app`, on liste les factures en `awaiting_user_confirmation` et on les présente une par une dans une modale séquentielle : - "Oui, payée" → mark paid + bonus rubis + cancel relances - "Non, en attente" → schedule relances + status → in_relance - "Plus tard" → skip session-only 3 endpoints auth-protected sous /api/v1/checkin/inapp/ (déclarés AVANT le groupe public à token sinon /:token/pending mange /inapp/pending). La modale fait toujours confiance au serveur : queue = pending refetch, display = queue[0], pas de cursor manuel — sinon on saute des factures quand le serveur retire la réponse précédente. Wording rassurant : "Aucune relance ne part sans votre validation". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "@/lib/api";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import type { InvoiceStatus } from "@rubis/shared";
|
|
|
|
/**
|
|
* Forme minimale renvoyée par GET /api/v1/checkin/inapp/pending — basée sur
|
|
* InvoiceTransformer côté API. On ne garde que ce dont la modale a besoin.
|
|
*/
|
|
export type PendingCheckinInvoice = {
|
|
id: string;
|
|
numero: string;
|
|
amountTtcCents: number;
|
|
issueDate: string;
|
|
dueDate: string;
|
|
status: InvoiceStatus;
|
|
clientName: string;
|
|
planName: string | null;
|
|
};
|
|
|
|
/** Liste des factures en attente de check-in pour l'org courante. */
|
|
export function usePendingCheckins() {
|
|
return useQuery({
|
|
queryKey: queryKeys.checkin.pending(),
|
|
queryFn: () =>
|
|
api.get<PendingCheckinInvoice[]>("/api/v1/checkin/inapp/pending"),
|
|
// Pas de polling — la liste change uniquement quand l'user répond ou
|
|
// qu'une nouvelle invoice arrive en awaiting_user_confirmation. On
|
|
// refetch sur mount + sur invalidate.
|
|
staleTime: 30_000,
|
|
});
|
|
}
|
|
|
|
/** Mutation : "oui, payée" — délègue à l'endpoint inappRespondPaid. */
|
|
export function useCheckinPaid() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (invoiceId: string) =>
|
|
api.post(`/api/v1/checkin/inapp/${invoiceId}/paid`),
|
|
onSuccess: () => {
|
|
// Tout l'écosystème dépend du statut : invalidate large (factures,
|
|
// dashboard KPIs, timeseries, pipeline, counts).
|
|
void qc.invalidateQueries({ queryKey: queryKeys.checkin.pending() });
|
|
void qc.invalidateQueries({ queryKey: queryKeys.invoices.all() });
|
|
void qc.invalidateQueries({ queryKey: queryKeys.dashboard.all() });
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Mutation : "non, toujours impayée" — programme les relances. */
|
|
export function useCheckinStillPending() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (invoiceId: string) =>
|
|
api.post(`/api/v1/checkin/inapp/${invoiceId}/pending`),
|
|
onSuccess: () => {
|
|
void qc.invalidateQueries({ queryKey: queryKeys.checkin.pending() });
|
|
void qc.invalidateQueries({ queryKey: queryKeys.invoices.all() });
|
|
void qc.invalidateQueries({ queryKey: queryKeys.dashboard.all() });
|
|
},
|
|
});
|
|
}
|