From 5ceffab0db51a71e075993e7a7912652efa63267 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 25 Aug 2026 17:50:30 -0300 Subject: [PATCH] =?UTF-8?q?corrre=C3=A7=C3=A3o=20na=20visualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20das=20tabelas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/importacao-plano-saude.css | 112 +++++++++++++-- static/js/importacao-plano-saude.js | 189 +++++++++++++++++--------- 2 files changed, 226 insertions(+), 75 deletions(-) diff --git a/static/css/importacao-plano-saude.css b/static/css/importacao-plano-saude.css index 36b621b..b0bc6bb 100644 --- a/static/css/importacao-plano-saude.css +++ b/static/css/importacao-plano-saude.css @@ -673,9 +673,9 @@ .ips-table-input { width: 100%; /* Só o suficiente pra não ficar colado na borda — a largura real da coluna - agora vem do (JS), ajustável até PID_IPS_COL_MIN_WIDTH (60px) - pela alça de redimensionar; um min-width maior aqui faria o input vazar - do antes desse limite. */ + agora vem do `width` inline no (JS), ajustável até + PID_IPS_COL_MIN_WIDTH (60px) pela alça de redimensionar; um min-width + maior aqui faria o input vazar do antes desse limite. */ min-width: 30px; padding: var(--space-2); border: 1px solid transparent; @@ -731,12 +731,12 @@ overflow-x: auto; } +/* Usada só pela aba "Alterações" (panelHtmlAlteracoes, JS) — um + de verdade, sem coluna redimensionável. As tabelas de Mensalidade/ + Coparticipação/Auditoria usam CSS Grid (.ips-grid, mais abaixo), não +
— ver a nota grande em wireColumnResize() no JS pro motivo. */ .ips-table-scroll .pa-table { width: auto; - /* `table-layout: fixed` + (importacao-plano-saude.js) é o que - permite cada coluna ter uma largura própria e ajustável — com o `auto` - padrão o navegador recalcula a largura pelo conteúdo e ignora o que a - alça de redimensionar define. */ table-layout: fixed; } @@ -747,6 +747,80 @@ text-overflow: ellipsis; } +/* Tabela de Mensalidade/Coparticipação/Auditoria (panelHtmlParaTipo/ + panelHtmlAuditoria, JS) — CSS Grid em vez de
//, + trocado depois de confirmar ao vivo com o usuário que o navegador dele + simplesmente não reflui a largura de
desta tela depois do + primeiro layout (testado de várias formas, inclusive mexendo direto no + DOM pelo Console, fora do código da aplicação — só uma tabela HTML nova + e pequena com as mesmas classes refluía normalmente). `grid-template- + columns` no container é o único lugar que define a largura de cada + coluna — cada `.ips-grid-row` é `display:contents` (marcação semântica + de linha, `role="row"`, sem caixa própria) só pra suas células + participarem direto do grid do `.ips-grid` ancestral; é assim que uma + única declaração de `grid-template-columns` vale pra cabeçalho e corpo + ao mesmo tempo, como as colunas de um
normal. */ +.ips-grid { + display: grid; + width: max-content; + min-width: 100%; +} + +.ips-grid-row { + display: contents; +} + +.ips-grid-cell { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border-subtle); + font-size: 0.88rem; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: background var(--transition-fast); +} + +.ips-grid-row:last-child .ips-grid-cell { + border-bottom: none; +} + +.ips-grid-row:hover .ips-grid-cell { + background: var(--card-bg-hover); +} + +.ips-grid-cell--head { + position: relative; + text-align: left; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); + background: var(--bg-surface-raised); +} + +/* Coluna de ações (ícone fixo) fica presa na primeira posição mesmo + rolando a tabela horizontalmente — mesmo espírito de + .pa-table th/td.ips-table-actions (ver mais abaixo), só que via grid. */ +.ips-grid-cell.ips-table-actions { + position: sticky; + left: 0; + z-index: 1; + text-align: center; + background: var(--bg-surface); +} + +.ips-grid-cell--head.ips-table-actions { + background: var(--bg-surface-raised); +} + +.pa-empty-row .ips-grid-cell { + text-align: center; + color: var(--text-muted); + padding: var(--space-6); +} + /* Coluna de ações (ícone fixo) não recebe alça de redimensionar, então fica de fora do `position: relative` — evita conflitar com o `position: sticky` dela (ver regra mais abaixo). */ @@ -757,9 +831,16 @@ .ips-col-resize-handle { position: absolute; top: 0; - right: -3px; + /* Fica encostada na borda interna da própria coluna (não a cavalo da + borda, "right: -3px" antigo) porque o `th`/`td` desta tabela tem + `overflow: hidden` (pro text-overflow:ellipsis do cabeçalho) — qualquer + parte da alça que ultrapassasse a borda do `th` (containing block dela, + por causa do `position: relative`) era cortada pelo próprio overflow, + sobrando só ~3px clicáveis de fato e tornando o arraste praticamente + impossível de acertar. Bug real reportado pelo usuário. */ + right: 0; bottom: 0; - width: 6px; + width: 8px; cursor: col-resize; z-index: 2; } @@ -779,6 +860,19 @@ body.is-resizing-column * { user-select: none; } +/* Linha-guia mostrada durante o arrasto (wireColumnResize, JS) — a coluna + em si só é redimensionada de fato ao soltar (renderPanels() reconstrói a + tabela com a largura nova), então esta linha é o único feedback visual + em tempo real do arraste. position:fixed pra acompanhar o cursor + independente do scroll horizontal da tabela. */ +.ips-col-resize-guide { + position: fixed; + width: 2px; + background: var(--accent); + z-index: 200; + pointer-events: none; +} + /* Clicar no cabeçalho ordena a tabela por aquela coluna (alterna asc/desc) — mesmo ícone ↕ estático de #ips-list-table (ver th[data-sort] mais acima), só que aqui aplicado a qualquer .ips-col-sortable. As colunas em si têm diff --git a/static/js/importacao-plano-saude.js b/static/js/importacao-plano-saude.js index aeca12a..d97148e 100644 --- a/static/js/importacao-plano-saude.js +++ b/static/js/importacao-plano-saude.js @@ -24,16 +24,21 @@ const PID_IPS_PESSOA_LABELS = { dependente: "Dependente", }; +// Larguras padrão ajustadas (pedido explícito do usuário) pra caber cada +// rótulo do cabeçalho sem cortar, sem precisar redimensionar manualmente +// toda vez que abre uma importação — continuam ajustáveis pela alça de +// redimensionar (colWidthsLinha, ver wireColumnResize), isso só muda o +// ponto de partida. const PID_IPS_CAMPOS_LINHA = [ - ["codigo_empresa", "Cód. Empresa", 90], - ["nome_func", "Nome Funcionário", 200], - ["cpf_func", "CPF Funcionário", 130], - ["codigo_out_emp", "Cód. Out. Emp.", 90], - ["data_inicial", "Data Inicial", 100], - ["nome_dependente", "Nome Dependente", 200], - ["cpf_dependente", "CPF Dependente", 130], - ["valor_empresa", "Valor Empresa", 110], - ["valor", "Valor", 90], + ["codigo_empresa", "Cód. Empresa", 110], + ["nome_func", "Nome Funcionário", 210], + ["cpf_func", "CPF Funcionário", 160], + ["codigo_out_emp", "Cód. Out. Emp.", 140], + ["data_inicial", "Data Inicial", 140], + ["nome_dependente", "Nome Dependente", 210], + ["cpf_dependente", "CPF Dependente", 160], + ["valor_empresa", "Valor Empresa", 130], + ["valor", "Valor", 100], ["descricao", "Descrição", 240], ]; @@ -1601,52 +1606,101 @@ document.addEventListener("DOMContentLoaded", async () => { ); } - function colGroupHtml(campos, widths, extraColWidth) { - const extraCol = extraColWidth ? `` : ""; - const cols = campos.map(([campo, , largura]) => ``).join(""); - return `${extraCol}${cols}`; - } - function resizeHandleHtml(campo, target) { return ``; } + // Monta o `grid-template-columns` do .ips-grid a partir dos mesmos pares + // [campo, label, largura] de sempre — ver a nota grande em wireColumnResize + // (abaixo) sobre por que essa tabela usa CSS Grid em vez de
. + function gridTemplateColumns(campos, widths, extraColWidth) { + const extra = extraColWidth ? `${extraColWidth}px ` : ""; + const cols = campos.map(([campo, , largura]) => `${widths[campo] || largura}px`).join(" "); + return `${extra}${cols}`; + } + // Colunas têm ordem fixa (não são mais arrastáveis) — só o clique pra - // ordenar as linhas por essa coluna continua disponível. + // ordenar as linhas por essa coluna continua disponível. A largura em si + // não é mais responsabilidade da célula — vem do `grid-template-columns` + // do container `.ips-grid` (ver gridTemplateColumns() acima). function colHeaderHtml(campo, label, target, sortState) { const ordenadaPorMim = sortState && sortState.key === campo; const sortClasse = ordenadaPorMim ? (sortState.dir === 1 ? " is-sort-asc" : " is-sort-desc") : ""; - return ``; + return `
${label} ↕${resizeHandleHtml(campo, target)}
`; } + // Tabela de Mensalidade/Coparticipação/Auditoria é montada com CSS Grid + // (.ips-grid/.ips-grid-row/.ips-grid-cell, importacao-plano-saude.css), + // não um
${label} ↕${resizeHandleHtml(campo, target)}
— trocado depois de confirmar ao vivo, testando com o + // usuário (inclusive mexendo direto pelo Console, fora do código da + // aplicação) que ESTE navegador simplesmente não reflui a largura desta + // tabela em particular (11 colunas, muitas linhas) depois do primeiro + // layout: nem /, nem `width` no - - - - - - - - - `; +
+
${acaoHtml}
+
${item.nome || "—"}
+
${item.cpf || "—"}
+
${item.tipo === "T" ? "Titular" : "Dependente"}
+
${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}
+
${Number(item.valor).toFixed(2).replace(".", ",")}
+
${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}
+
${item.detalhe || ""}
+
`; }) .join(""); return `
-
, nem forçar reflow + // (offsetWidth, alternar table-layout fixed/auto), nem reconstruir a + // tabela inteira via innerHTML resolvia — uma tabela nova pequena com as + // mesmas classes CSS refluía normalmente, então não era o CSS; era algo + // específico do algoritmo de table-layout do navegador nesta tabela + // grande. CSS Grid usa um algoritmo de dimensionamento completamente + // diferente (grid-template-columns no container, sem nenhuma relação com + // o de ), então fica imune a esse problema. + // + // Pointer Events + setPointerCapture (trava mousemove/mouseup na própria + // alça, independente de onde o cursor esteja de verdade) + "click" com + // stopPropagation (a própria célula de cabeçalho é clicável pra ordenar, + // delegação em panelsEl mais abaixo — sem isso o clique de soltar o + // arrasto vazava pra cima da célula vizinha e disparava uma ordenação + // por engano). O arrasto em si só mostra uma linha-guia + // (.ips-col-resize-guide, position:fixed, puramente visual) seguindo o + // cursor — a largura só é aplicada de fato ao soltar, via renderPanels() + // (reconstrói o HTML com o `grid-template-columns` novo). function wireColumnResize(root) { root.querySelectorAll(".ips-col-resize-handle").forEach((handle) => { - handle.addEventListener("mousedown", (event) => { + handle.addEventListener("pointerdown", (event) => { + if (event.button !== undefined && event.button !== 0) return; event.preventDefault(); - const th = handle.closest("th"); - const table = th.closest("table"); - const colIndex = Array.from(th.parentElement.children).indexOf(th); - const col = table.querySelectorAll("col")[colIndex]; - if (!col) return; + event.stopPropagation(); + const headCell = handle.closest(".ips-grid-cell--head"); + const wrap = handle.closest(".ips-table-scroll"); + if (!headCell || !wrap) return; const campo = handle.getAttribute("data-resize-campo"); const widthsState = handle.getAttribute("data-resize-target") === "auditoria" ? colWidthsAuditoria : colWidthsLinha; const startX = event.clientX; - const startWidth = th.getBoundingClientRect().width; - document.body.classList.add("is-resizing-column"); + const startWidth = headCell.getBoundingClientRect().width; + const cellLeft = headCell.getBoundingClientRect().left; + const wrapRect = wrap.getBoundingClientRect(); + let novaLargura = startWidth; - function onMouseMove(e) { - const novaLargura = Math.max(PID_IPS_COL_MIN_WIDTH, Math.round(startWidth + (e.clientX - startX))); - col.style.width = `${novaLargura}px`; + const guide = document.createElement("div"); + guide.className = "ips-col-resize-guide"; + guide.style.top = `${wrapRect.top}px`; + guide.style.height = `${wrapRect.height}px`; + document.body.appendChild(guide); + + function posicionaGuide() { + guide.style.left = `${cellLeft + novaLargura}px`; } - function onMouseUp() { - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); + posicionaGuide(); + document.body.classList.add("is-resizing-column"); + handle.setPointerCapture(event.pointerId); + + function onPointerMove(e) { + novaLargura = Math.max(PID_IPS_COL_MIN_WIDTH, Math.round(startWidth + (e.clientX - startX))); + posicionaGuide(); + } + function onPointerUp() { + handle.removeEventListener("pointermove", onPointerMove); + handle.removeEventListener("pointerup", onPointerUp); + handle.removeEventListener("pointercancel", onPointerUp); document.body.classList.remove("is-resizing-column"); - widthsState[campo] = parseInt(col.style.width, 10); + guide.remove(); + widthsState[campo] = novaLargura; + renderPanels(); } - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); + handle.addEventListener("pointermove", onPointerMove); + handle.addEventListener("pointerup", onPointerUp); + handle.addEventListener("pointercancel", onPointerUp); }); + handle.addEventListener("click", (event) => event.stopPropagation()); }); } @@ -1697,8 +1751,8 @@ document.addEventListener("DOMContentLoaded", async () => { .map(([campo]) => { const editavel = !concluida && (editavelLivre || PID_IPS_CAMPOS_SEMPRE_EDITAVEIS.includes(campo)); return editavel - ? `` - : ``; + ? `
` + : `
${escapeHtml(linha[campo])}
`; }) .join(""); const removerHtml = concluida @@ -1707,25 +1761,28 @@ document.addEventListener("DOMContentLoaded", async () => { `; return ` - - +
+
${removerHtml}
${celulasHtml} -
`; + `; }) .join(""); return ` ${resumoHtml}
-
${escapeHtml(linha[campo])}
${removerHtml}
- ${colGroupHtml(campos, colWidthsLinha, 44)} - - ${campos +
+
+
+ ${campos .map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir })) - .join("")}
- - ${linhasHtml || ``} -
Nenhuma linha.
+ .join("")} + + ${ + linhasHtml || + `
Nenhuma linha.
` + } + ${concluida ? "" : `
@@ -1769,31 +1826,31 @@ document.addEventListener("DOMContentLoaded", async () => { acaoHtml = ``; } return ` -
${acaoHtml}${item.nome || "—"}${item.cpf || "—"}${item.tipo === "T" ? "Titular" : "Dependente"}${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}${Number(item.valor).toFixed(2).replace(".", ",")}${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}${item.detalhe || ""}
- ${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)} - - ${campos + }" role="columnheader" data-col-campo="acao" data-col-target="auditoria">Ação ↕ + ${campos .map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir })) - .join("")} - - ${linhasHtml} -
+
+
Ação ↕
+ .join("")} + + ${linhasHtml} + `; }