import { useEffect } from "react"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; import { Banknote, ArrowRight, Lock, Loader2, Trash2, CheckCircle2, AlertCircle, } from "lucide-react"; import { Button, Card, Chip } from "@rubis/ui"; import { useSubscription } from "@/lib/billing"; import { useBankConnections, useBankingSettings, useBankingStatus, useDisconnectBank, useInitBanking, useUpdateBankingSettings, type BankConnection, type BankingReconciliationMode, } from "@/lib/banking"; /** * BankingSection — carte Banque dans /parametres. * * 3 états visuels : * - Free : upsell card (CTA "Passer au plan Pro") * - Pro/Business + 0 banque : carte vide avec CTA "Connecter une banque" * - Pro/Business + 1 banque : carte connectée (logo, accounts, bouton * déconnecter, toggle mode de réconciliation) * * Détection ?banking=connected|error dans l'URL : affiche un toast et * nettoie l'URL (replace, pas de history entry). Évite le toast en * boucle si on F5. */ type BankingSectionProps = { /** Params lus depuis la route parent (cf. parametres.tsx → searchSchema). */ callbackStatus?: "connected" | "error"; callbackReason?: string; }; export function BankingSection({ callbackStatus, callbackReason, }: BankingSectionProps) { const { data: status } = useBankingStatus(); const { data: sub } = useSubscription(); const isPaid = sub?.plan === "pro" || sub?.plan === "business"; const navigate = useNavigate(); const connectionsQuery = useBankConnections({ enabled: status?.enabled === true && isPaid, }); // Toast post-callback : on lit la search bag et on nettoie l'URL. useEffect(() => { if (!callbackStatus) return; if (callbackStatus === "connected") { toast.success("Banque connectée. Récupération des comptes en cours."); void connectionsQuery.refetch(); } else { toast.error( callbackReason ? `Connexion impossible (${callbackReason}). Réessayez.` : "Connexion impossible. Réessayez.", ); } // Nettoie l'URL pour ne pas re-déclencher le toast au refresh / nav. void navigate({ to: "/parametres", search: {}, replace: true, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [callbackStatus, callbackReason]); if (!isPaid) { return ; } return ( ); } // -------------------------------------------------------------------- // Upsell (Free) — visuel cohérent avec la section Marque (Business) // -------------------------------------------------------------------- function UpsellCard() { return (

Plan Pro ou Business requis

Connectez votre banque pour détecter automatiquement les factures payées et arrêter les relances inutiles.

); } // -------------------------------------------------------------------- // Vue Pro/Business // -------------------------------------------------------------------- function BankingPaidView({ isLoading, connections, }: { isLoading: boolean; connections: BankConnection[]; }) { const active = connections.find((c) => c.state !== "revoked"); if (isLoading) { return ( ); } return (
{active ? ( ) : ( )}
); } // -------------------------------------------------------------------- // CTA "Connecter une banque" // -------------------------------------------------------------------- function ConnectCta() { const initMutation = useInitBanking(); const onConnect = async () => { try { const { webviewUrl } = await initMutation.mutateAsync(); // Full nav vers Powens — pas un popup, parce que le retour fait // un redirect 302 via le tunnel API. window.location.href = webviewUrl; } catch (err) { const msg = err instanceof Error ? err.message : "Connexion impossible. Réessayez."; toast.error(msg); } }; return (

Aucune banque connectée

Rubis lit vos virements entrants pour détecter les factures payées. Lecture seule. Aucun déplacement de fonds.

); } // -------------------------------------------------------------------- // Carte "Banque connectée" // -------------------------------------------------------------------- function ConnectedBankCard({ connection }: { connection: BankConnection }) { const disconnect = useDisconnectBank(); const onDisconnect = async () => { if (!window.confirm(`Déconnecter ${connection.bankName} ?`)) return; try { await disconnect.mutateAsync(connection.id); toast.success("Banque déconnectée."); } catch { toast.error("Déconnexion impossible. Réessayez."); } }; return (

{connection.bankName}

{connection.lastSyncAt && ( · synchronisé {formatRelative(connection.lastSyncAt)} )}

{connection.lastError && (
{connection.lastError}
)}
    {connection.accounts.length === 0 ? (
  • Aucun compte récupéré pour cette banque.
  • ) : ( connection.accounts.map((a) => (
  • {a.name}

    {a.ibanMasked && (

    {a.ibanMasked}

    )}
    {a.balanceCents !== null && (

    {(a.balanceCents / 100).toLocaleString("fr-FR", { style: "currency", currency: a.currency, })}

    )}
  • )) )}
); } function StateBadge({ state }: { state: string }) { if (state === "active") { return ( ); } return ( ); } // -------------------------------------------------------------------- // Toggle mode de réconciliation // -------------------------------------------------------------------- function ReconciliationModeToggle() { const { data, isLoading } = useBankingSettings(); const mutate = useUpdateBankingSettings(); if (isLoading || !data) return null; const current = data.reconciliationMode; const setMode = (mode: BankingReconciliationMode) => { if (mode === current) return; void mutate.mutateAsync(mode).then( () => toast.success(mode === "auto" ? "Mode automatique activé." : "Mode manuel activé."), () => toast.error("Mise à jour impossible. Réessayez."), ); }; return (

Réconciliation

Quand un virement matche une facture en attente, Rubis peut soit vous suggérer le match, soit la marquer payée automatiquement.

setMode("manual")}> Manuelle — je valide chaque match setMode("auto")}> Automatique — match exact = facture payée
); } // -------------------------------------------------------------------- // Helpers // -------------------------------------------------------------------- function formatRelative(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const min = Math.round(diff / 60_000); if (min < 1) return "à l'instant"; if (min < 60) return `il y a ${min} min`; const h = Math.round(min / 60); if (h < 24) return `il y a ${h} h`; const d = Math.round(h / 24); return `il y a ${d} j`; }