- {email ? (
-
-
-
- De :{" "}
- {email.from.name} <{email.from.email}>
-
-
- À :{" "}
- {email.to.name ? `${email.to.name} — ` : ""}
- {email.to.email}
-
- {email.replyTo && (
-
- Reply-To :{" "}
- {email.replyTo}
-
- )}
-
- {email.subject}
-
-
-
-
- ) : (
-
- Chargement de l'email…
-
+ {/* Body — change selon l'étape */}
+
+ {/* Card facture — toujours visible : contexte + lien vers la fiche */}
+
+
+ {step === "ask" && (
+
setStep("email")}
+ />
)}
+ {step === "email" && }
+ {step === "paid" && }
- {/* Footer */}
-
-
- {remaining > 0
- ? `${remaining} autre${remaining > 1 ? "s" : ""} email${remaining > 1 ? "s" : ""} à voir`
- : "Cliquez pour reprendre la démo"}
-
-
-
-
-
+ {/* Footer — uniquement à l'étape email/paid (l'étape ask a ses propres boutons) */}
+ {step !== "ask" && (
+
+
+ {remaining > 0
+ ? `${remaining} autre${remaining > 1 ? "s" : ""} event${remaining > 1 ? "s" : ""} en file`
+ : "Cliquez pour reprendre l'horloge"}
+
+
+
+ )}
>
);
}
+
+/** Étape 1 — la question. */
+function AskStep({
+ isRelance,
+ marking,
+ onYes,
+ onNo,
+}: {
+ isRelance: boolean;
+ marking: boolean;
+ onYes: () => void;
+ onNo: () => void;
+}) {
+ return (
+
+
+
+ Avant d'envoyer
+
+
+ Avez-vous été payé sur cette facture ?
+
+
+ {isRelance
+ ? "Rubis est sur le point de relancer votre client. Si la facture vient d'être réglée, on évite l'email inutile et on encaisse +1 rubis."
+ : "Rubis s'apprête à vous demander confirmation. Si vous savez déjà qu'elle est payée, on saute cette étape."}
+
+
+
+
+
+
+
+
+
+ En conditions réelles, l'email de confirmation pose cette question au
+ client final. Ici on raccourcit pour la démo.
+
+
+ );
+}
+
+/**
+ * Card de contexte facture — visible en permanence dans le slide-over.
+ * Cliquable : ouvre la fiche facture dans la vraie app (ferme la slide,
+ * l'horloge reste en pause pour qu'on puisse revenir).
+ */
+function InvoiceCard({
+ invoice,
+ fallbackNumero,
+ onNavigate,
+}: {
+ invoice: InvoiceDetail | null;
+ fallbackNumero: string;
+ onNavigate: () => void;
+}) {
+ if (!invoice) {
+ return (
+
+
+ Chargement de la facture {fallbackNumero}…
+
+
+ );
+ }
+
+ const dueLabel = formatDueDelta(invoice.dueDate);
+ const isLate = isOverdue(invoice.dueDate) && invoice.status !== "paid";
+
+ return (
+
+ {/* Header : numéro + statut + flèche externe */}
+
+
+
+
+
+ {invoice.numero}
+
+
+
{invoice.clientName}
+
+
+
+
+
+
+
+ {/* Montant en gros + dates */}
+
+
+ {formatEuros(invoice.amountTtcCents)}
+
+
+
+
+
+ Émise {formatDate(invoice.issueDate)} · échue{" "}
+ {formatDate(invoice.dueDate)}
+
+
+
+ {dueLabel}
+
+
+
+
+ {invoice.planName && (
+
+ Plan : {invoice.planName}
+
+ )}
+
+ );
+}
+
+/** Étape 2A — preview de l'email envoyé (réponse "Non"). */
+function EmailStep({ email }: { email: DemoCapturedEmail | null }) {
+ if (!email) {
+ return (
+
+ Chargement de l'email…
+
+ );
+ }
+ return (
+
+
+
+ De :{" "}
+ {email.from.name} <{email.from.email}>
+
+
+ À :{" "}
+ {email.to.name ? `${email.to.name} — ` : ""}
+ {email.to.email}
+
+ {email.replyTo && (
+
+ Reply-To :{" "}
+ {email.replyTo}
+
+ )}
+
+ {email.subject}
+
+
+
+
+ );
+}
+
+/** Étape 2B — confirmation paiement (réponse "Oui"). */
+function PaidStep({ invoiceNumero }: { invoiceNumero: string }) {
+ return (
+
+
+
+
+
+
+ Encaissée
+
+
+ Facture {invoiceNumero}{" "}
+ marquée payée
+
+
+ Les relances futures de cette facture sont annulées
+ automatiquement. Pas d'email inutile envoyé à votre client.
+ Vous gagnez{" "}
+ +1 rubis pour les 10 minutes
+ que Rubis vient de vous économiser.
+
+
+
+ Regardez le dashboard derrière : le compteur a déjà bougé.
+
+
+ );
+}
diff --git a/apps/web/src/components/factures/PdfPreview.tsx b/apps/web/src/components/factures/PdfPreview.tsx
index 8bef192..07c0c54 100644
--- a/apps/web/src/components/factures/PdfPreview.tsx
+++ b/apps/web/src/components/factures/PdfPreview.tsx
@@ -4,24 +4,22 @@ import { api, ApiError } from "@/lib/api";
import { cn } from "@/lib/utils";
/**
- * Aperçu du fichier importé (PDF / image) côté review OCR.
+ * Aperçu du fichier importé (PDF / image) — utilisé sur :
+ * - la review OCR (volet gauche, source = batch + draft)
+ * - la fiche facture (source = invoice id direct)
*
- * V1 = placeholder hardcoded (`
` sans batchId)
- * → on n'avait jamais branché le streaming MinIO. Maintenant on fetch
- * GET /invoices/import-batch/:id/drafts/:draftId/pdf via api.fetchBlob
- * (Bearer auto-injecté), on crée un object URL, et on l'affiche dans :
- * -
+