# Research: General CRM Backlog Fixes

All five items were resolved by reading the existing implementation rather than by evaluating hypothetical alternatives — this is a bugfix/small-enhancement bundle against live code, not a greenfield choice. Each section states the confirmed root cause (or existing convention) and the decision that follows from it.

## 1. Interação save behavior (US1)

**Finding**: Both interaction forms — `pages/interacoes/interacoes.php` (line ~187) and `pages/interacoes/sucesso.php` (line ~153) — hardcode `required` on `<select multiple name="profissionais">`, independent of the `efetividade` radio. Both forms' JS (`includes/js/interacoes.js:366-382` and `includes/js/sucesso.js:339-354`) already toggle `required` on `titulo`, `descricao`, `canal`, `status`, and `tipo` based on the efetividade value, but never touch `profissionais`. The backend (`includes/ajax/interacoes/create.php`, `update.php`) already loops over `$profissionais` with `if (!empty($profissional))` and does not error on an empty array — it was never the blocker.

**Decision**: Remove the hardcoded `required` attribute from the `profissionais` select in both forms, and add it to the existing `efetividade`-change handler in both `interacoes.js` and `sucesso.js`, so it is required only when `efetividade == 1` (mirroring the existing titulo/descricao/canal/status/tipo pattern exactly). Additionally default `$profissionais = $_POST['profissionais'] ?? [];` in both `create.php` and `update.php` — functionally the transaction already succeeds with zero Profissionais, but without the `?? []` default a browser that omits the field entirely (common jQuery `.serialize()` behavior for an empty multi-select) trips a PHP "foreach() argument must be of type array|object, null given" warning. Hardening this is in scope for FR-001 since it's a one-line defensive fix directly in the same code path being touched.

**Alternatives considered**: Making Profissionais conditionally required via a `data-required-when` declarative attribute read by shared form-validation JS — rejected, no such generic mechanism exists in this codebase; every other conditional-required field in this exact form already uses the same imperative `inputRequired(selector, bool)` call inside the efetividade handler, so matching that convention is both simpler and more consistent than introducing a new pattern for one field.

## 2. Infinite loading on empty results (US2)

**Finding**: `showPagina(objetos, row_callback, page, search)` is duplicated nearly verbatim in three shared JS files: `includes/js/bd.js`, `includes/js/inicio.js`, and `includes/js/cadastro.js`. All three compute `p = objetos[page - 1]` (or the searched equivalent) and then iterate `p`. `bd.js` guards with `if (p) { p.forEach(...) }`; `inicio.js` guards with `p?.forEach(...)`; **`cadastro.js` has no guard** — `p.forEach(...)` directly. When the query/search returns zero rows, `objetos` is `[]`, `p` is `undefined`, and `cadastro.js`'s copy throws `TypeError: Cannot read properties of undefined (reading 'forEach')`. This is called from inside an ajax success callback whose caller chains a `.then()` to remove the page's `loading` class (e.g. `profissionais.js:88-90`); the uncaught throw breaks that promise chain before the `.then()` runs, so the loading spinner never clears. `pagination()` itself (called after) already tolerates `total === 0` fine — it's exactly this one missing guard.

**Decision**: Patch `includes/js/cadastro.js`'s `showPagina()` to match its two already-correct siblings (`p?.forEach(...)`, or the `if (p)` form used in `bd.js` — either is proven safe in this codebase). Additionally add a "no results" empty-state row to `.objetos-table > tbody` when `total === 0`, consistently across all three copies (`bd.js`, `inicio.js`, `cadastro.js`), since none of the three currently render an explicit empty-state message today — they just silently leave the table body empty, which satisfies FR-004 (loading clears) but not FR-005 (clear "no results" state) on its own.

**Alternatives considered**: Consolidating the three duplicate `showPagina`/`pagination` implementations into one shared function — rejected for this change. It would fix the same bug and remove real duplication, but it touches every page that includes any of the three files, is a much larger blast radius than a backlog bugfix warrants, and isn't required by any FR in the spec. Flagged here as a legitimate future cleanup, not taken on in this plan.

## 3. Partial-term search on system-wide selects (US3)

**Finding**: The vast majority of searchable selects in the system go through `includes/js/choices-select.js`'s `initChoicesSelect()`, which configures Choices.js with `fuseOptions: { includeScore: true, threshold: 0.0, ignoreLocation: true }`. `ignoreLocation: true` correctly removes Fuse's default ~60-character match-location window (confirmed against Fuse.js documentation), but `threshold: 0.0` demands a near-perfect match and is the more likely source of missed partial matches — e.g., diacritics (typing "sao" for "São"), multi-word concatenated labels (`"{nomefantasia} - {razaosocial}"`), or minor formatting differences all fail at threshold 0 even though they're an obviously-intended match to a human. Separately, the small subset of selects using `data-choices-module="search"` (server-side type-ahead, e.g. `pesquisa/empresa.php`, `pesquisa/profissional.php`) already use `LIKE '%term%'` — contains-style matching — server-side, so they are not the source of the reported bug.

**Decision**: Raise `threshold` from `0.0` to a moderate value (in the ~0.3–0.4 range, tuned during implementation/manual verification per Constitution Principle VIII) while keeping `ignoreLocation: true`, so a typed substring matches anywhere in a label even with minor accent/formatting differences, without the tradeoff of true fuzzy/typo-tolerant matching that a much higher threshold would introduce. This is a single shared-config change in `choices-select.js` that benefits every select using the declarative `.choices-select-element` pattern or the manual `initChoicesSelect()` call, satisfying FR-006/FR-007 without touching any of the individual pages that use it.

