corrreção na visualização das tabelas

This commit is contained in:
Gabriel 2026-08-25 17:50:30 -03:00
parent 70b517c9c6
commit 5ceffab0db
2 changed files with 226 additions and 75 deletions

View File

@ -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 <colgroup> (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 <td> antes desse limite. */
agora vem do `width` inline no <th> (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 <td> 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 <table>
de verdade, sem coluna redimensionável. As tabelas de Mensalidade/
Coparticipação/Auditoria usam CSS Grid (.ips-grid, mais abaixo), não
<table> — ver a nota grande em wireColumnResize() no JS pro motivo. */
.ips-table-scroll .pa-table {
width: auto;
/* `table-layout: fixed` + <colgroup> (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 <table>/<colgroup>/<col>,
trocado depois de confirmar ao vivo com o usuário que o navegador dele
simplesmente não reflui a largura de <table> 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 <table> 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

View File

@ -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 ? `<col style="width:${extraColWidth}px">` : "";
const cols = campos.map(([campo, , largura]) => `<col style="width:${widths[campo] || largura}px">`).join("");
return `<colgroup>${extraCol}${cols}</colgroup>`;
}
function resizeHandleHtml(campo, target) {
return `<span class="ips-col-resize-handle" data-resize-campo="${campo}" data-resize-target="${target}"></span>`;
}
// 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 <table>.
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 `<th class="ips-col-sortable${sortClasse}" data-col-campo="${campo}" data-col-target="${target}">${label} <span class="ips-sort-icon">↕</span>${resizeHandleHtml(campo, target)}</th>`;
return `<div class="ips-grid-cell ips-grid-cell--head ips-col-sortable${sortClasse}" role="columnheader" data-col-campo="${campo}" data-col-target="${target}">${label} <span class="ips-sort-icon">↕</span>${resizeHandleHtml(campo, target)}</div>`;
}
// 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 <table> — 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 <col>/<colgroup>, nem `width` no <th>, 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 <table>), 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
? `<td><input type="text" class="ips-table-input" data-linha-campo="${campo}" value="${escapeAttr(linha[campo])}" /></td>`
: `<td><span class="ips-table-value">${escapeHtml(linha[campo])}</span></td>`;
? `<div class="ips-grid-cell" role="cell"><input type="text" class="ips-table-input" data-linha-campo="${campo}" value="${escapeAttr(linha[campo])}" /></div>`
: `<div class="ips-grid-cell" role="cell"><span class="ips-table-value">${escapeHtml(linha[campo])}</span></div>`;
})
.join("");
const removerHtml = concluida
@ -1707,25 +1761,28 @@ document.addEventListener("DOMContentLoaded", async () => {
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>`;
return `
<tr data-linha-id="${linha.id}">
<td class="ips-table-actions">${removerHtml}</td>
<div class="ips-grid-row" role="row" data-linha-id="${linha.id}">
<div class="ips-grid-cell ips-table-actions" role="cell">${removerHtml}</div>
${celulasHtml}
</tr>`;
</div>`;
})
.join("");
return `
${resumoHtml}
<div class="pa-table-wrap ips-table-scroll">
<table class="pa-table">
${colGroupHtml(campos, colWidthsLinha, 44)}
<thead>
<tr><th class="ips-table-actions"></th>${campos
<div class="ips-grid" role="table" style="grid-template-columns:${gridTemplateColumns(campos, colWidthsLinha, 44)}">
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-grid-cell--head ips-table-actions" role="columnheader"></div>
${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir }))
.join("")}</tr>
</thead>
<tbody>${linhasHtml || `<tr class="pa-empty-row"><td colspan="${campos.length + 1}">Nenhuma linha.</td></tr>`}</tbody>
</table>
.join("")}
</div>
${
linhasHtml ||
`<div class="ips-grid-row pa-empty-row" role="row"><div class="ips-grid-cell" role="cell" style="grid-column:1 / -1">Nenhuma linha.</div></div>`
}
</div>
</div>
${concluida ? "" : `
<div class="ips-add-row">
@ -1769,31 +1826,31 @@ document.addEventListener("DOMContentLoaded", async () => {
acaoHtml = `<button type="button" class="btn-outline ips-vincular-btn" data-vincular-auditoria="${item.id}">Vincular pessoa</button>`;
}
return `
<tr>
<td class="ips-table-actions">${acaoHtml}</td>
<td>${item.nome || "—"}</td>
<td>${item.cpf || "—"}</td>
<td>${item.tipo === "T" ? "Titular" : "Dependente"}</td>
<td>${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}</td>
<td>${Number(item.valor).toFixed(2).replace(".", ",")}</td>
<td><span class="ips-auditoria-motivo">${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}</span></td>
<td>${item.detalhe || ""}</td>
</tr>`;
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-table-actions" role="cell">${acaoHtml}</div>
<div class="ips-grid-cell" role="cell">${item.nome || "—"}</div>
<div class="ips-grid-cell" role="cell">${item.cpf || "—"}</div>
<div class="ips-grid-cell" role="cell">${item.tipo === "T" ? "Titular" : "Dependente"}</div>
<div class="ips-grid-cell" role="cell">${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}</div>
<div class="ips-grid-cell" role="cell">${Number(item.valor).toFixed(2).replace(".", ",")}</div>
<div class="ips-grid-cell" role="cell"><span class="ips-auditoria-motivo">${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}</span></div>
<div class="ips-grid-cell" role="cell">${item.detalhe || ""}</div>
</div>`;
})
.join("");
return `
<div class="pa-table-wrap ips-table-scroll">
<table class="pa-table">
${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}
<thead>
<tr><th class="ips-table-actions ips-col-sortable${
<div class="ips-grid" role="table" style="grid-template-columns:${gridTemplateColumns(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}">
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-grid-cell--head ips-table-actions ips-col-sortable${
auditoriaSortKey === "acao" ? (auditoriaSortDir === 1 ? " is-sort-asc" : " is-sort-desc") : ""
}" data-col-campo="acao" data-col-target="auditoria">Ação <span class="ips-sort-icon">↕</span></th>${campos
}" role="columnheader" data-col-campo="acao" data-col-target="auditoria">Ação <span class="ips-sort-icon">↕</span></div>
${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir }))
.join("")}</tr>
</thead>
<tbody>${linhasHtml}</tbody>
</table>
.join("")}
</div>
${linhasHtml}
</div>
</div>`;
}