// screens.jsx — écrans du parcours (hors Run et Confirm) // // CÂBLAGE (2026-08-07, passe « enlever le static ») : plus AUCUNE donnée de // démonstration. Chaque écran lit le tenant et n'affiche que ce qu'il sait ; // vide quand il ne sait rien. Règle appliquée partout : un champ visible est // un champ PERSISTÉ — un champ qu'on saisit et qui se perd en silence est un // mensonge d'interface. (Revenus nets et bénéfice ont été retirés : la base // n'a pas encore de colonne pour eux.) // Langue : chrome via I18N.t(), noms du métier via le lexique du domaine. const { useState: useStateS, useEffect: useEffectS } = React; /* ============ CONNEXION ============ */ function LoginScreen({ onLogin }) { const { t } = useLang(); const [email, setEmail] = useStateS(G4M.email || ""); const [password, setPassword] = useStateS(""); const [busy, setBusy] = useStateS(false); const [err, setErr] = useStateS(null); const enter = async () => { setBusy(true); setErr(null); try { // AUTH BÊTA : le mot de passe échange contre un jeton — c'est lui qui // prouve l'identité à chaque appel. Le serveur résout le tenant depuis // le compte : le navigateur ne le choisit jamais. const me = await G4M.login(email.trim(), password); if (!me || !me.tenantId) throw new Error(t("Compte inconnu ou backend injoignable :") + " tenant absent"); onLogin(); } catch (e) { setErr(e.message); } finally { setBusy(false); } }; return (
{t("Connexion sécurisée")}

{t("Bienvenue")}

{t("Connectez-vous pour reprendre vos {demandes} ou en commencer une nouvelle.")}