**Alternatives considered**: Switching to Fuse's `useExtendedSearch` with an implicit substring (`'`) operator prepended to the user's query — rejected as unnecessarily complex for this codebase (it changes query syntax semantics, e.g. what a literal space or quote in the search term means) when a threshold adjustment achieves the same practical outcome with a one-line config change.

## 4. Profissional form: "Não sei" + número de filhos (US4)

**Finding**: `sexo`, `estado_civil`, `conjuge_sexo`, and `filho_sexo` are all bound as plain string parameters (`bind_param('s', ...)`) with no enum/lookup table constraining accepted values elsewhere in the touched files — the values currently sent (`masculino`, `feminino`, `solteiro`, `casado`, etc.) are just strings the form happens to send today. Children are currently one fixed set of columns directly on `Profissional新` (`filho`, `filho_sexo`, `filho_nome`, `filho_nascimento_ano`) driven by a single Sim/Não radio — there is no per-child repetition today, unlike the form's own "Formação" and "Endereços anteriores" sections, which already use a JS-driven "add another" pattern (`#formacoes-container`, `#enderecos-anteriores-container`) for genuinely unbounded repeated sub-records.

**Decision**: Add a "Não sei" option as a plain additional radio value (e.g. `nao_sei`) to the `sexo`, `estado_civil`, `conjuge_sexo`, and `filho_sexo` groups — no schema change needed since these are unconstrained string columns. For children, introduce a new one-to-many child table (`ProfissionalFilho新`: id, `profissional`, `nome`, `sexo`, `nascimento_ano`) and drive its UI with the same repeatable-fieldset JS pattern already proven for Formação/Endereços anteriores, replacing the single fixed `filho_*` columns. On edit, wipe the Profissional's existing `ProfissionalFilho新` rows and reinsert the submitted set — the same wipe-and-reinsert convention already used for `InteracaoProduto新`/`InteracaoProfissional新` on interaction edits, and explicitly recognized (with a flag-for-discussion caveat) by Constitution Principle X.

**Alternatives considered**: Keeping children as fixed columns and adding `filho2_*`, `filho3_*`, etc. up to some cap — rejected, hard-caps the feature exactly where the spec calls for unbounded, and doesn't match how every other genuinely-repeatable section of this same form is already modeled (child table, not repeated columns).

## 5. Cellphone-before-landline phone ordering (US5)

**Finding (superseded once, see below)**: `Telefone新` rows are looked up per-Profissional via a polymorphic `objeto`/`referencia` pair (`WHERE t.objeto = 'Profissional' AND t.referencia = ?`), ordered only by `t.id` (insertion order) — there is no column classifying a number as cellphone vs. landline anywhere in the schema. An initial pass built exactly that: a `Telefone新.tipo_linha` column populated by a Brazil-only PHP digit-length heuristic, ordered by SQL at read-time. That was reverted (see below) once a better existing pattern was found.

**Finding, revised**: `includes/js/profissionais.js` (also duplicated in `empresa.js`/`geral.js`/`home.js`) already has everything needed, client-side: `getPhoneFormat(country)` asks Google's `libphonenumber` (already vendored) for that country's real example `FIXED_LINE` and `MOBILE` numbers, and `formatPhone(cc, ddi, value)` already does `format.find(f => (f.match(/9/g) || []).length === digits.length)` — i.e. it already picks the mobile-vs-fixed template by matching the *stored* number's digit count against the two real per-country templates, purely to choose a display mask. That match is, itself, an accurate country-aware classification for any country libphonenumber knows — not just Brazil like the PHP heuristic was. There is still no server-side (PHP) equivalent, and the project has no `composer.json` at all, so a full PHP libphonenumber port remains disproportionate for this — but that's no longer a gap, because the classification the feature needs was already sitting in the client-side code, unexposed.

**Decision**: Do the classification and the cellphone-first ordering entirely client-side, in `includes/js/profissionais.js`, reusing that exact match instead of adding a PHP heuristic or any new schema:
- `isTelefoneCelular(cc, value)` — same `getPhoneFormat` + digit-count match `formatPhone` already does, returns whether the match landed on the mobile template.
- `ordenarTelefonesCelularPrimeiro(telefones)` — stable-sorts an array of `{cc, telefone}` by that check, so same-type numbers keep their original relative order (spec edge case).
- Applied at all three places a Profissional's phones are rendered: the Profissionais list column (first phone shown), the info panel (every stored number), and the edit-form repopulation on reopen (reorders `telefoneddi`/`telefone`/`telefonetipo` together, by the same permutation, before the rest of that flow consumes them).

This reverses the write-time/schema approach entirely: `Telefone新.tipo_linha`, its two migration scripts, `includes/funcoes/telefone.php`, and the `ajaxheader.php` include were all removed; `create.php`/`update.php` no longer write anything new when saving a phone number. Sorting happens fresh at render time instead of being persisted.

**Alternatives considered**: The PHP-heuristic-plus-column approach above was actually built, tested against the dev DB (11-vs-10 digit distinction confirmed empirically, not just assumed), and then reverted in favor of this one — its real cost wasn't the schema change itself, it was being Brazil-only when the system already accepts and displays phone numbers from any country, and a materially better, already-proven pattern existed one file away. A full PHP libphonenumber port — still rejected, for the same dependency-free-PHP-layer reasoning, and now genuinely unnecessary. Computing classification at read-time instead of persisting it — this is now the chosen approach, not the rejected one, since accuracy (works for every country) outweighs the minor cost of re-classifying at render time instead of once at write time; the tradeoff is that the sort must be applied at every render site rather than falling out "for free" from a single sorted read, which is a real, accepted maintenance cost, not something to lose track of if a fourth display site is added later.
