// StarterAccountsModal — one-shot opt-in modal proposing 4 starter accounts // (Compte chèque, CELI, REER, Compte non-enregistré) to existing profiles // when they first land on /balance. Issue #179. // // Behavior: // - 4 checkboxes default-checked. // - Collision rule (case-insensitive trim name + same category): the // matching checkbox is disabled and uncheckable; tooltip explains why. // - "Ajouter les comptes sélectionnés" → atomic BEGIN/COMMIT INSERT, then // onClose(insertedIds). // - "Plus tard" → no INSERT, onClose([]). // - Parent owns isOpen state and writes user_preferences.balance_starter_proposed // in onClose so the modal never re-appears. import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { X, Loader2 } from "lucide-react"; import { STARTER_ACCOUNTS, getStarterCollisions, proposeStarterAccounts, } from "../../services/balance.service"; export interface StarterAccountsModalProps { /** Parent guard — modal renders only when true. */ isOpen: boolean; /** * Fired in both branches (confirm + dismiss). The parent uses the returned * ids to write `user_preferences.balance_starter_proposed` so the modal * never re-appears, regardless of which branch was taken. */ onClose: (acceptedIds: number[]) => void; } export default function StarterAccountsModal({ isOpen, onClose, }: StarterAccountsModalProps) { const { t } = useTranslation(); const [collisions, setCollisions] = useState>(new Set()); const [selected, setSelected] = useState>( () => new Set(STARTER_ACCOUNTS.map((s) => s.key)) ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [collisionsLoaded, setCollisionsLoaded] = useState(false); // Load collisions once when the modal opens. We pre-uncheck colliding // starters (and disable them) so the visible default-checked count matches // what would actually be inserted. useEffect(() => { if (!isOpen) return; let cancelled = false; void (async () => { try { const c = await getStarterCollisions(); if (cancelled) return; setCollisions(c); setSelected((prev) => { const next = new Set(prev); for (const k of c) next.delete(k); return next; }); setCollisionsLoaded(true); } catch { if (!cancelled) setCollisionsLoaded(true); } })(); return () => { cancelled = true; }; }, [isOpen]); if (!isOpen) return null; const toggle = (key: string) => { if (collisions.has(key)) return; setSelected((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; const handleAdd = async () => { if (submitting) return; setError(null); setSubmitting(true); try { const ids = await proposeStarterAccounts(Array.from(selected)); setSubmitting(false); onClose(ids); } catch { setSubmitting(false); setError(t("balance.starters.errors.insert")); } }; const handleLater = () => { if (submitting) return; onClose([]); }; return createPortal(

{t("balance.starters.title")}

{t("balance.starters.description")}

    {STARTER_ACCOUNTS.map((s) => { const isCollision = collisions.has(s.key); const isChecked = selected.has(s.key); return (
  • ); })}
{error && (
{error}
)}
, document.body ); }