setEmail(e.target.value)} onKeyDown={(e) => e.key === "Enter" && enter()} /> setPassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && enter()} /> {busy ? t("Connexion…") : t("Se connecter")} {err &&

{t("Compte inconnu ou backend injoignable :")} {err}

}
{t("Authentification gérée par votre identifiant unique (OIDC)")}
{t("Hébergement et traitement au Québec")}
); } /* ============ TABLEAU DE BORD ============ */ const RUN_STATUS_UI = { done: { statut: "soumis", pct: 100 }, awaiting_review: { statut: "a_completer", pct: 85 }, awaiting_user: { statut: "a_completer", pct: 70 }, filling: { statut: "a_completer", pct: 60 }, inferring: { statut: "a_completer", pct: 40 }, parsing: { statut: "a_completer", pct: 20 }, pending: { statut: "brouillon", pct: 5 }, failed: { statut: "brouillon", pct: 0 }, }; /* Carte « Plateforme » — metering, visible du rôle admin seulement. Tous les tenants, métriques agrégées, aucun contenu client (bi.v_metering_platform). */ function PlatformCard() { const { t } = useLang(); const [rows, meta] = useG4M(() => G4M.platformUsage(), null, []); if (!meta.live || !rows || !rows.length) return null; const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + " k" : String(n)); return (
{t("Plateforme · consommation par entreprise")}
{rows.map((r, i) => ( ))}
{t("Entreprise")}{t("Mois")}{t("Runs")} {t("Unités SaaS")}{t("Questions")} {t("Mémoire (0 jeton)")}{t("Jetons LLM")}{t("Durée moy.")}
{r.companyName || r.tenantId.slice(0, 8)} {(r.month || "").slice(0, 7)} {r.runsCompleted}/{r.runs} {r.saasUnits} {r.questions} {r.memoryHits} {fmt(r.llmTokensIn)} → {fmt(r.llmTokensOut)} {Math.round(r.avgDurationMs / 1000)}s
); } function DashboardScreen({ onNew, onOpen, onFirstVisit }) { const { t, lang } = useLang(); const [runs, meta] = useG4M(() => G4M.runs(10), null, []); const [session] = useG4M(() => G4M.me(), null, []); // Premier passage d'un compte sans aucune demande : proposer le tutoriel. useEffectS(() => { if (meta.live && runs && runs.length === 0 && onFirstVisit) onFirstVisit(); }, [meta.live, runs && runs.length]); const demandes = (runs || []).map((r) => { const ui = RUN_STATUS_UI[r.status] || { statut: "brouillon", pct: 0 }; return { id: r.id, titre: r.projectTitle || t("{demande} sans titre"), programme: r.companyName || "—", statut: ui.statut, pct: typeof r.progressPct === "number" && r.progressPct ? Math.round(r.progressPct) : ui.pct, maj: r.createdAt ? new Date(r.createdAt).toLocaleDateString(lang === "en" ? "en-CA" : "fr-CA") : "—", }; }); return (
{(session && session.companyName) &&
{session.companyName}
}

{t("Vos {demandes}")}

{t("Reprenez là où vous étiez, ou lancez une nouvelle {demande}.")}

{t("Nouvelle {demande}")}
{(session && session.role === "admin") && }
e.key === "Enter" && onNew()}>
{t("Commencer une nouvelle {demande}")}
{t("Téléversez un {formulaire}. On s'occupe du reste.")}
{demandes.length === 0 && !meta.loading && (
{t("Aucune {demande} pour l'instant")}

{t("Votre première {demande} commence par le {formulaire} à remplir. Téléversez-le : on lit les champs, on remplit ce qu'on peut, et on ne vous demande que le reste.")}

)} {demandes.map((d, i) => (
e.key === "Enter" && i === 0 && onOpen()}>
{d.programme}
{d.titre}
{t("Mis à jour")} : {d.maj}
{d.statut !== "brouillon" || d.pct > 0 ? (
{d.pct}% {t("complété")}
) : null} {i === 0 && {t("Reprendre")}}
))}
); } /* ============ ÉTAPE 1 — PROFIL ENTREPRISE ============ */ function PInput({ value, onChange, mono, locked, placeholder, ariaLabel, as, options }) { const cls = "pf-input" + (mono ? " mono" : "") + (locked ? " locked" : ""); if (as === "select") { return ( ); } return onChange && onChange(e.target.value)} />; } function PField({ label, guide, required, children, span2 }) { return (
{children}
); } function CompanyScreen({ onNext }) { const { t } = useLang(); const [remote, meta] = useG4M(() => G4M.company(), null, []); const [session] = useG4M(() => G4M.me(), null, []); const [saving, setSaving] = useStateS(false); const [saveErr, setSaveErr] = useStateS(null); const caeSearch = useCaeSearch(); // On part VIDE, toujours. Le profil du tenant remplit ce qu'il sait ; le // reste attend la personne. (L'ancien mode « démonstration » pré-remplissait // l'écran avec une entreprise fictive — un clic sur Enregistrer la // persistait. Supprimé, pas conditionné.) const [c, setC] = useStateS({ raison: "", neq: "", forme: "", bn: "", constitution: "", employes: "", revenus_bruts: "", secteur: "" }); const [codes, setCodes] = useStateS([{ systeme: "CAE", code: "", libelle: "", principal: true }]); const [addr, setAddr] = useStateS(""); const [contact, setContact] = useStateS({ nom: "", titre: "", courriel: "", tel: "" }); const [needs, setNeeds] = useStateS([]); const [prodOff, setProdOff] = useStateS(""); const [growth, setGrowth] = useStateS(""); const [showMore, setShowMore] = useStateS(false); useEffectS(() => { if (!remote) return; setC({ raison: remote.legalName || "", neq: remote.neq || "", forme: remote.legalForm || "", bn: remote.bnFederal || "", constitution: remote.dateConstitution || "", employes: remote.numEmployees != null ? String(remote.numEmployees) : "", revenus_bruts: remote.annualRevenueRange || "", secteur: remote.sector || "", }); setAddr(remote.address || ""); setProdOff(remote.productsOffered || ""); setGrowth(remote.growthObjectives || ""); setNeeds(Array.isArray(remote.financingNeeds) ? remote.financingNeeds : []); const saved = Array.isArray(remote.activityCodes) ? remote.activityCodes.filter((x) => x && x.code) : []; setCodes(saved.length ? saved.map((x) => ({ systeme: x.systeme || "CAE", code: x.code || "", libelle: x.libelle || "", principal: !!x.principal })) : [{ systeme: "CAE", code: remote.scianCode || "", libelle: remote.sector || "", principal: true }]); setContact({ nom: remote.contactName || (session && session.fullName) || "", titre: remote.contactTitle || "", courriel: remote.email || (session && session.email) || "", tel: remote.contactPhone || "", }); }, [remote && remote.id, session && session.email]); const set = (k, v) => setC((o) => ({ ...o, [k]: v })); const setCode = (i, k, v) => setCodes((cs) => cs.map((x, j) => j === i ? { ...x, [k]: v } : x)); const setPrimary = (i) => setCodes((cs) => cs.map((x, j) => ({ ...x, principal: j === i }))); const addCode = () => setCodes((cs) => [...cs, { systeme: "SCIAN", code: "", libelle: "", principal: false }]); const delCode = (i) => setCodes((cs) => cs.filter((_, j) => j !== i)); const toggleNeed = (id) => setNeeds((n) => n.includes(id) ? n.filter((x) => x !== id) : [...n, id]); const save = async () => { if (!(remote && remote.id)) { setSaveErr(t("Compte inconnu ou backend injoignable :")); return; } setSaving(true); setSaveErr(null); try { const principal = codes.find((x) => x.principal) || codes[0] || {}; await G4M.saveCompany(remote.id, { legalName: c.raison, neq: c.neq, sector: c.secteur, legalForm: c.forme, bnFederal: c.bn, numEmployees: parseInt(c.employes) || null, annualRevenueRange: c.revenus_bruts, dateConstitution: c.constitution || null, address: addr || null, scianCode: principal.code || null, activityCodes: codes.filter((x) => x.code || x.libelle), financingNeeds: needs, productsOffered: prodOff || null, growthObjectives: growth || null, contactNom: contact.nom || null, contactTitre: contact.titre || null, contactCourriel: contact.courriel || null, contactTel: contact.tel || null, }); } catch (e) { setSaveErr(e.message); setSaving(false); return; } setSaving(false); onNext(); }; return (

{t("Ces informations serviront à remplir ")}{t("tous vos {formulaires}")}{t(" : ce que vous saisissez ici, on ne vous le redemandera plus. ")} {t("Vous ne savez pas où trouver une information ?")} {t("Cliquez le « ? » à côté du champ.")}

{t("Ce profil ne se remplit ")}{t("qu'une fois")}{t(". Il sert à toutes vos {demandes} : plus vous le complétez, mieux on cible les bons {programmes} et plus le remplissage est précis.")}

{/* --- Identité --- */}
1 {t("Identité de l'entreprise")}
set("raison", v)} ariaLabel={t("Raison sociale")} /> set("neq", v)} ariaLabel="NEQ" /> {t("Tel qu'enregistré à votre dossier. Corrigez-le au besoin.")} set("forme", v)} ariaLabel={t("Statut juridique")} /> set("bn", v)} ariaLabel={t("Numéro d'entreprise fédéral (NE / ARC)")} /> {/* Une vraie date, pas une année : le moteur de dates refuse de deviner un mois. AAAA-MM-JJ est le format pivot. */} set("constitution", v)} ariaLabel={t("Date de constitution (AAAA-MM-JJ)")} /> set("employes", v)} ariaLabel={t("Nombre d'employés")} /> set("revenus_bruts", v)} ariaLabel={t("Revenus bruts (chiffre d'affaires)")} options={REVENUE_RANGES} /> set("secteur", v)} ariaLabel={t("Secteur d'activité")} />
{/* --- Adresse officielle --- */}
2 {t("Adresses")}
{t("Adresse officielle de l'entreprise")}

