diff --git a/apps/app/components/crm/company-siren-field.tsx b/apps/app/components/crm/company-siren-field.tsx index 044fea0f6..7734e0555 100644 --- a/apps/app/components/crm/company-siren-field.tsx +++ b/apps/app/components/crm/company-siren-field.tsx @@ -1,36 +1,40 @@ "use client"; -import Renew from "@carbon/icons-react/es/Renew"; -import Search from "@carbon/icons-react/es/Search"; import { Badge } from "@crm/ui/components/badge"; import { Button } from "@crm/ui/components/button"; -import { Icon } from "@crm/ui/components/icon"; +import { EmptyCellValue } from "@crm/ui/components/empty-cell"; +import { Input } from "@crm/ui/components/input"; import { Spinner } from "@crm/ui/components/spinner"; import { cn } from "@crm/ui/lib/utils"; import { useMutation } from "@tanstack/react-query"; -import { useState } from "react"; +import { useId, useState } from "react"; import { toast } from "sonner"; import { PROPERTY_LABEL, PROPERTY_ROW } from "@/components/detail-sheet"; import { useCrmCache } from "@/lib/trpc/cache"; import { useTRPC } from "@/lib/trpc/client"; -import type { RouterOutputs } from "@/lib/trpc/types"; -type ResolveResult = RouterOutputs["companies"]["resolveSiren"]; -type SirenCandidate = Extract< - ResolveResult, - { outcome: "ok" } ->["candidates"][number]; +const ROW = cn(PROPERTY_ROW, "items-center"); +const CONTROL = + "h-8 w-full justify-start px-2 font-normal hover:border-input hover:bg-muted/40 border border-transparent"; function formatSiren(siren: string): string { return siren.replace(/(\d{3})(?=\d)/g, "$1 ").trim(); } +function isValidSirenDraft(value: string): boolean { + return value.length === 9 && /^\d+$/.test(value); +} + /** - * Affiche le SIREN d une company, ou propose de le resoudre via - * VigieProcure. La resolution est une mutation (jamais un effet declenche - * au chargement) -- le clic explicite reste le seul declencheur, y compris - * pour l'ecriture automatique sur candidat unique "exact" (doctrine - * validee par Franck : exception ciblee et reversible, pas un backfill). + * Affiche le SIREN d une company, et permet de le saisir/corriger a la + * main. La resolution automatique via la base SIRENE entreprises a ete + * retiree : les comptes du CRM sont souvent des entites publiques + * (mairies, hopitaux) absentes de cette base, et la resolution pouvait + * matcher une entite homonyme sans rapport puis l'ecrire sans validation + * humaine (decision Franck). Le SIREN est desormais un champ inline + * editable comme les autres champs de la fiche Company, avec une garde + * cote client (9 chiffres) avant l'appel serveur, qui reste la source de + * verite (regex stricte dans companies.contracts.ts). */ export function CompanySirenField({ companyId, @@ -41,8 +45,9 @@ export function CompanySirenField({ }) { const trpc = useTRPC(); const cache = useCrmCache(); - const [candidates, setCandidates] = useState(null); - const [notConfigured, setNotConfigured] = useState(false); + const id = useId(); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(siren ?? ""); const setSirenMutation = useMutation( trpc.companies.setSiren.mutationOptions({ @@ -51,7 +56,7 @@ export function CompanySirenField({ toast.error(result.reason); return; } - setCandidates(null); + setEditing(false); await cache.company(companyId, { settle: "record" }); toast.success(`SIREN ${formatSiren(result.siren)} enregistre.`); }, @@ -59,148 +64,76 @@ export function CompanySirenField({ }), ); - const resolve = useMutation( - trpc.companies.resolveSiren.mutationOptions({ - onSuccess: (result) => { - if (result.outcome === "not-configured") { - setNotConfigured(true); - return; - } - if (result.outcome === "unauthorized" || result.outcome === "failed") { - toast.error(result.reason); - return; - } - - if (result.candidates.length === 0) { - toast("Aucun SIREN trouve pour cette fiche."); - return; - } - - const [only, ...rest] = result.candidates; - if (only && rest.length === 0 && only.confidence === "exact") { - setSirenMutation.mutate({ id: companyId, siren: only.siren }); - return; - } + const submit = () => { + if (!isValidSirenDraft(draft)) return; + setSirenMutation.mutate({ id: companyId, siren: draft }); + }; - setCandidates(result.candidates); - }, - onError: (error) => toast.error(error.message), - }), - ); - - if (siren) { + if (editing) { return ( -
- SIREN - {formatSiren(siren)} -
- ); - } - - return ( -
- SIREN -
- {notConfigured ? ( - - Resolution SIREN non configuree sur cette installation. - - ) : ( +
+ +
+ + setDraft(event.target.value.replace(/\D/g, "").slice(0, 9)) + } + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + submit(); + } + if (event.key === "Escape") { + setDraft(siren ?? ""); + setEditing(false); + } + }} + /> - )} - - {candidates ? ( - - setSirenMutation.mutate({ - id: companyId, - siren: candidate.siren, - }) - } - onRetry={() => resolve.mutate({ id: companyId })} - /> - ) : null} +
-
- ); -} + ); + } -function SirenCandidateList({ - candidates, - saving, - onPick, - onRetry, -}: { - candidates: SirenCandidate[]; - saving: boolean; - onPick: (candidate: SirenCandidate) => void; - onRetry: () => void; -}) { return ( -
-
- - {candidates.length === 1 - ? "1 correspondance possible" - : `${candidates.length} correspondances possibles`} - - -
- {candidates.map((candidate) => ( - - ))} + )} +
); }