{t("Seule l'adresse officielle est enregistrée à votre dossier pour l'instant.")}

{/* --- Codes d'activité (multiples, persistés en jsonb) --- */}
3 {t("Codes d'activité")}

{t("Une entreprise peut en avoir plusieurs. Tapez votre activité dans le libellé : on cherche dans le référentiel officiel et le code se remplit seul. Indiquez le principal.")}

{codes.map((cd, i) => (
setCode(i, "systeme", v)} ariaLabel="Référentiel" options={CODE_SYSTEMS} /> setCode(i, "code", v)} ariaLabel="Code" /> setCode(i, "libelle", v)} onPick={(m) => setCodes((cs) => cs.map((x, j) => j === i ? { ...x, code: m.code, libelle: m.libelle, systeme: "CAE" } : x))} />
))}
{/* --- Personne-ressource (signataire) --- */}
4 {t("Personnes-ressources")}
setContact((x) => ({ ...x, nom: v }))} /> setContact((x) => ({ ...x, titre: v }))} /> setContact((x) => ({ ...x, courriel: v }))} /> setContact((x) => ({ ...x, tel: v }))} />
{t("Signataire principal de la {demande}")}
{/* --- Section facultative repliable (persistée elle aussi) --- */}
{showMore && (
{FINANCING_NEEDS.map((n) => { const on = needs.includes(n.id); return ( ); })}