Inclusão da busca automática no Questor e reestruturação visual - Plano de Saúde
This commit is contained in:
parent
7fc771aafb
commit
1fe649ac86
@ -2,7 +2,11 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(python -c ' *)",
|
"Bash(python -c ' *)",
|
||||||
"Bash(.venv/Scripts/python.exe manage.py shell -c ' *)"
|
"Bash(.venv/Scripts/python.exe manage.py shell -c ' *)",
|
||||||
|
"Bash(.venv/Scripts/python.exe manage.py makemigrations portal_api --dry-run --verbosity 2)",
|
||||||
|
"Bash(.venv/Scripts/python.exe manage.py makemigrations portal_api)",
|
||||||
|
"Bash(node --check static/js/importacao-plano-saude.js)",
|
||||||
|
"Bash(grep -n \"Histórico \\(\\\\`#ips-list-table\\\\`\\)\" CLAUDE.md)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
CLAUDE.md
34
CLAUDE.md
@ -549,8 +549,8 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` impleme
|
|||||||
|
|
||||||
### Modelos (`portal_api/models.py`)
|
### Modelos (`portal_api/models.py`)
|
||||||
|
|
||||||
- `ImportacaoPlanoSaude`: uma execução da ferramenta — `operadora`/`nome_operadora`, `tipos_lancamento` (JSONField, lista), `custeio_por_tipo` (JSONField, `{"mensalidade": {"titular": {"modo": "empresa"|"empregado"|"especifica", "limite_valor": float|None, "percentual": float|None}, "dependente": {...}}, "coparticipacao": {...}}` — ver regra de custeio acima), `regra_empresa` (CharField, blank — chave de `planos_saude.regras_empresa.REGRAS_EMPRESA` quando "mensalidade" foi custeada por uma regra especial em vez do `custeio_por_tipo["mensalidade"]` normal, ver "Regra empresa" abaixo), os dois arquivos anexados (`planilha_padrao`/`arquivo_operadora`, `FileField` com o mesmo padrão de validator de tamanho de `LinkFerramenta.icone`, só que 15MB em vez de 2MB — são documentos reais, não ícones), `status` (`revisao`/`concluida`), `criado_por`, `criado_em`/`concluida_em`. **Com histórico**: decisão explícita do usuário — cada importação fica salva (quem fez, quando, arquivos), não é um fluxo descartável.
|
- `ImportacaoPlanoSaude`: uma execução da ferramenta — `operadora`/`nome_operadora`, `tipos_lancamento` (JSONField, lista), `custeio_por_tipo` (JSONField, `{"mensalidade": {"titular": {"modo": "empresa"|"empregado"|"especifica", "limite_valor": float|None, "percentual": float|None}, "dependente": {...}}, "coparticipacao": {...}}` — ver regra de custeio acima), `regra_empresa` (CharField, blank — chave de `planos_saude.regras_empresa.REGRAS_EMPRESA` quando "mensalidade" foi custeada por uma regra especial em vez do `custeio_por_tipo["mensalidade"]` normal, ver "Regra empresa" abaixo), os dois arquivos anexados (`planilha_padrao`/`arquivo_operadora`, `FileField` com o mesmo padrão de validator de tamanho de `LinkFerramenta.icone`, só que 15MB em vez de 2MB — são documentos reais, não ícones; `planilha_padrao` é `blank=True` desde que passou a poder vir de uma busca no Questor em vez de upload, ver "Planilha padrão via Questor (SQL)" abaixo), `competencia` (DateField, null — só preenchida quando a origem da planilha padrão foi essa busca no Questor), `status` (`revisao`/`concluida`), `criado_por`, `criado_em`/`concluida_em`. **Com histórico**: decisão explícita do usuário — cada importação fica salva (quem fez, quando, arquivos), não é um fluxo descartável.
|
||||||
- `ImportacaoPlanoSaudeLinha`: uma linha da planilha padrão já casada com o valor do mês (espelha `LinhaSistema` campo a campo) — **todos os campos são editáveis** na tela de revisão antes de gerar o CSV (decisão explícita do usuário, não só os valores). `valor`/`valor_empresa` ficam como `CharField` no mesmo formato string do pipeline (`"51,69"`/`"0"`), não `DecimalField`, pra manter fidelidade 1:1 com o CSV final sem risco de arredondamento.
|
- `ImportacaoPlanoSaudeLinha`: uma linha da planilha padrão já casada com o valor do mês (espelha `LinhaSistema` campo a campo) — na tela de revisão, uma linha que já veio do processamento (upload ou Questor) só edita **Valor Empresa/Valor**; os demais campos (cadastro da pessoa) só ficam editáveis numa linha incluída manualmente via "Adicionar linha" (regra revista — nasceu como "todos os campos editáveis em qualquer linha", decisão do usuário depois de ver dados reais na tela: só uma linha nova precisa editar o cadastro, uma linha já casada não devia arriscar um cadastro certo sendo alterado por engano). Essa restrição é só de UI (`importacao-plano-saude.js`, `linhasIncluidasManualmente()` — deriva de `ImportacaoPlanoSaudeAlteracao` já carregada, sem campo novo), o backend continua aceitando PATCH em qualquer campo. `valor`/`valor_empresa` ficam como `CharField` no mesmo formato string do pipeline (`"51,69"`/`"0"`), não `DecimalField`, pra manter fidelidade 1:1 com o CSV final sem risco de arredondamento.
|
||||||
- `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa".
|
- `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa".
|
||||||
- `ImportacaoPlanoSaudeAlteracao`: log de cada edição de campo/inclusão/exclusão de linha feita manualmente na revisão — ver seção "Alterações" abaixo.
|
- `ImportacaoPlanoSaudeAlteracao`: log de cada edição de campo/inclusão/exclusão de linha feita manualmente na revisão — ver seção "Alterações" abaixo.
|
||||||
- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada, mesmo espírito de `LinkFerramenta`/`AcessoGeral` — o próprio model não tem FK pra nada; é `ImportacaoPlanoSaude.regra_custeio_salva` que aponta pra cá (opcional, `SET_NULL`), só como registro de qual regra (se alguma) foi aplicada pra preencher aquele formulário.
|
- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada, mesmo espírito de `LinkFerramenta`/`AcessoGeral` — o próprio model não tem FK pra nada; é `ImportacaoPlanoSaude.regra_custeio_salva` que aponta pra cá (opcional, `SET_NULL`), só como registro de qual regra (se alguma) foi aplicada pra preencher aquele formulário.
|
||||||
@ -558,13 +558,35 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` impleme
|
|||||||
### Fluxo e endpoints
|
### Fluxo e endpoints
|
||||||
|
|
||||||
`ImportacaoPlanoSaudeViewSet` (`/api/importacoes-plano-saude/`, `PermissaoApp("utilitarios", "importacao-plano-saude")` pra todos os métodos):
|
`ImportacaoPlanoSaudeViewSet` (`/api/importacoes-plano-saude/`, `PermissaoApp("utilitarios", "importacao-plano-saude")` pra todos os métodos):
|
||||||
- `create()` (multipart, `ImportacaoPlanoSaudeCreateSerializer` valida a entrada) salva o model (isso já grava os 2 arquivos em `MEDIA_ROOT`) e roda `pipeline.processa_importacao()` **de forma síncrona** usando `instance.arquivo_operadora.path`/`instance.planilha_padrao.path` — sem fila/Celery, o arquivo típico processa em menos de um request. Se o processamento falhar (PDF num layout desconhecido etc.), apaga os arquivos recém-salvos + o registro órfão e devolve 400.
|
- `create()` (multipart, `ImportacaoPlanoSaudeCreateSerializer` valida a entrada) resolve a planilha padrão (upload **ou** busca no Questor — ver "Planilha padrão via Questor (SQL)" abaixo), salva o model (isso já grava os 2 arquivos em `MEDIA_ROOT`) e roda `pipeline.processa_importacao()` **de forma síncrona** usando `instance.arquivo_operadora.path` + a lista de `LinhaSistema` já resolvida — sem fila/Celery, o arquivo típico processa em menos de um request. Se o processamento falhar (PDF num layout desconhecido etc.), apaga os arquivos recém-salvos + o registro órfão e devolve 400.
|
||||||
- `GET /operadoras/` (`@action` sem detail) devolve `pipeline.lista_operadoras()` — fonte única pro combobox pesquisável "Operadora" do formulário (`#ips-operadora-combo`, mesmo padrão de "Regra de custeio salva" — ver "Regras de custeio salvas" abaixo), sem duplicar a lista em JS. `label` já vem no formato `"<código> - <Nome>"` (ex.: `"3755 - Itamed Saúde"`) — o código é o de cadastro da operadora no Questor, pedido explícito do usuário pra identificar a operadora sem ambiguidade (útil quando duas operadoras têm nome parecido); editar em `pipeline.OPERADORAS`, não formatar o código separadamente no frontend.
|
- `GET /operadoras/` (`@action` sem detail) devolve `pipeline.lista_operadoras()` — fonte única pro combobox pesquisável "Operadora" do formulário (`#ips-operadora-combo`, mesmo padrão de "Regra de custeio salva" — ver "Regras de custeio salvas" abaixo), sem duplicar a lista em JS. `label` já vem no formato `"<código> - <Nome>"` (ex.: `"3755 - Itamed Saúde"`) — o código é o de cadastro da operadora no Questor, pedido explícito do usuário pra identificar a operadora sem ambiguidade (útil quando duas operadoras têm nome parecido); editar em `pipeline.OPERADORAS`, não formatar o código separadamente no frontend.
|
||||||
- `POST /{id}/gerar/` monta o(s) CSV(s) a partir das **linhas já salvas** (isto é, já com qualquer edição feita na revisão — não reprocessa os arquivos originais) usando `leiaute_sistema.CABECALHO`; 1 tipo de lançamento vira um `.csv` direto, 2 tipos (mensalidade + coparticipação) viram um `.zip` com um `.csv` por tipo (`zipfile` em memória). Pode ser chamada de novo pra regerar depois de mais edições — não bloqueia edição subsequente.
|
- `POST /{id}/gerar/` monta o(s) CSV(s) a partir das **linhas já salvas** (isto é, já com qualquer edição feita na revisão — não reprocessa os arquivos originais) usando `leiaute_sistema.CABECALHO`; 1 tipo de lançamento vira um `.csv` direto, 2 tipos (mensalidade + coparticipação) viram um `.zip` com um `.csv` por tipo (`zipfile` em memória). Sempre marca `status="concluida"` (+ `concluida_em`) — pode ser chamada de novo enquanto `concluida` (regera o mesmo arquivo a partir do que já está salvo), mas a partir daí toda edição de linha/auditoria/alteração fica bloqueada até reabrir (ver `reabrir()` abaixo e "Trava de edição pós-conclusão").
|
||||||
|
- `POST /{id}/reabrir/` volta `status="revisao"` (zera `concluida_em`) — contrapartida de `gerar()`, é o único jeito de voltar a editar uma importação concluída. Botão "Editar" na tela de Revisão, visível só quando `status === "concluida"`.
|
||||||
|
|
||||||
`ImportacaoPlanoSaudeLinhaViewSet` (`/api/importacoes-plano-saude-linhas/{id}/`, só GET/PATCH): edição de uma linha por vez, disparada por `blur`/`change` de cada `<input>` na tela de revisão — mesma permissão de toggle único, sem checagem de "dono".
|
`ImportacaoPlanoSaudeLinhaViewSet` (`/api/importacoes-plano-saude-linhas/{id}/`, só GET/PATCH): edição de uma linha por vez, disparada por `blur`/`change` de cada `<input>` na tela de revisão — mesma permissão de toggle único, sem checagem de "dono". `create()`/`partial_update()`/`destroy()` recusam (400) se a importação já estiver `concluida` — ver "Trava de edição pós-conclusão" abaixo.
|
||||||
|
|
||||||
`ImportacaoPlanoSaudeAuditoriaViewSet` (`/api/importacoes-plano-saude-auditoria/{id}/resolver/`, só `POST`) — ver seção própria abaixo.
|
`ImportacaoPlanoSaudeAuditoriaViewSet` (`/api/importacoes-plano-saude-auditoria/{id}/resolver/`, só `POST`) — ver seção própria abaixo; também recusa se a importação estiver `concluida`.
|
||||||
|
|
||||||
|
**Histórico (`#ips-list-table`): ordenação por coluna + filtro "estilo Excel" por coluna, os dois client-side** sobre o array já carregado (`GET /api/importacoes-plano-saude/`, sem paginação/filtro no servidor) — mesmo padrão de ordenação já usado em `#ua-table`/Ramais (`th[data-sort]`, ícone `↕`). O filtro (`criarFiltroColuna()`, `importacao-plano-saude.js`) nasceu como um segundo campo de texto por coluna, mas foi revisto a pedido do usuário pra imitar o filtro de planilha (Excel/Sheets): um botão de funil dentro do próprio `<th>` de cada coluna filtrável (Cód. Empresa/Operadora/Status/Criado por) abre um popup com busca + checklist dos **valores distintos daquela coluna** (reaproveita `.checklist-box`/`.checklist-search`/`.checklist-select-all`/`.modal-checkbox` de `components.css` — mesmo componente já usado nos checklists de Perfis de Acesso), tudo desmarcável/marcável, com "Aplicar"/"Limpar" no rodapé (só aplica no clique, não a cada checkbox — evita re-renderizar a lista principal a cada toque). Os filtros das 4 colunas combinam entre si (AND). `listFiltros[campo]` vale `null` (sem filtro) ou um `Set` dos valores brutos marcados; marcar **todos** os valores existentes equivale a `null` (sem filtro), pra um valor novo que apareça depois (operadora nova, por exemplo) não nascer excluído até o usuário marcá-lo manualmente. O botão de funil ganha `.is-active` (cor de destaque) enquanto a coluna tiver um filtro aplicado — mesmo sinal visual do funil "azul" do Excel. `.pa-table-wrap` normalmente usa `overflow:hidden` pra arredondar os cantos da tabela; só a tabela do histórico (`.ips-list-table-wrap`) sobrescreve pra `visible`, senão o popup (que precisa aparecer por cima das linhas, não só dentro do cabeçalho) seria cortado ali. Qualquer mudança de filtro (ou de ordenação) volta pra página 1. `listEmpty` mostra uma mensagem diferente conforme o caso: "Nenhuma importação realizada ainda." quando o histórico está mesmo vazio, "Nenhuma importação encontrada com esse filtro." quando o vazio é só resultado do filtro aplicado.
|
||||||
|
|
||||||
|
### Trava de edição pós-conclusão
|
||||||
|
|
||||||
|
Depois que `POST /{id}/gerar/` marca uma importação como `concluida`, editar/incluir/excluir uma linha (`ImportacaoPlanoSaudeLinhaViewSet`), resolver um item de auditoria (`ImportacaoPlanoSaudeAuditoriaViewSet.resolver`) ou reverter uma alteração (`ImportacaoPlanoSaudeAlteracaoViewSet.reverter`) passam a ser recusados (400) — decisão explícita do usuário, depois de ver que a tabela de revisão continuava 100% editável mesmo depois do arquivo já ter sido gerado e entregue. `_garante_importacao_em_revisao(importacao)` (função módulo-level em `views.py`, chamada no início de cada um desses pontos de escrita) é o único lugar que checa isso — levanta `ValidationError` com uma mensagem pedindo pra usar o botão "Editar" (`POST /{id}/reabrir/`) primeiro. `gerar()` em si nunca é bloqueado (pode ser chamado de novo com a importação já `concluida`, só regera o mesmo arquivo a partir do que está salvo).
|
||||||
|
|
||||||
|
No frontend (`importacao-plano-saude.js`), a tela de Revisão espelha essa trava puramente pra UX (a validação real é sempre a do backend acima): com `importacaoAtual.status === "concluida"`, toda célula da tabela de Mensalidade/Coparticipação vira texto (não `<input>`, nem Valor Empresa/Valor — a regra de "só Valor Empresa/Valor editáveis" descrita no bullet de `ImportacaoPlanoSaudeLinha` acima só se aplica quando a importação ainda está em revisão), o "×" de remover linha e o botão "Adicionar linha" somem, "Vincular pessoa" (Auditoria) e "Reverter" (Alterações) também somem. O botão "Editar" (`#ips-review-editar-btn`, ao lado de "Gerar Arquivo") aparece só nesse estado e chama `POST /{id}/reabrir/`, atualizando `importacaoAtual` e re-renderizando as abas.
|
||||||
|
|
||||||
|
Clicar em "Gerar Arquivo" (`gerarBtn`) sempre volta pro histórico (`showView("list")` + `refreshList()`) depois do download disparar — decisão explícita do usuário, já que a partir daí a importação está `concluida` e travada (ver acima), não há mais nada pra revisar de imediato na própria tela.
|
||||||
|
|
||||||
|
### Planilha padrão via Questor (SQL)
|
||||||
|
|
||||||
|
Até uma rodada anterior, a "planilha padrão" (cadastro dos beneficiários, sem valores — o mesmo que `le_planilha_padrao` lê de um CSV) só chegava por upload manual, exportado à mão do Questor. O usuário forneceu e validou uma consulta SQL equivalente contra o próprio banco do Questor, então "Nova Importação" ganhou um segundo caminho: buscar essa planilha automaticamente a partir de empresa (já resolvida pela `RegraCusteioPlanoSaude` escolhida) + operadora + competência (mês/ano digitado na tela) — **sem precisar mais exportar/anexar nada** nesse caso. O upload manual continua existindo como alternativa (Questor fora do ar, ou empresa ainda não migrada) — decisão explícita do usuário, não uma substituição total.
|
||||||
|
|
||||||
|
- **Toggle na tela** (`importacao-plano-saude.js`/`.html`, dentro do bloco "1. Planilha padrão"): dois radios, "Buscar automaticamente do Questor" (padrão) / "Anexar manualmente" — o primeiro revela um campo "Competência" **texto livre com máscara MM/AAAA** (`<input type="text" inputmode="numeric" placeholder="MM/AAAA" maxlength="7">` + `mascaraCompetencia()`/`competenciaParaIso()` em JS — deliberadamente **não** um `<input type="month">`: o seletor nativo do browser foi rejeitado pelo usuário como UX ruim; mesmo padrão de digitação livre já usado em `indicador-desempenho.js`/`pidIndMascaraCompetencia`, copiado aqui em vez de compartilhado, como as demais funções pequenas duplicadas entre telas), o segundo revela o `<input type="file">` de sempre. Trocar de modo limpa o outro campo, pra nunca mandar os dois juntos (o backend também recusa isso).
|
||||||
|
- **Backend**: `ImportacaoPlanoSaudeCreateSerializer.competencia` é um `serializers.DateField()` normal (mesmo padrão de `IndicadorApuracaoCreateSerializer.competencia`) — o frontend já manda o ISO `"AAAA-MM-01"` convertido a partir da máscara, nunca a string mascarada crua. `validate()` exige exatamente uma das duas origens (nunca as duas, nunca nenhuma).
|
||||||
|
- `ImportacaoPlanoSaudeViewSet.create()` (views.py): quando não veio arquivo, resolve a planilha **antes** de criar o registro — `portal_api.planos_saude.questor_planilha.busca_linhas_questor(codigo_empresa, codigo_operadora, competencia)` consulta `sqls.questor.QuestorSQL.consulta_planilha_plano_saude` (só leitura — `select_mappings_query`, nunca `execute`/`execute_returning`, ver [[feedback_bancos_externos_somente_leitura]]) via `DatabaseConnection("questor")`. Uma falha de conexão/consulta aqui devolve 400 direto, sem nada persistido ainda (diferente do caminho de upload, que só sabe se o arquivo é válido depois de já ter salvo o registro — por isso, nesse, o cleanup de arquivo/registro órfão continua sendo necessário). Zero linhas retornadas (empresa sem plano ativo na competência) também é 400. O resultado é serializado de volta pra CSV (`questor_planilha.linhas_para_csv_bytes`, mesmo formato de `leiaute_sistema.CABECALHO`) e salvo como `ContentFile` no próprio campo `planilha_padrao` — preserva o histórico completo mesmo pra importações que nunca tiveram upload. A "trava de conferência do código de empresa" (que confere que a planilha anexada tem alguma linha da empresa da regra) é pulada nesse caminho, redundante já que a consulta já filtrou por esse `codigo_empresa`.
|
||||||
|
- **`pipeline.processa_importacao`** deixou de ler o arquivo sozinho (não recebe mais `caminho_planilha_padrao: str`) — recebe `linhas_sistema_template: List[LinhaSistema]` já pronta, de qualquer uma das duas origens (`le_planilha_padrao(caminho)` pro upload, `busca_linhas_questor(...)` pro Questor) — a decisão de qual usar ficou inteiramente em `views.py create()`.
|
||||||
|
- **Código da operadora**: até então só existia embutido no `label` de `pipeline.OPERADORAS` (ex. `"5060 - Unimed Saúde"`, extraído por *string split* onde só o nome era preciso). Passou a existir como campo próprio (`OPERADORAS[chave]["codigo_operadora"]`, junto de `"nome"`) — é o valor usado pra filtrar a consulta por operadora (`codigooutemp` no Questor, código da OPERADORA, não confundir com `codigo_empresa` do cliente); `pipeline.label_operadora(chave)` calcula o `"<código> - <Nome>"` de exibição a partir desses dois campos onde ainda é preciso (`lista_operadoras()`, mensagens de erro, `nome_operadora` da importação).
|
||||||
|
- `ImportacaoPlanoSaude.competencia` (`DateField`, null) registra a competência usada — só preenchida quando a origem foi o Questor; exibida na tela de Revisão ao lado do nome da operadora.
|
||||||
|
|
||||||
### Resolução manual de auditoria por nome
|
### Resolução manual de auditoria por nome
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -0,0 +1,66 @@
|
|||||||
|
Cód. Contr/Contrat. Pagador;Id. Título;Número Título;Valor Bruto;Valor Líquido;Situação Título;Contratante;Id. Titular;Família;Id. Benef.;Cód.Beneficiário;Nome Beneficiário;Tipo Benef.;Grau Depend.;Código Combinação;Reservado;Valor;Competência;Tipo;Tipo Ato Médico;Cód. Serviço;Descrição Serviço;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;756165;TABATA CAROLINE GOES DA SILVA - 835199;756165;'9750167003613895;TABATA CAROLINE GOES DA SILVA;1;0 - TITULAR;10215;;272,77;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;756165;TABATA CAROLINE GOES DA SILVA - 835199;754971;'9750167003821633;WENDY MANOEL MIGUEL MELO DA SILVA;2;1 - CONJUGE;10008;;231,78;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;756165;TABATA CAROLINE GOES DA SILVA - 835199;756166;'9750167003613909;LUISA VALENTINA GOES DE MELO;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;762483;ADRIANA JARDIM DE CARVALHO - 835478;762483;'9750167003631320;ADRIANA JARDIM DE CARVALHO;1;0 - TITULAR;10008;;706,01;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;761760;TAWANY THYESKA DE PIERRI MARQUES - 835910;761760;'9750167003658732;TAWANY THYESKA DE PIERRI MARQUES;1;0 - TITULAR;10215;;238,92;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;761760;TAWANY THYESKA DE PIERRI MARQUES - 835910;761761;'9750167003658740;THAEMY LOHAYNE DE PIERRE BORGES;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;351,21;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;110;08/2026;Serviços ;Consulta;10101012;Consulta em consultorio no horario normal ou preestabelecido;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;45,65;08/2026;Serviços ;Exame;41301080;Ceratoscopia computadorizada - monocular;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;4,26;08/2026;Serviços ;Exame;41301323;Tonometria - binocular;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;51,08;08/2026;Serviços ;Exame;41501128;Paquimetria ultrassonica - monocular;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;766974;EDSON AUGUSTO DOS SANTOS - 838034;766974;'9750167003785599;EDSON AUGUSTO DOS SANTOS;1;0 - TITULAR;10008;;28,98;08/2026;Serviços ;Outros;50000560;Consulta ambulatorial por nutricionista com diretriz definida pela ANS - n 103;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;757635;JOICE GABRIELLY CONRADO - 840123;757635;'9750167003885879;JOICE GABRIELLY CONRADO;1;0 - TITULAR;10215;;199,65;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;272,8;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;74,25;08/2026;Serviços ;Consulta;10101039;Consulta em pronto socorro;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;3,72;08/2026;Serviços ;Exame;40301583;Colesterol HDL - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;4,96;08/2026;Serviços ;Exame;40301591;Colesterol LDL - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40301605;Colesterol total - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40301630;Creatinina - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40302040;Glicose - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40302504;Transaminase oxalacetica amino transferase aspartato - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40302512;Transaminase piruvica amino transferase de alanina - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;2,48;08/2026;Serviços ;Exame;40302547;Triglicerideos - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,74;08/2026;Serviços ;Exame;40302580;Ureia - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;5,58;08/2026;Serviços ;Exame;40302733;Hemoglobina glicada Fracao A1c - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;8,28;08/2026;Serviços ;Exame;40302830;Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;3,72;08/2026;Serviços ;Exame;40304361;Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;1,86;08/2026;Serviços ;Exame;40310060;Bacterioscopia Gram, Ziehl, Albert etc, por lamina;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;6,19;08/2026;Serviços ;Exame;40310213;Cultura, urina com contagem de colonias;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;11,15;08/2026;Serviços ;Exame;40310426;Antibiograma automatizado;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;2,48;08/2026;Serviços ;Exame;40311210;Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;9,29;08/2026;Serviços ;Exame;40316190;Cortisol - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;12,39;08/2026;Serviços ;Exame;40316246;Estradiol - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;15,48;08/2026;Serviços ;Exame;40316270;Ferritina - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;8,05;08/2026;Serviços ;Exame;40316289;Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;7,8;08/2026;Serviços ;Exame;40316335;Hormonio luteinizante LH - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;13,01;08/2026;Serviços ;Exame;40316408;Progesterona - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;10,53;08/2026;Serviços ;Exame;40316416;Prolactina - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;10,53;08/2026;Serviços ;Exame;40316491;T4 livre - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;10,53;08/2026;Serviços ;Exame;40316521;Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;10,53;08/2026;Serviços ;Exame;40316572;Vitamina B12 - pesquisa e/ou dosagem;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;143,99;08/2026;Serviços ;Exame;40601200;Procedimento diagnostico em peca anatomica ou cirurgica simples;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;74,25;08/2026;Serviços ;Exame;40808041;Mamografia digital bilateral com diretriz definida pela ANS - n 52;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;59,76;08/2026;Serviços ;Exame;40808130;Densitometria ossea - rotina: coluna e femur ou dois segmentos;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;38,52;08/2026;Serviços ;Exame;40901114;US - Mamas;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;755807;'9750167003885950;LUCIANA CARDOSO FERNANDES;1;0 - TITULAR;10008;;44,36;08/2026;Serviços ;Exame;40901300;US - Transvaginal utero, ovario, anexos e vagina;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;755807;LUCIANA CARDOSO FERNANDES - 840127;857049;'9750167018903290;JOEL MARCONCINI;2;1 - CONJUGE;10008;;490,29;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;780352;EVELYN CHRISTINE CURCINO - 847272;780352;'9750167013066030;EVELYN CHRISTINE CURCINO;1;0 - TITULAR;10215;;210,38;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;781791;TAILLANY TAMYRISS DE PIERRI MARQUES - 848274;781791;'9750167013085115;TAILLANY TAMYRISS DE PIERRI MARQUES;1;0 - TITULAR;10215;;199,65;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;781791;TAILLANY TAMYRISS DE PIERRI MARQUES - 848274;781791;'9750167013085115;TAILLANY TAMYRISS DE PIERRI MARQUES;1;0 - TITULAR;10215;;74,25;08/2026;Serviços ;Exame;40901122;US - Abdome total abdome superior, rins, bexiga, aorta, veia cava inferior e ad;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;781791;TAILLANY TAMYRISS DE PIERRI MARQUES - 848274;781791;'9750167013085115;TAILLANY TAMYRISS DE PIERRI MARQUES;1;0 - TITULAR;10215;;39,43;08/2026;Serviços ;Exame;40901211;US - Estruturas superficiais cervical ou axila ou musculo ou tendao;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;781791;TAILLANY TAMYRISS DE PIERRI MARQUES - 848274;781791;'9750167013085115;TAILLANY TAMYRISS DE PIERRI MARQUES;1;0 - TITULAR;10215;;44,36;08/2026;Serviços ;Exame;40901300;US - Transvaginal utero, ovario, anexos e vagina;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;795128;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG - 858372;795128;'9750167013261614;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG;1;0 - TITULAR;10215;;210,38;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;795128;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG - 858372;795128;'9750167013261614;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG;1;0 - TITULAR;10215;;74,25;08/2026;Serviços ;Consulta;10101039;Consulta em pronto socorro;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;795128;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG - 858372;798055;'9750167014716674;MARIA JULIA MERTIG FAGUNDES;2;10 - FILHO(A);10215;;151,51;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;795128;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG - 858372;798339;'9750167015073823;ROBERTO CARLOS CHINAIDER FAGUNDES;2;1 - CONJUGE;10215;;238,92;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;819205;FRANCISCO AQUINO CHAPARRO - 874116;819205;'9750167018482406;FRANCISCO AQUINO CHAPARRO;1;0 - TITULAR;10008;;272,8;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;819205;FRANCISCO AQUINO CHAPARRO - 874116;822752;'9750167018523960;CAMILA CASSIA MORAES COSTA;2;1 - CONJUGE;10008;;272,8;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;844618;FRANCIELLI SOUZA DE JESUS - 886633;844618;'9750167018752799;FRANCIELLI SOUZA DE JESUS;1;0 - TITULAR;10215;;290,61;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;844618;FRANCIELLI SOUZA DE JESUS - 886633;849666;'9750167018811701;ELOIZA DE SOUZA;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;844618;FRANCIELLI SOUZA DE JESUS - 886633;849667;'9750167018811710;ELENA DE SOUZA;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;853105;KESSY KOHNLEIN - 892339;853105;'9750167018854672;KESSY KOHNLEIN;1;0 - TITULAR;10220;;137,23;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;854462;DAIANE DA LUZ GUEIROS DA SILVA - 893274;854462;'9750167018871135;DAIANE DA LUZ GUEIROS DA SILVA;1;0 - TITULAR;10008;;213,95;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;854462;DAIANE DA LUZ GUEIROS DA SILVA - 893274;856557;'9750167018897436;RENAN GABRIEL DA LUZ DOMINGOS;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
5884128-0;3187862;201960424;6818,83;6818,83;Aberto;ROSSONI PIOTTO CIA LTDA - 00384013000454;854462;DAIANE DA LUZ GUEIROS DA SILVA - 893274;869895;'9750167019048971;LUCCA GABRIEL DA LUZ DE JESUS;2;10 - FILHO(A);10008;;130,13;08/2026;Mensalidade;;;;
|
||||||
|
23
media/planos_saude/planilha_padrao/questor_221_2026-08.csv
Normal file
23
media/planos_saude/planilha_padrao/questor_221_2026-08.csv
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
|
||||||
|
221;ADRIANA JARDIM DE CARVALHO;569.433.180-04;5060;01/02/2025;;;0;0;
|
||||||
|
221;DAIANE DA LUZ GUEIROS DA SILVA;080.947.869-22;5060;01/07/2026;RENAN GABRIEL DA LUZ DOMINGOS;800.859.409-84;0;0;
|
||||||
|
221;DAIANE DA LUZ GUEIROS DA SILVA;080.947.869-22;5060;01/07/2026;;;0;0;
|
||||||
|
221;DAIANE DA LUZ GUEIROS DA SILVA;080.947.869-22;5060;01/07/2026;LUCCA GABRIEL DA LUZ DE JESUS;002.261.929-11;0;0;
|
||||||
|
221;EDSON AUGUSTO DOS SANTOS;033.535.459-99;5060;01/06/2025;;;0;0;
|
||||||
|
221;EVELYN CHRISTINE CURCINO;102.022.109-79;5060;01/06/2025;;;0;0;
|
||||||
|
221;FRANCIELLI SOUZA DE JESUS;056.918.579-32;5060;01/02/2026;ELENA DE SOUZA;158.076.209-33;0;0;
|
||||||
|
221;FRANCIELLI SOUZA DE JESUS;056.918.579-32;5060;01/02/2026;;;0;0;
|
||||||
|
221;FRANCIELLI SOUZA DE JESUS;056.918.579-32;5060;01/02/2026;ELOIZA DE SOUZA;142.759.789-86;0;0;
|
||||||
|
221;FRANCISCO AQUINO CHAPARRO;043.273.629-80;5060;01/11/2025;CAMILA CASSIA MORAES COSTA;042.355.369-07;0;0;
|
||||||
|
221;FRANCISCO AQUINO CHAPARRO;043.273.629-80;5060;01/11/2025;;;0;0;
|
||||||
|
221;JOICE GABRIELLY CONRADO;013.817.749-03;5060;01/06/2025;;;0;0;
|
||||||
|
221;KESSY KOHNLEIN;107.820.969-30;5060;01/03/2026;;;0;0;
|
||||||
|
221;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG;121.074.169-58;5060;01/06/2025;;;0;0;
|
||||||
|
221;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG;121.074.169-58;5060;01/06/2025;ROBERTO CARLOS CHINAIDER FAGUNDES;078.468.529-06;0;0;
|
||||||
|
221;KETLIN RAYANI SILVA DE OLIVEIRA MERTIG;121.074.169-58;5060;01/06/2025;MARIA JULIA MERTIG FAGUNDES;156.306.539-89;0;0;
|
||||||
|
221;LUCIANA CARDOSO FERNANDES;044.214.069-02;5060;01/04/2026;JOEL MARCONCINI;969.398.939-20;0;0;
|
||||||
|
221;LUCIANA CARDOSO FERNANDES;044.214.069-02;5060;01/04/2026;;;0;0;
|
||||||
|
221;TABATA CAROLINE GOES DA SILVA;072.082.879-10;5060;01/06/2025;LUISA VALENTINA GOES MELO;107.096.349-60;0;0;
|
||||||
|
221;TABATA CAROLINE GOES DA SILVA;072.082.879-10;5060;01/06/2025;WENDY MANOEL MIGUEL MELO DA SILVA;080.701.949-60;0;0;
|
||||||
|
221;TABATA CAROLINE GOES DA SILVA;072.082.879-10;5060;01/06/2025;;;0;0;
|
||||||
|
221;TAILLANY TAMYRISS DE PIERRI MARQUES;120.239.569-43;5060;01/06/2025;;;0;0;
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
|
||||||
|
221;ADRIANA OLIVEIRA DOS SANTOS CODO;022.366.869-94;3755;01/12/2023;;;0;0;
|
||||||
|
221;ADRIANA OLIVEIRA DOS SANTOS CODO;022.366.869-94;3755;01/12/2023;RAFHAEL HENRIQUE CODO;133.710.699-23;0;0;
|
||||||
|
221;ANELISA CLAUDIA TOLEDO;028.627.449-30;3755;01/12/2023;MURILO AUGUSTO TOLEDO SILVA;140.796.419-40;0;0;
|
||||||
|
221;ANELISA CLAUDIA TOLEDO;028.627.449-30;3755;01/12/2023;;;0;0;
|
||||||
|
221;APARECIDA DOBNER;389.259.312-49;3755;01/12/2023;ANNA CAROLINA DOBNER WILLMBRINK;097.280.639-38;0;0;
|
||||||
|
221;APARECIDA DOBNER;389.259.312-49;3755;01/12/2023;;;0;0;
|
||||||
|
221;DIEGO RODRIGO DA FONSECA;007.377.729-30;3755;01/12/2023;;;0;0;
|
||||||
|
221;ELIANA KARPSAK LOPES;064.896.469-86;3755;01/12/2023;;;0;0;
|
||||||
|
221;ELIANA KARPSAK LOPES;064.896.469-86;3755;01/12/2023;LUCAS ANTONIO DE OLIVEIRA;066.200.469-83;0;0;
|
||||||
|
221;FERNANDO BERLATTO;040.875.749-38;3755;01/12/2023;;;0;0;
|
||||||
|
221;JANE MARILIA BEKER;025.273.319-39;3755;01/12/2023;;;0;0;
|
||||||
|
221;JESSICA DE FATIMA CATTANI DE PAULA;092.879.749-00;3755;01/12/2023;;;0;0;
|
||||||
|
221;JOAO VICTOR DE OLIVEIRA EGIDIO;051.271.009-08;3755;01/12/2023;;;0;0;
|
||||||
|
221;LEANDRA SPEROTTO;022.795.359-20;3755;01/12/2023;;;0;0;
|
||||||
|
221;LEANDRA SPEROTTO;022.795.359-20;3755;01/12/2023;LUCAS SPEROTTO SCHIRMANN;125.709.619-22;0;0;
|
||||||
|
221;LUCAS DE SOUZA;093.861.469-08;3755;01/12/2023;;;0;0;
|
||||||
|
221;MARCOS PAULO GOBBO;057.539.659-89;3755;01/12/2023;;;0;0;
|
||||||
|
221;MARIZA KARPSAK LOPES;726.088.899-72;3755;01/12/2023;EDUARDO KARPSAK LOPES;096.771.579-22;0;0;
|
||||||
|
221;MARIZA KARPSAK LOPES;726.088.899-72;3755;01/12/2023;;;0;0;
|
||||||
|
221;PAULA MICHELLI DA SILVA;073.516.259-05;3755;01/12/2023;;;0;0;
|
||||||
|
221;PAULA MICHELLI DA SILVA;073.516.259-05;3755;01/12/2023;EMANUELLY FERREIRA DA SILVA;152.794.939-70;0;0;
|
||||||
|
221;VANESSA JUIF TRINDADE;032.476.309-37;3755;01/12/2023;;;0;0;
|
||||||
|
221;ZAIRA DAVID BARBOSA;007.396.889-79;3755;01/12/2023;;;0;0;
|
||||||
|
24
plano.md
24
plano.md
@ -1043,6 +1043,30 @@ Sequência natural da rodada 77 (fundo de `.login-page`): o usuário pediu o mes
|
|||||||
|
|
||||||
Depois de ver o fundo liso (`var(--bg-canvas)`, rodada 77), usuário pediu um refinamento: "um tom de roxo um pouco mais claro e o fundo branco levemente escurecido", só no tema claro. Trocado por `radial-gradient(circle at 20% 20%, rgba(var(--accent-rgb), 0.12), transparent 45%)` sobre `#ece7f2` (levemente mais escuro que o `--bg-canvas` puro, `#f3f1f7`) — o brilho radial já existia no tema escuro com opacidade `0.25`, aqui ficou bem mais sutil (`0.12`) pra não ficar turvo sobre um fundo claro. A cor exata é fixa só nesta regra (não altera o token `--bg-canvas`), então o resto do Portal no tema claro continua com o tom original.
|
Depois de ver o fundo liso (`var(--bg-canvas)`, rodada 77), usuário pediu um refinamento: "um tom de roxo um pouco mais claro e o fundo branco levemente escurecido", só no tema claro. Trocado por `radial-gradient(circle at 20% 20%, rgba(var(--accent-rgb), 0.12), transparent 45%)` sobre `#ece7f2` (levemente mais escuro que o `--bg-canvas` puro, `#f3f1f7`) — o brilho radial já existia no tema escuro com opacidade `0.25`, aqui ficou bem mais sutil (`0.12`) pra não ficar turvo sobre um fundo claro. A cor exata é fixa só nesta regra (não altera o token `--bg-canvas`), então o resto do Portal no tema claro continua com o tom original.
|
||||||
|
|
||||||
|
### 80. Buscar a planilha padrão do Questor via SQL (Importação de Plano de Saúde)
|
||||||
|
|
||||||
|
Até aqui, "Nova Importação" exigia exportar manualmente do Questor a "planilha padrão" (CSV) e anexá-la, além do relatório da operadora. Usuário validou uma consulta SQL contra o Questor (mesmo pacote `database/` só-leitura já usado em `empresas_questor.py`) que devolve exatamente essa informação a partir de `codigo_empresa` (já cadastrado em `RegraCusteioPlanoSaude`) + operadora + competência (mês/ano) — eliminando o passo manual na maioria dos casos.
|
||||||
|
|
||||||
|
- **Upload manual continua existindo**, como alternativa (Questor fora do ar, ou empresa ainda não migrada) — decisão explícita do usuário. "Nova Importação" ganhou um toggle "Buscar automaticamente do Questor" (padrão) / "Anexar manualmente", com um campo "Competência" (`<input type="month">`) só no primeiro modo.
|
||||||
|
- **Código da operadora**: até então só existia embutido no `label` de `pipeline.OPERADORAS` (ex. "5060 - Unimed Saúde"), extraído por *string split* onde precisava só do nome. Separado em campos próprios (`codigo_operadora`/`nome`), com `label_operadora()` calculando o label onde ainda é exibido — elimina o split e dá um valor confiável pra filtrar a consulta por operadora.
|
||||||
|
- **Nova consulta** (`sqls.questor.QuestorSQL.consulta_planilha_plano_saude`) e novo módulo `portal_api/planos_saude/questor_planilha.py` (`busca_linhas_questor`/`linhas_para_csv_bytes`) — a busca é resolvida **antes** de criar o registro `ImportacaoPlanoSaude` (uma falha de conexão nunca deixa nada órfão pra limpar, diferente do caminho de upload). `pipeline.processa_importacao` deixou de ler o arquivo sozinho (`caminho_planilha_padrao: str`) e passou a receber a lista de `LinhaSistema` já resolvida (`linhas_sistema_template`), de qualquer uma das duas origens — decisão de qual usar ficou em `views.py`.
|
||||||
|
- Mesmo vindo do Questor, um CSV é gerado (mesmo formato do upload) e salvo no campo `planilha_padrao` — preserva o histórico completo já documentado pra toda importação. Novo campo `ImportacaoPlanoSaude.competencia` (`DateField`, null) registra a competência usada, só quando a origem foi o Questor (migração `0045`).
|
||||||
|
- A "trava de conferência do código de empresa" (existente desde a rodada do Cadastro de Regras) é pulada quando a origem é Questor — redundante, já que a própria consulta já filtrou por aquele `codigo_empresa`.
|
||||||
|
|
||||||
|
**Ajuste no mesmo dia, depois do primeiro teste real**: o campo "Competência" nasceu como `<input type="month">` — usuário rejeitou o seletor nativo do browser como "horrível" e pediu o mesmo padrão já usado no Indicador de Desempenho: texto livre com máscara MM/AAAA (`mascaraCompetencia()`/`competenciaParaIso()`, cópia local das mesmas funções de `indicador-desempenho.js`), o usuário digita "082026" e o campo forma "08/2026" sozinho. Backend acompanhou: `competencia` no serializer é um `DateField` normal (mesmo padrão de `IndicadorApuracaoCreateSerializer`), recebendo o ISO `"AAAA-MM-01"` já convertido pelo JS, nunca a string mascarada crua.
|
||||||
|
|
||||||
|
**Segundo ajuste, mesmo teste real**: a coluna `DATAINICIAL` do CSV gerado via Questor saía no formato ISO (`2023-12-01`, `linha["datainicial"].isoformat()` — a renderização padrão do Postgres pra uma coluna `date`), mas o leiaute real de importação do sistema espera `DD/MM/AAAA` (`01/12/2023`) nessa coluna — mesmo formato que uma planilha padrão exportada manualmente do Questor já usa. Corrigido em `questor_planilha.busca_linhas_questor()` pra `linha["datainicial"].strftime("%d/%m/%Y")`. `data_inicial` é sempre um passthrough string em todo o pipeline (nenhum outro ponto faz parsing de data nele, só grava/lê a string), então a mudança não exigiu tocar em mais nenhum arquivo. Nenhum registro histórico ficou com o formato errado — a única importação de teste que chegou a usar a busca via Questor (antes deste ajuste) já tinha sido excluída pelo próprio usuário.
|
||||||
|
|
||||||
|
**Terceiro ajuste, mesmo dia — regra de edição da tabela de revisão revista**: até aqui, todo campo de `ImportacaoPlanoSaudeLinha` era editável em qualquer linha na tela de Revisão (decisão de uma rodada anterior). Vendo dados reais de uma importação via Questor (22 linhas, várias famílias), o usuário pediu pra restringir: só `Valor Empresa`/`Valor` editáveis numa linha que já veio do processamento (upload ou Questor) — os demais campos (cadastro da pessoa: nome, CPF, código, data...) só ficam editáveis numa linha incluída manualmente via "Adicionar linha", e só nela. Implementado 100% no frontend (`importacao-plano-saude.js`, `linhasIncluidasManualmente()`), sem campo novo: reaproveita o sinal que já existia em `ImportacaoPlanoSaudeAlteracao` (toda linha criada via "Adicionar linha" já gerava um registro `tipo="inclusao"` ali, só pra alimentar a aba "Alterações" — agora também decide, no cliente, quais linhas ganham os campos extras liberados). O backend continua aceitando PATCH em qualquer campo (nenhuma trava nova no serializer/model) — é uma restrição de UI, não de permissão.
|
||||||
|
|
||||||
|
**Quarto ajuste, mesmo dia — trava de edição depois de "Gerar Arquivo"**: o ajuste anterior já restringia *quais campos*, mas o usuário notou que uma importação já `concluida` (arquivo já gerado/entregue) continuava 100% editável — pediu pra travar de vez, com um botão "Editar" explícito pra reabrir quando for realmente necessário corrigir algo depois da conclusão. Diferente do ajuste anterior, este saiu **também no backend**: `_garante_importacao_em_revisao(importacao)` (nova função em `views.py`) é chamada em todo ponto que escreve dentro de uma importação já criada — `ImportacaoPlanoSaudeLinhaViewSet.perform_create/update/destroy`, `ImportacaoPlanoSaudeAuditoriaViewSet.resolver`, `ImportacaoPlanoSaudeAlteracaoViewSet.reverter` — e recusa (400) se `status == "concluida"`. Novo endpoint `POST /api/importacoes-plano-saude/{id}/reabrir/` (`ImportacaoPlanoSaudeViewSet.reabrir`) volta `status="revisao"` e zera `concluida_em` — único jeito de destravar. `gerar()` em si nunca é bloqueado (sempre pode regerar/rebaixar o arquivo já concluído). No frontend, a tela de Revisão some com todo controle de edição (inputs viram texto, "×"/"Adicionar linha"/"Vincular pessoa"/"Reverter" somem) quando `status === "concluida"`, e mostra um botão "Editar" (ao lado de "Gerar Arquivo") que chama o `reabrir()` e volta tudo ao normal. Testado ponta a ponta via `APIRequestFactory`/`force_authenticate` (sem afetar dados reais, dentro de uma transação revertida): reabrir muda o status, edita normalmente em revisão, e volta a bloquear ao marcar concluída de novo.
|
||||||
|
|
||||||
|
**Quinto ajuste, mesmo dia**: "Gerar Arquivo" passou a voltar direto pro histórico depois do download disparar (`showView("list")` + `refreshList()`) — como a importação já fica `concluida`/travada nesse momento (ajuste anterior), não fazia sentido continuar na tela de Revisão sem nada pra fazer ali.
|
||||||
|
|
||||||
|
**Sexto ajuste, mesmo dia — filtro no histórico**: com o histórico crescendo (23 importações já), o usuário pediu pra filtrar a lista, não só ordenar — ex.: ver todas as execuções da empresa 221, ou combinar empresa 221 + operadora 3755. Primeira versão foi um campo de texto por coluna (Cód. Empresa/Operadora/Criado por) + um `<select>` pra Status. 100% client-side, sem endpoint novo.
|
||||||
|
|
||||||
|
**Sétimo ajuste, mesmo dia — filtro revisado pra "estilo Excel"**: usuário pediu pra trocar os campos de texto por um filtro de planilha de verdade — clicar num ícone de funil na coluna e marcar/desmarcar os valores que aparecem numa lista, como no AutoFilter do Excel. Reescrito (`criarFiltroColuna()`) como um popup por coluna com busca + checklist dos valores distintos daquela coluna (reaproveita `.checklist-box` de Perfis de Acesso, sem CSS/componente novo pra isso) e botões "Aplicar"/"Limpar" — os filtros das 4 colunas continuam combinando entre si (AND). Precisou de um ajuste de CSS colateral: `.pa-table-wrap` corta com `overflow:hidden` pra arredondar os cantos da tabela, o que cortaria o popup também — resolvido com uma classe extra só nesta tabela (`.ips-list-table-wrap`) sobrescrevendo pra `overflow:visible`.
|
||||||
|
|
||||||
## Roadmap / próximos passos
|
## Roadmap / próximos passos
|
||||||
|
|
||||||
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
|
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-08-21 20:05
|
||||||
|
|
||||||
|
import portal_api.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal_api', '0044_empresaquestor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='importacaoplanosaude',
|
||||||
|
name='competencia',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Competência (Questor)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importacaoplanosaude',
|
||||||
|
name='planilha_padrao',
|
||||||
|
field=models.FileField(blank=True, max_length=255, upload_to='planos_saude/planilha_padrao/', validators=[portal_api.models.validar_tamanho_arquivo_plano_saude], verbose_name='Planilha padrão (Questor)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -563,8 +563,16 @@ class ImportacaoPlanoSaude(models.Model):
|
|||||||
"Planilha padrão (Questor)",
|
"Planilha padrão (Questor)",
|
||||||
upload_to="planos_saude/planilha_padrao/",
|
upload_to="planos_saude/planilha_padrao/",
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
validators=[validar_tamanho_arquivo_plano_saude],
|
validators=[validar_tamanho_arquivo_plano_saude],
|
||||||
)
|
)
|
||||||
|
# Preenchida só quando a planilha padrão veio de uma busca direta no
|
||||||
|
# Questor (via SQL) em vez de upload manual — ver
|
||||||
|
# portal_api.planos_saude.questor_planilha. Nesse caso, planilha_padrao
|
||||||
|
# ainda é salva (um CSV gerado no mesmo formato do upload), só que essa
|
||||||
|
# data indica a origem alternativa. Fica em branco pra toda importação
|
||||||
|
# que veio por upload.
|
||||||
|
competencia = models.DateField("Competência (Questor)", null=True, blank=True)
|
||||||
arquivo_operadora = models.FileField(
|
arquivo_operadora = models.FileField(
|
||||||
"Arquivo da operadora",
|
"Arquivo da operadora",
|
||||||
upload_to="planos_saude/operadora/",
|
upload_to="planos_saude/operadora/",
|
||||||
@ -591,8 +599,14 @@ class ImportacaoPlanoSaudeLinha(models.Model):
|
|||||||
"""Uma linha da planilha padrão (leiaute do sistema) dentro de uma
|
"""Uma linha da planilha padrão (leiaute do sistema) dentro de uma
|
||||||
ImportacaoPlanoSaude, já com o valor do mês casado pelo pipeline —
|
ImportacaoPlanoSaude, já com o valor do mês casado pelo pipeline —
|
||||||
espelha `portal_api.planos_saude.modelos.LinhaSistema` campo a campo.
|
espelha `portal_api.planos_saude.modelos.LinhaSistema` campo a campo.
|
||||||
Todos os campos (não só os valores) são editáveis na tela de revisão
|
Todos os campos aceitam PATCH no backend (sem trava aqui), mas a tela de
|
||||||
antes de gerar o CSV final (decisão explícita do usuário)."""
|
revisão só expõe `valor_empresa`/`valor` como editáveis numa linha que já
|
||||||
|
veio do processamento — os demais campos (cadastro da pessoa) só ficam
|
||||||
|
editáveis numa linha incluída manualmente via "Adicionar linha" (decisão
|
||||||
|
revista do usuário; ver `linhasIncluidasManualmente()` em
|
||||||
|
`importacao-plano-saude.js`, que decide isso no cliente a partir de
|
||||||
|
`ImportacaoPlanoSaudeAlteracao` — não há trava correspondente aqui no
|
||||||
|
model/serializer, é só a UI que restringe)."""
|
||||||
|
|
||||||
importacao = models.ForeignKey(ImportacaoPlanoSaude, on_delete=models.CASCADE, related_name="linhas")
|
importacao = models.ForeignKey(ImportacaoPlanoSaude, on_delete=models.CASCADE, related_name="linhas")
|
||||||
tipo_lancamento = models.CharField("Tipo de lançamento", max_length=20)
|
tipo_lancamento = models.CharField("Tipo de lançamento", max_length=20)
|
||||||
|
|||||||
@ -11,7 +11,6 @@ OPERADORAS abaixo. Nada mais precisa mudar.
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from portal_api.planos_saude.leiaute_sistema import le_planilha_padrao
|
|
||||||
from portal_api.planos_saude.matcher import casa_individuos_com_planilha
|
from portal_api.planos_saude.matcher import casa_individuos_com_planilha
|
||||||
from portal_api.planos_saude.modelos import ItemAuditoria, LinhaSistema
|
from portal_api.planos_saude.modelos import ItemAuditoria, LinhaSistema
|
||||||
from portal_api.planos_saude.regras_empresa import valida_regra_empresa
|
from portal_api.planos_saude.regras_empresa import valida_regra_empresa
|
||||||
@ -29,38 +28,56 @@ CUSTEIOS_VALIDOS = ("empresa", "empregado", "especifica")
|
|||||||
|
|
||||||
OPERADORAS = {
|
OPERADORAS = {
|
||||||
"amil_odonto_mensalidade": {
|
"amil_odonto_mensalidade": {
|
||||||
"label": "3758 - Amil Odonto",
|
"codigo_operadora": "3758",
|
||||||
|
"nome": "Amil Odonto",
|
||||||
"parser": AmilOdontoMensalidade,
|
"parser": AmilOdontoMensalidade,
|
||||||
},
|
},
|
||||||
"unimed_saude": {
|
"unimed_saude": {
|
||||||
"label": "5060 - Unimed Saúde",
|
"codigo_operadora": "5060",
|
||||||
|
"nome": "Unimed Saúde",
|
||||||
"parser": UnimedSaude,
|
"parser": UnimedSaude,
|
||||||
},
|
},
|
||||||
"itamed_saude": {
|
"itamed_saude": {
|
||||||
"label": "3755 - Itamed Saúde",
|
"codigo_operadora": "3755",
|
||||||
|
"nome": "Itamed Saúde",
|
||||||
"parser": ItamedSaude,
|
"parser": ItamedSaude,
|
||||||
},
|
},
|
||||||
"dental_uni_odonto_mensalidade": {
|
"dental_uni_odonto_mensalidade": {
|
||||||
"label": "1723 - Dental Uni Odonto",
|
"codigo_operadora": "1723",
|
||||||
|
"nome": "Dental Uni Odonto",
|
||||||
"parser": DentalUniOdontoMensalidade,
|
"parser": DentalUniOdontoMensalidade,
|
||||||
},
|
},
|
||||||
"unimed_oeste_pr_saude": {
|
"unimed_oeste_pr_saude": {
|
||||||
"label": "4709 - Unimed Oeste do Paraná",
|
"codigo_operadora": "4709",
|
||||||
|
"nome": "Unimed Oeste do Paraná",
|
||||||
"parser": UnimedOestePrSaude,
|
"parser": UnimedOestePrSaude,
|
||||||
},
|
},
|
||||||
"bradesco_saude": {
|
"bradesco_saude": {
|
||||||
"label": "1386 - Bradesco Saúde",
|
"codigo_operadora": "1386",
|
||||||
|
"nome": "Bradesco Saúde",
|
||||||
"parser": BradescoSaude,
|
"parser": BradescoSaude,
|
||||||
},
|
},
|
||||||
"bradesco_dental_odonto_mensalidade": {
|
"bradesco_dental_odonto_mensalidade": {
|
||||||
"label": "3759 - Bradessaude Odonto",
|
"codigo_operadora": "3759",
|
||||||
|
"nome": "Bradessaude Odonto",
|
||||||
"parser": BradescoDentalOdontoMensalidade,
|
"parser": BradescoDentalOdontoMensalidade,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def label_operadora(operadora_key: str) -> str:
|
||||||
|
""""<código de cadastro da operadora no Questor> - <Nome>" (ex.:
|
||||||
|
"5060 - Unimed Saúde") — esse código é da OPERADORA, não do
|
||||||
|
`codigo_empresa` do cliente que contratou o plano. Fonte única pra
|
||||||
|
montar o label a partir dos campos separados de OPERADORAS, usada
|
||||||
|
tanto por `lista_operadoras()` quanto por quem precisava do `label`
|
||||||
|
bruto antes desta função existir."""
|
||||||
|
dados = OPERADORAS[operadora_key]
|
||||||
|
return f"{dados['codigo_operadora']} - {dados['nome']}"
|
||||||
|
|
||||||
|
|
||||||
def lista_operadoras() -> List[Dict[str, str]]:
|
def lista_operadoras() -> List[Dict[str, str]]:
|
||||||
return [{"key": chave, "label": dados["label"]} for chave, dados in OPERADORAS.items()]
|
return [{"key": chave, "label": label_operadora(chave)} for chave in OPERADORAS]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -73,7 +90,7 @@ class ResultadoProcessamento:
|
|||||||
def processa_importacao(
|
def processa_importacao(
|
||||||
operadora_key: str,
|
operadora_key: str,
|
||||||
caminho_arquivo_operadora: str,
|
caminho_arquivo_operadora: str,
|
||||||
caminho_planilha_padrao: str,
|
linhas_sistema_template: List[LinhaSistema],
|
||||||
tipos_selecionados: List[str],
|
tipos_selecionados: List[str],
|
||||||
custeio_por_tipo: Dict[str, dict],
|
custeio_por_tipo: Dict[str, dict],
|
||||||
regra_empresa_key: Optional[str] = None,
|
regra_empresa_key: Optional[str] = None,
|
||||||
@ -90,6 +107,11 @@ def processa_importacao(
|
|||||||
float|None}, "dependente": {...}}`). Réplica do loop de main.py, com o
|
float|None}, "dependente": {...}}`). Réplica do loop de main.py, com o
|
||||||
acréscimo do filtro por tipos_selecionados e do custeio configurável.
|
acréscimo do filtro por tipos_selecionados e do custeio configurável.
|
||||||
|
|
||||||
|
`linhas_sistema_template` já vem pronta (lida do arquivo via
|
||||||
|
`le_planilha_padrao` ou buscada no Questor via
|
||||||
|
`questor_planilha.busca_linhas_questor` — decisão de qual das duas
|
||||||
|
fontes usar é de quem chama, em views.py).
|
||||||
|
|
||||||
`regra_empresa_key`, quando informado, substitui o custeio configurável
|
`regra_empresa_key`, quando informado, substitui o custeio configurável
|
||||||
de "mensalidade" por uma regra especial cadastrada em
|
de "mensalidade" por uma regra especial cadastrada em
|
||||||
portal_api.planos_saude.regras_empresa (ex.: teto de custeio por
|
portal_api.planos_saude.regras_empresa (ex.: teto de custeio por
|
||||||
@ -102,7 +124,6 @@ def processa_importacao(
|
|||||||
parser_operadora = operadora_info["parser"]()
|
parser_operadora = operadora_info["parser"]()
|
||||||
|
|
||||||
individuos, auditoria_extracao = parser_operadora.extrai(caminho_arquivo_operadora)
|
individuos, auditoria_extracao = parser_operadora.extrai(caminho_arquivo_operadora)
|
||||||
linhas_sistema_template = le_planilha_padrao(caminho_planilha_padrao)
|
|
||||||
|
|
||||||
regra_empresa_fn = None
|
regra_empresa_fn = None
|
||||||
if regra_empresa_key:
|
if regra_empresa_key:
|
||||||
|
|||||||
77
portal_api/planos_saude/questor_planilha.py
Normal file
77
portal_api/planos_saude/questor_planilha.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Busca a "planilha padrão" (mesma informação que o Questor hoje exporta
|
||||||
|
manualmente pra Importação de Plano de Saúde) direto do banco do Questor,
|
||||||
|
via SQL — alternativa ao upload manual do CSV. Ver
|
||||||
|
`sqls.questor.QuestorSQL.consulta_planilha_plano_saude` pra consulta em si
|
||||||
|
(fornecida e validada pelo usuário) e `ImportacaoPlanoSaudeViewSet.create`
|
||||||
|
em views.py pra onde isso é consumido.
|
||||||
|
|
||||||
|
Só leitura (`select_mappings_query`, nunca `execute`/`execute_returning` —
|
||||||
|
ver [[feedback_bancos_externos_somente_leitura]] na memória do projeto).
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from database.connection import DatabaseConnection
|
||||||
|
from sqls.questor import QuestorSQL
|
||||||
|
|
||||||
|
from portal_api.planos_saude.leiaute_sistema import CABECALHO
|
||||||
|
from portal_api.planos_saude.modelos import LinhaSistema
|
||||||
|
|
||||||
|
|
||||||
|
def busca_linhas_questor(codigo_empresa: str, codigo_operadora: str, competencia: date) -> List[LinhaSistema]:
|
||||||
|
"""Consulta o Questor e devolve a lista de `LinhaSistema` pra essa
|
||||||
|
empresa+operadora+competência — mesmo formato que `le_planilha_padrao`
|
||||||
|
devolveria a partir do CSV exportado manualmente. `valor_empresa`/`valor`
|
||||||
|
sempre voltam como o placeholder `"0"` (a própria consulta já devolve
|
||||||
|
`0::numeric` pros dois — são sempre sobrescritos depois pelo casamento
|
||||||
|
com o arquivo da operadora, nunca usados como estão). Propaga qualquer
|
||||||
|
exceção de conexão/consulta pro chamador — diferente de
|
||||||
|
`empresas_questor.resolve_nome_empresa`, aqui o dado é essencial ao
|
||||||
|
fluxo, não só informativo pra exibição."""
|
||||||
|
query = QuestorSQL.consulta_planilha_plano_saude(codigo_empresa, codigo_operadora, competencia)
|
||||||
|
with DatabaseConnection("questor") as db:
|
||||||
|
linhas_query = db.select_mappings_query(query)
|
||||||
|
|
||||||
|
return [
|
||||||
|
LinhaSistema(
|
||||||
|
codigo_empresa=str(linha["codigoempresa"]),
|
||||||
|
nome_func=(linha["nomefunc"] or "").strip(),
|
||||||
|
cpf_func=(linha["cpffunc"] or "").strip(),
|
||||||
|
codigo_out_emp=str(linha["codigooperadora"]),
|
||||||
|
# DD/MM/AAAA — mesmo formato que a planilha padrão exportada
|
||||||
|
# manualmente do Questor já usa nessa coluna (leiaute de
|
||||||
|
# importação real, não o formato ISO que o Postgres devolve por
|
||||||
|
# padrão pra uma coluna `date`).
|
||||||
|
data_inicial=linha["datainicial"].strftime("%d/%m/%Y") if linha["datainicial"] else "",
|
||||||
|
nome_dependente=(linha["nomedepend"] or "").strip(),
|
||||||
|
cpf_dependente=(linha["cpfdepend"] or "").strip(),
|
||||||
|
valor_empresa="0",
|
||||||
|
valor="0",
|
||||||
|
descricao="",
|
||||||
|
)
|
||||||
|
for linha in linhas_query
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def linhas_para_csv_bytes(linhas: List[LinhaSistema]) -> bytes:
|
||||||
|
"""Serializa `linhas` no mesmo formato/cabeçalho de
|
||||||
|
`leiaute_sistema.CABECALHO` (CSV `;`-delimitado, UTF-8) — usado só pra
|
||||||
|
persistir um snapshot fiel no campo `ImportacaoPlanoSaude.planilha_padrao`
|
||||||
|
quando a origem foi o Questor (busca via SQL), preservando o histórico
|
||||||
|
completo do jeito que já existe pro caminho de upload manual."""
|
||||||
|
linhas_texto = [";".join(CABECALHO)]
|
||||||
|
for linha in linhas:
|
||||||
|
linhas_texto.append(";".join([
|
||||||
|
linha.codigo_empresa,
|
||||||
|
linha.nome_func,
|
||||||
|
linha.cpf_func,
|
||||||
|
linha.codigo_out_emp,
|
||||||
|
linha.data_inicial,
|
||||||
|
linha.nome_dependente,
|
||||||
|
linha.cpf_dependente,
|
||||||
|
linha.valor_empresa,
|
||||||
|
linha.valor,
|
||||||
|
linha.descricao,
|
||||||
|
]))
|
||||||
|
return ("\r\n".join(linhas_texto) + "\r\n").encode("utf-8-sig")
|
||||||
@ -507,7 +507,15 @@ class ImportacaoPlanoSaudeCreateSerializer(serializers.Serializer):
|
|||||||
`ImportacaoPlanoSaudeViewSet.create` em views.py."""
|
`ImportacaoPlanoSaudeViewSet.create` em views.py."""
|
||||||
|
|
||||||
operadora = serializers.ChoiceField(choices=list(OPERADORAS.keys()))
|
operadora = serializers.ChoiceField(choices=list(OPERADORAS.keys()))
|
||||||
planilha_padrao = serializers.FileField()
|
# Exatamente uma das duas origens da planilha padrão precisa vir
|
||||||
|
# preenchida — ver validate(): upload manual (comportamento original)
|
||||||
|
# ou busca automática no Questor por competência (mês/ano digitado na
|
||||||
|
# tela, formato "AAAA-MM").
|
||||||
|
planilha_padrao = serializers.FileField(required=False)
|
||||||
|
# Mesmo formato de entrada de IndicadorApuracaoCreateSerializer — o
|
||||||
|
# frontend converte a máscara "MM/AAAA" pro ISO "AAAA-MM-01" antes de
|
||||||
|
# enviar (ver pidIpsCompetenciaParaIso em importacao-plano-saude.js).
|
||||||
|
competencia = serializers.DateField(required=False, allow_null=True)
|
||||||
arquivo_operadora = serializers.FileField()
|
arquivo_operadora = serializers.FileField()
|
||||||
tipos_lancamento = serializers.CharField()
|
tipos_lancamento = serializers.CharField()
|
||||||
# Chave de REGRAS_EMPRESA (portal_api.planos_saude.regras_empresa) — ver
|
# Chave de REGRAS_EMPRESA (portal_api.planos_saude.regras_empresa) — ver
|
||||||
@ -582,6 +590,29 @@ class ImportacaoPlanoSaudeCreateSerializer(serializers.Serializer):
|
|||||||
attrs["tipos_lancamento_lista"] = tipos
|
attrs["tipos_lancamento_lista"] = tipos
|
||||||
attrs["custeio_por_tipo"] = custeio_por_tipo
|
attrs["custeio_por_tipo"] = custeio_por_tipo
|
||||||
attrs["regra_empresa"] = regra_empresa_key
|
attrs["regra_empresa"] = regra_empresa_key
|
||||||
|
|
||||||
|
# Exatamente uma origem pra planilha padrão: upload manual (arquivo)
|
||||||
|
# ou busca automática no Questor (competência). Ver
|
||||||
|
# ImportacaoPlanoSaudeViewSet.create, em views.py, pra onde cada uma
|
||||||
|
# das duas é de fato resolvida em `linhas_sistema_template`.
|
||||||
|
planilha_padrao = attrs.get("planilha_padrao")
|
||||||
|
competencia = attrs.get("competencia")
|
||||||
|
if planilha_padrao and competencia:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"competencia": "Anexe a planilha manualmente ou informe a competência para buscar no Questor — não os dois."}
|
||||||
|
)
|
||||||
|
if not planilha_padrao and not competencia:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"planilha_padrao": "Anexe a planilha padrão ou informe a competência para buscar no Questor."}
|
||||||
|
)
|
||||||
|
|
||||||
|
if competencia:
|
||||||
|
regra_custeio_salva = attrs.get("regra_custeio_salva")
|
||||||
|
if not regra_custeio_salva or not regra_custeio_salva.codigo_empresa:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"competencia": "Buscar a planilha no Questor exige selecionar a empresa (regra de custeio cadastrada)."}
|
||||||
|
)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
@ -747,6 +778,7 @@ class ImportacaoPlanoSaudeListSerializer(serializers.ModelSerializer):
|
|||||||
"id",
|
"id",
|
||||||
"nome_operadora",
|
"nome_operadora",
|
||||||
"codigo_empresa",
|
"codigo_empresa",
|
||||||
|
"competencia",
|
||||||
"tipos_lancamento",
|
"tipos_lancamento",
|
||||||
"custeio_por_tipo",
|
"custeio_por_tipo",
|
||||||
"status",
|
"status",
|
||||||
@ -782,6 +814,7 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
|
|||||||
"nome_operadora",
|
"nome_operadora",
|
||||||
"codigo_empresa",
|
"codigo_empresa",
|
||||||
"nome_empresa",
|
"nome_empresa",
|
||||||
|
"competencia",
|
||||||
"tipos_lancamento",
|
"tipos_lancamento",
|
||||||
"custeio_por_tipo",
|
"custeio_por_tipo",
|
||||||
"regra_empresa",
|
"regra_empresa",
|
||||||
@ -846,14 +879,11 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
def _nome_operadora_sem_codigo(operadora_key: str) -> str:
|
def _nome_operadora_sem_codigo(operadora_key: str) -> str:
|
||||||
"""`pipeline.OPERADORAS[chave]["label"]` vem no formato "<código de
|
"""Nome da operadora (ex.: "Unimed Saúde"), sem o código de cadastro
|
||||||
cadastro da operadora no Questor> - <Nome>" (ex.: "5060 - Unimed
|
dela no Questor — usado só pra compor o `nome` derivado de
|
||||||
Saúde") — esse código é da OPERADORA, não do `codigo_empresa` do
|
|
||||||
cliente que contratou o plano; usado só pra compor o `nome` derivado de
|
|
||||||
RegraCusteioPlanoSaude sem repetir esse código, que não tem relação com
|
RegraCusteioPlanoSaude sem repetir esse código, que não tem relação com
|
||||||
a empresa."""
|
o `codigo_empresa` do cliente que contratou o plano."""
|
||||||
label = OPERADORAS[operadora_key]["label"]
|
return OPERADORAS[operadora_key]["nome"]
|
||||||
return label.split(" - ", 1)[1] if " - " in label else label
|
|
||||||
|
|
||||||
|
|
||||||
class RegraCusteioPlanoSaudeSerializer(serializers.ModelSerializer):
|
class RegraCusteioPlanoSaudeSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import zipfile
|
|||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
import holidays
|
import holidays
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Max, Q, QuerySet
|
from django.db.models import Max, Q, QuerySet
|
||||||
@ -70,6 +71,7 @@ from .planos_saude import pipeline as planos_saude_pipeline
|
|||||||
from .planos_saude import regras_empresa as planos_saude_regras_empresa
|
from .planos_saude import regras_empresa as planos_saude_regras_empresa
|
||||||
from .planos_saude.leiaute_sistema import CABECALHO as PLANO_SAUDE_CABECALHO
|
from .planos_saude.leiaute_sistema import CABECALHO as PLANO_SAUDE_CABECALHO
|
||||||
from .planos_saude.leiaute_sistema import formata_valor_br, le_planilha_padrao, parse_valor_br
|
from .planos_saude.leiaute_sistema import formata_valor_br, le_planilha_padrao, parse_valor_br
|
||||||
|
from .planos_saude.questor_planilha import busca_linhas_questor, linhas_para_csv_bytes
|
||||||
from .planos_saude.regras_empresa import RegraEmpresaIncompativelError
|
from .planos_saude.regras_empresa import RegraEmpresaIncompativelError
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
AcessoGeralSecaoSerializer,
|
AcessoGeralSecaoSerializer,
|
||||||
@ -698,7 +700,7 @@ def _valida_arquivo_operadora(arquivo: UploadedFile, operadora_key: str) -> dict
|
|||||||
"valido": False,
|
"valido": False,
|
||||||
"mensagem": (
|
"mensagem": (
|
||||||
f"Não foi possível reconhecer este arquivo como um relatório da operadora "
|
f"Não foi possível reconhecer este arquivo como um relatório da operadora "
|
||||||
f"{operadora_info['label']} — confira se é o arquivo certo e se o formato "
|
f"{planos_saude_pipeline.label_operadora(operadora_key)} — confira se é o arquivo certo e se o formato "
|
||||||
"(PDF ou CSV, conforme esperado por essa operadora) está correto."
|
"(PDF ou CSV, conforme esperado por essa operadora) está correto."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -746,6 +748,20 @@ def _snapshot_linha_plano_saude(linha: ImportacaoPlanoSaudeLinha) -> dict[str, A
|
|||||||
return dados
|
return dados
|
||||||
|
|
||||||
|
|
||||||
|
def _garante_importacao_em_revisao(importacao: ImportacaoPlanoSaude) -> None:
|
||||||
|
"""Bloqueia qualquer edição de linha/auditoria/alteração de uma
|
||||||
|
ImportacaoPlanoSaude já `concluida` — `ImportacaoPlanoSaudeViewSet.reabrir`
|
||||||
|
é o único jeito de voltar a editar (volta o status pra `revisao`).
|
||||||
|
Chamada em todo ponto que grava algo dentro de uma importação já criada
|
||||||
|
(linha/auditoria/alteração), exceto `gerar()` em si, que só lê o que já
|
||||||
|
está salvo e pode ser chamado de novo pra rebaixar o mesmo arquivo
|
||||||
|
mesmo depois de concluída."""
|
||||||
|
if importacao.status == ImportacaoPlanoSaude.STATUS_CONCLUIDA:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": 'Esta importação já foi concluída. Clique em "Editar" para reabri-la antes de alterar.'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
||||||
"""Ferramenta "Importação de Plano de Saúde" (Utilitários) — permissão de
|
"""Ferramenta "Importação de Plano de Saúde" (Utilitários) — permissão de
|
||||||
toggle único (sem par visualizar/editar, ver catalogo.py "utilitarios"), então
|
toggle único (sem par visualizar/editar, ver catalogo.py "utilitarios"), então
|
||||||
@ -823,24 +839,66 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
|||||||
custeio_por_tipo = dados["custeio_por_tipo"]
|
custeio_por_tipo = dados["custeio_por_tipo"]
|
||||||
operadora_key = dados["operadora"]
|
operadora_key = dados["operadora"]
|
||||||
operadora_info = planos_saude_pipeline.OPERADORAS[operadora_key]
|
operadora_info = planos_saude_pipeline.OPERADORAS[operadora_key]
|
||||||
|
regra_custeio_obj = dados.get("regra_custeio_salva")
|
||||||
|
competencia = dados.get("competencia")
|
||||||
|
|
||||||
|
planilha_padrao_arquivo = dados.get("planilha_padrao")
|
||||||
|
linhas_sistema_template = None
|
||||||
|
planilha_padrao_gerada = None
|
||||||
|
if not planilha_padrao_arquivo:
|
||||||
|
# Sem upload: busca a planilha padrão no Questor, ANTES de criar
|
||||||
|
# a importação — uma falha de conexão/consulta aqui nunca deixa
|
||||||
|
# nada órfão pra limpar (diferente do caminho de upload, que só
|
||||||
|
# sabe se o arquivo é válido depois de já ter salvo o registro).
|
||||||
|
codigo_empresa_regra = normalizar_codigo_empresa(regra_custeio_obj.codigo_empresa)
|
||||||
|
codigo_operadora = operadora_info["codigo_operadora"]
|
||||||
|
try:
|
||||||
|
linhas_sistema_template = busca_linhas_questor(codigo_empresa_regra, codigo_operadora, competencia)
|
||||||
|
except Exception:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Não foi possível buscar a planilha padrão no Questor agora. "
|
||||||
|
"Tente novamente em alguns instantes ou anexe a planilha manualmente."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if not linhas_sistema_template:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
f"Nenhum beneficiário encontrado no Questor para a empresa {regra_custeio_obj.codigo_empresa} "
|
||||||
|
f"com a operadora {operadora_info['nome']} na competência informada."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
planilha_padrao_gerada = ContentFile(
|
||||||
|
linhas_para_csv_bytes(linhas_sistema_template),
|
||||||
|
name=f"questor_{codigo_empresa_regra}_{competencia:%Y-%m}.csv",
|
||||||
|
)
|
||||||
|
|
||||||
importacao = ImportacaoPlanoSaude.objects.create(
|
importacao = ImportacaoPlanoSaude.objects.create(
|
||||||
operadora=operadora_key,
|
operadora=operadora_key,
|
||||||
nome_operadora=operadora_info["label"],
|
nome_operadora=planos_saude_pipeline.label_operadora(operadora_key),
|
||||||
tipos_lancamento=tipos,
|
tipos_lancamento=tipos,
|
||||||
custeio_por_tipo=custeio_por_tipo,
|
custeio_por_tipo=custeio_por_tipo,
|
||||||
regra_empresa=dados["regra_empresa"],
|
regra_empresa=dados["regra_empresa"],
|
||||||
regra_custeio_salva=dados.get("regra_custeio_salva"),
|
regra_custeio_salva=regra_custeio_obj,
|
||||||
criado_por=request.user,
|
criado_por=request.user,
|
||||||
planilha_padrao=dados["planilha_padrao"],
|
competencia=competencia,
|
||||||
|
planilha_padrao=planilha_padrao_arquivo or planilha_padrao_gerada,
|
||||||
arquivo_operadora=dados["arquivo_operadora"],
|
arquivo_operadora=dados["arquivo_operadora"],
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if planilha_padrao_arquivo:
|
||||||
|
linhas_sistema_template = le_planilha_padrao(importacao.planilha_padrao.path)
|
||||||
resultado = planos_saude_pipeline.processa_importacao(
|
resultado = planos_saude_pipeline.processa_importacao(
|
||||||
operadora_key=operadora_key,
|
operadora_key=operadora_key,
|
||||||
caminho_arquivo_operadora=importacao.arquivo_operadora.path,
|
caminho_arquivo_operadora=importacao.arquivo_operadora.path,
|
||||||
caminho_planilha_padrao=importacao.planilha_padrao.path,
|
linhas_sistema_template=linhas_sistema_template,
|
||||||
tipos_selecionados=tipos,
|
tipos_selecionados=tipos,
|
||||||
custeio_por_tipo=custeio_por_tipo,
|
custeio_por_tipo=custeio_por_tipo,
|
||||||
regra_empresa_key=dados["regra_empresa"] or None,
|
regra_empresa_key=dados["regra_empresa"] or None,
|
||||||
@ -878,9 +936,10 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
|||||||
# `regra_empresa_chave` (onde é redundante com a checagem que
|
# `regra_empresa_chave` (onde é redundante com a checagem que
|
||||||
# `regras_empresa.valida_regra_empresa()` já faz dentro do try acima)
|
# `regras_empresa.valida_regra_empresa()` já faz dentro do try acima)
|
||||||
# — protege contra o registro em REGRAS_EMPRESA ficar dessincronizado
|
# — protege contra o registro em REGRAS_EMPRESA ficar dessincronizado
|
||||||
# da RegraCusteioPlanoSaude correspondente.
|
# da RegraCusteioPlanoSaude correspondente. Quando a planilha veio do
|
||||||
regra_custeio_obj = dados.get("regra_custeio_salva")
|
# Questor (não upload), essa conferência é redundante — a consulta já
|
||||||
if regra_custeio_obj and regra_custeio_obj.codigo_empresa:
|
# filtrou pelo próprio `codigo_empresa` da regra — então é pulada.
|
||||||
|
if planilha_padrao_arquivo and regra_custeio_obj and regra_custeio_obj.codigo_empresa:
|
||||||
todas_linhas = [linha for linhas in resultado.linhas_por_tipo.values() for linha in linhas]
|
todas_linhas = [linha for linhas in resultado.linhas_por_tipo.values() for linha in linhas]
|
||||||
# A planilha padrão traz o código cru do Questor (pode ter zero à
|
# A planilha padrão traz o código cru do Questor (pode ter zero à
|
||||||
# esquerda, ex. "092"), enquanto RegraCusteioPlanoSaude.codigo_empresa
|
# esquerda, ex. "092"), enquanto RegraCusteioPlanoSaude.codigo_empresa
|
||||||
@ -974,12 +1033,27 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
|||||||
response["Content-Disposition"] = f'attachment; filename="importacao_plano_saude_{importacao.id}.zip"'
|
response["Content-Disposition"] = f'attachment; filename="importacao_plano_saude_{importacao.id}.zip"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def reabrir(self, request: Request, pk: str | None = None) -> Response:
|
||||||
|
"""Contrapartida de gerar(): volta uma importação `concluida` pra
|
||||||
|
`revisao`, liberando de novo a edição de linhas/auditoria/alterações
|
||||||
|
(ver `_garante_importacao_em_revisao`) — botão "Editar" na tela de
|
||||||
|
Revisão, visível só quando `status == "concluida"`."""
|
||||||
|
importacao = self.get_object()
|
||||||
|
importacao.status = ImportacaoPlanoSaude.STATUS_REVISAO
|
||||||
|
importacao.concluida_em = None
|
||||||
|
importacao.save(update_fields=["status", "concluida_em"])
|
||||||
|
serializer = ImportacaoPlanoSaudeDetailSerializer(importacao)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class ImportacaoPlanoSaudeLinhaViewSet(viewsets.ModelViewSet):
|
class ImportacaoPlanoSaudeLinhaViewSet(viewsets.ModelViewSet):
|
||||||
"""Edição das linhas de uma importação já criada — todos os campos (não só os
|
"""Edição das linhas de uma importação já criada — todos os campos (não só os
|
||||||
valores) são editáveis na tela de revisão, decisão explícita do usuário. Mesma
|
valores) são editáveis na tela de revisão (com a restrição de UI + backend
|
||||||
permissão de toggle único de ImportacaoPlanoSaudeViewSet; qualquer usuário
|
de `_garante_importacao_em_revisao`: nada disso é permitido depois que a
|
||||||
autorizado pode editar linha de qualquer importação, sem conceito de "dono".
|
importação foi concluída, até reabri-la). Mesma permissão de toggle único
|
||||||
|
de ImportacaoPlanoSaudeViewSet; qualquer usuário autorizado pode editar
|
||||||
|
linha de qualquer importação, sem conceito de "dono".
|
||||||
`create()` permite incluir manualmente uma linha nova (botão "Adicionar linha"
|
`create()` permite incluir manualmente uma linha nova (botão "Adicionar linha"
|
||||||
na revisão) — nasce em branco/"0" e entra automaticamente no CSV gerado, já
|
na revisão) — nasce em branco/"0" e entra automaticamente no CSV gerado, já
|
||||||
que `gerar()` (na outra view) lê todas as linhas da importação sem distinguir
|
que `gerar()` (na outra view) lê todas as linhas da importação sem distinguir
|
||||||
@ -1004,6 +1078,7 @@ class ImportacaoPlanoSaudeLinhaViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def perform_create(self, serializer: ImportacaoPlanoSaudeLinhaCreateSerializer) -> None:
|
def perform_create(self, serializer: ImportacaoPlanoSaudeLinhaCreateSerializer) -> None:
|
||||||
importacao = serializer.validated_data["importacao"]
|
importacao = serializer.validated_data["importacao"]
|
||||||
|
_garante_importacao_em_revisao(importacao)
|
||||||
tipo = serializer.validated_data["tipo_lancamento"]
|
tipo = serializer.validated_data["tipo_lancamento"]
|
||||||
maior_ordem = ImportacaoPlanoSaudeLinha.objects.filter(
|
maior_ordem = ImportacaoPlanoSaudeLinha.objects.filter(
|
||||||
importacao=importacao, tipo_lancamento=tipo
|
importacao=importacao, tipo_lancamento=tipo
|
||||||
@ -1022,6 +1097,7 @@ class ImportacaoPlanoSaudeLinhaViewSet(viewsets.ModelViewSet):
|
|||||||
# serializer.instance ainda reflete os valores ANTES do save() abaixo —
|
# serializer.instance ainda reflete os valores ANTES do save() abaixo —
|
||||||
# é o que permite comparar campo a campo o que de fato mudou.
|
# é o que permite comparar campo a campo o que de fato mudou.
|
||||||
linha_anterior = serializer.instance
|
linha_anterior = serializer.instance
|
||||||
|
_garante_importacao_em_revisao(linha_anterior.importacao)
|
||||||
valores_anteriores = {
|
valores_anteriores = {
|
||||||
campo: getattr(linha_anterior, campo)
|
campo: getattr(linha_anterior, campo)
|
||||||
for campo in PLANO_SAUDE_CAMPOS_ALTERACAO
|
for campo in PLANO_SAUDE_CAMPOS_ALTERACAO
|
||||||
@ -1042,6 +1118,7 @@ class ImportacaoPlanoSaudeLinhaViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance: ImportacaoPlanoSaudeLinha) -> None:
|
def perform_destroy(self, instance: ImportacaoPlanoSaudeLinha) -> None:
|
||||||
|
_garante_importacao_em_revisao(instance.importacao)
|
||||||
ImportacaoPlanoSaudeAlteracao.objects.create(
|
ImportacaoPlanoSaudeAlteracao.objects.create(
|
||||||
importacao=instance.importacao,
|
importacao=instance.importacao,
|
||||||
tipo=ImportacaoPlanoSaudeAlteracao.TIPO_EXCLUSAO,
|
tipo=ImportacaoPlanoSaudeAlteracao.TIPO_EXCLUSAO,
|
||||||
@ -1076,6 +1153,7 @@ class ImportacaoPlanoSaudeAlteracaoViewSet(viewsets.GenericViewSet):
|
|||||||
própria reversão não gera um novo registro de alteração (evita um
|
própria reversão não gera um novo registro de alteração (evita um
|
||||||
loop de "reverter a reversão")."""
|
loop de "reverter a reversão")."""
|
||||||
alteracao = self.get_object()
|
alteracao = self.get_object()
|
||||||
|
_garante_importacao_em_revisao(alteracao.importacao)
|
||||||
if alteracao.revertida:
|
if alteracao.revertida:
|
||||||
raise ValidationError({"detail": "Esta alteração já foi revertida."})
|
raise ValidationError({"detail": "Esta alteração já foi revertida."})
|
||||||
|
|
||||||
@ -1154,6 +1232,7 @@ class ImportacaoPlanoSaudeAuditoriaViewSet(viewsets.GenericViewSet):
|
|||||||
sobrescrever sem querer um lançamento que já casou automaticamente
|
sobrescrever sem querer um lançamento que já casou automaticamente
|
||||||
com outra pessoa do arquivo da operadora."""
|
com outra pessoa do arquivo da operadora."""
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
_garante_importacao_em_revisao(item.importacao)
|
||||||
|
|
||||||
if item.motivo not in ImportacaoPlanoSaudeAuditoria.MOTIVOS_RESOLVIVEIS:
|
if item.motivo not in ImportacaoPlanoSaudeAuditoria.MOTIVOS_RESOLVIVEIS:
|
||||||
raise ValidationError({"detail": "Esse item de auditoria não pode ser resolvido manualmente."})
|
raise ValidationError({"detail": "Esse item de auditoria não pode ser resolvido manualmente."})
|
||||||
|
|||||||
110
sqls/questor.py
110
sqls/questor.py
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import date
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from database.query import SQLQuery
|
from database.query import SQLQuery
|
||||||
@ -20,3 +21,112 @@ class QuestorSQL:
|
|||||||
e.codigoempresa = :codigo_empresa
|
e.codigoempresa = :codigo_empresa
|
||||||
""")
|
""")
|
||||||
return SQLQuery(sql=sql, params={"codigo_empresa": codigo_empresa})
|
return SQLQuery(sql=sql, params={"codigo_empresa": codigo_empresa})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def consulta_planilha_plano_saude(
|
||||||
|
codigo_empresa: str, codigo_operadora: str, competencia: date
|
||||||
|
) -> SQLQuery:
|
||||||
|
"""Mesma informação da "planilha padrão" que o Questor hoje exporta
|
||||||
|
manualmente pra Importação de Plano de Saúde (ver
|
||||||
|
`portal_api.planos_saude.questor_planilha.busca_linhas_questor`),
|
||||||
|
pra uma única empresa+operadora+competência — consulta fornecida e
|
||||||
|
validada pelo usuário. `competencia` é sempre o primeiro dia do mês
|
||||||
|
(ex.: `date(2026, 8, 1)`); `codigo_operadora` filtra pra só o plano
|
||||||
|
da operadora selecionada (é `codigooutemp`, código de cadastro da
|
||||||
|
OPERADORA no Questor — não confundir com `codigo_empresa`, ver
|
||||||
|
`pipeline.OPERADORAS`)."""
|
||||||
|
sql = dedent(r"""
|
||||||
|
with periodo_plano as (
|
||||||
|
select
|
||||||
|
codigoempresa,
|
||||||
|
codigofunccontr,
|
||||||
|
codigooutemp,
|
||||||
|
max(datainicial) datainicial
|
||||||
|
from
|
||||||
|
funcplanosaude
|
||||||
|
where
|
||||||
|
codigoempresa = :codigo_empresa
|
||||||
|
and codigooutemp = :codigo_operadora
|
||||||
|
and datainicial <= :competencia
|
||||||
|
and (datafinal is null or datafinal >= :competencia)
|
||||||
|
group by
|
||||||
|
codigoempresa,
|
||||||
|
codigofunccontr,
|
||||||
|
codigooutemp
|
||||||
|
),
|
||||||
|
dados_plano_func as (
|
||||||
|
select
|
||||||
|
codigoempresa,
|
||||||
|
codigofunccontr,
|
||||||
|
codigooutemp,
|
||||||
|
datainicial,
|
||||||
|
datafinal,
|
||||||
|
null::int codigodepend
|
||||||
|
from
|
||||||
|
funcplanosaude
|
||||||
|
union
|
||||||
|
select
|
||||||
|
codigoempresa,
|
||||||
|
codigofunccontr,
|
||||||
|
codigooutemp,
|
||||||
|
datainicial,
|
||||||
|
null::date datafinal,
|
||||||
|
codigodepend
|
||||||
|
from
|
||||||
|
funcplanosaudedepend
|
||||||
|
)
|
||||||
|
select
|
||||||
|
d.codigoempresa,
|
||||||
|
fp.nomefunc,
|
||||||
|
fp.cpffunc,
|
||||||
|
d.codigooutemp codigooperadora,
|
||||||
|
oe.nomeoutemp nomeoperadora,
|
||||||
|
d.datainicial,
|
||||||
|
dep.nomedepend,
|
||||||
|
dep.cpfdepend,
|
||||||
|
0::numeric(18,2) valorempresa,
|
||||||
|
0::numeric(18,2) valor,
|
||||||
|
null::text descricao
|
||||||
|
from
|
||||||
|
dados_plano_func d
|
||||||
|
join
|
||||||
|
periodo_plano p on
|
||||||
|
d.codigoempresa = p.codigoempresa
|
||||||
|
and d.codigofunccontr = p.codigofunccontr
|
||||||
|
and d.codigooutemp = p.codigooutemp
|
||||||
|
and d.datainicial = p.datainicial
|
||||||
|
join
|
||||||
|
empresa e on
|
||||||
|
d.codigoempresa = e.codigoempresa
|
||||||
|
join
|
||||||
|
funccontrato fc on
|
||||||
|
d.codigoempresa = fc.codigoempresa
|
||||||
|
and d.codigofunccontr = fc.codigofunccontr
|
||||||
|
join
|
||||||
|
funcpessoa fp on
|
||||||
|
fc.codigofuncpessoa = fp.codigofuncpessoa
|
||||||
|
join
|
||||||
|
outraempresa oe on
|
||||||
|
d.codigooutemp = oe.codigooutemp
|
||||||
|
left join
|
||||||
|
dependente dep on
|
||||||
|
d.codigoempresa = dep.codigoempresa
|
||||||
|
and d.codigofunccontr = dep.codigofunccontr
|
||||||
|
and d.codigodepend = dep.codigodepend
|
||||||
|
where
|
||||||
|
d.codigoempresa = :codigo_empresa
|
||||||
|
and d.codigooutemp = :codigo_operadora
|
||||||
|
and (fc.datadem is null or fc.datadem >= :competencia)
|
||||||
|
order by
|
||||||
|
fp.nomefunc,
|
||||||
|
fp.cpffunc,
|
||||||
|
codigooperadora
|
||||||
|
""")
|
||||||
|
return SQLQuery(
|
||||||
|
sql=sql,
|
||||||
|
params={
|
||||||
|
"codigo_empresa": codigo_empresa,
|
||||||
|
"codigo_operadora": codigo_operadora,
|
||||||
|
"competencia": competencia,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@ -61,6 +61,85 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filtro "estilo Excel" por coluna do histórico (ips-list-table) — botão de
|
||||||
|
funil dentro do próprio <th> de ordenação (não conflita com o clique de
|
||||||
|
ordenar: o JS ignora cliques dentro de .ips-col-filter antes de tratar
|
||||||
|
como ordenação) + popup com busca/checklist (reaproveita .checklist-*
|
||||||
|
de components.css, mesmo componente já usado nos checklists de Perfis de
|
||||||
|
Acesso/Departamento). .pa-table-wrap normalmente usa overflow:hidden pra
|
||||||
|
arredondar os cantos da tabela — sem abrir mão disso pra tabela nenhuma,
|
||||||
|
ips-list-table-wrap (só esta tabela) sobrescreve pra visible, senão o
|
||||||
|
popup (que precisa "vazar" pra fora do cabeçalho, sobre as linhas) seria
|
||||||
|
cortado ali. */
|
||||||
|
.ips-list-table-wrap {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__btn.is-active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__popup {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 20;
|
||||||
|
width: 220px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--bg-surface-raised);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||||
|
text-align: left;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__popup .checklist-box {
|
||||||
|
max-height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-col-filter__actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Paginação do histórico — só Anterior/Próxima + "Página X de Y", sem
|
/* Paginação do histórico — só Anterior/Próxima + "Página X de Y", sem
|
||||||
numeração de páginas (lista tende a ser curta; um seletor de tamanho de
|
numeração de páginas (lista tende a ser curta; um seletor de tamanho de
|
||||||
página, como em ramais-lookup.js, seria demais pra esse caso). */
|
página, como em ramais-lookup.js, seria demais pra esse caso). */
|
||||||
@ -144,6 +223,16 @@
|
|||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toggle "Buscar automaticamente do Questor" / "Anexar manualmente" — só
|
||||||
|
dois radios lado a lado, mesmo espírito de .ips-custeio-radios (radio +
|
||||||
|
.modal-checkbox), sem o recuo/coluna deles porque aqui não há
|
||||||
|
titular/dependente por baixo, só a escolha em si. */
|
||||||
|
.ips-planilha-origem-opcoes {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-5);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
.ips-file-field {
|
.ips-file-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -518,6 +607,21 @@
|
|||||||
background: var(--bg-canvas);
|
background: var(--bg-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Célula não editável (todo campo fora de Valor Empresa/Valor, numa linha
|
||||||
|
que não foi incluída manualmente via "Adicionar linha") — mesmo padding
|
||||||
|
de .ips-table-input, pra não desalinhar a coluna, mas sem borda/hover de
|
||||||
|
input, já que não é editável. */
|
||||||
|
.ips-table-value {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 30px;
|
||||||
|
padding: var(--space-2);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.ips-table-scroll {
|
.ips-table-scroll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 420px;
|
height: 420px;
|
||||||
|
|||||||
@ -87,6 +87,10 @@ async function pidCriarImportacaoPlanoSaude(formData) {
|
|||||||
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData });
|
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pidReabrirImportacaoPlanoSaude(id) {
|
||||||
|
return pidApiRequest(`/importacoes-plano-saude/${id}/reabrir/`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
|
async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("tipo", tipo);
|
formData.append("tipo", tipo);
|
||||||
@ -214,6 +218,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const formPlanilhaName = document.getElementById("ips-form-planilha-name");
|
const formPlanilhaName = document.getElementById("ips-form-planilha-name");
|
||||||
const formPlanilhaRemoveBtn = document.getElementById("ips-form-planilha-remove");
|
const formPlanilhaRemoveBtn = document.getElementById("ips-form-planilha-remove");
|
||||||
const formPlanilhaStatus = document.getElementById("ips-form-planilha-status");
|
const formPlanilhaStatus = document.getElementById("ips-form-planilha-status");
|
||||||
|
// Origem da planilha padrão: buscar do Questor (padrão) ou anexar
|
||||||
|
// manualmente (alternativa — Questor fora do ar, ou empresa ainda não
|
||||||
|
// migrada). Ver ImportacaoPlanoSaudeCreateSerializer.validate() no
|
||||||
|
// backend, que exige exatamente uma das duas.
|
||||||
|
const planilhaOrigemRadios = Array.from(document.querySelectorAll('input[name="ips-planilha-origem"]'));
|
||||||
|
const planilhaQuestorField = document.getElementById("ips-planilha-questor-field");
|
||||||
|
const planilhaUploadField = document.getElementById("ips-planilha-upload-field");
|
||||||
|
const formCompetencia = document.getElementById("ips-form-competencia");
|
||||||
const formArquivo = document.getElementById("ips-form-arquivo");
|
const formArquivo = document.getElementById("ips-form-arquivo");
|
||||||
const formArquivoName = document.getElementById("ips-form-arquivo-name");
|
const formArquivoName = document.getElementById("ips-form-arquivo-name");
|
||||||
const formArquivoRemoveBtn = document.getElementById("ips-form-arquivo-remove");
|
const formArquivoRemoveBtn = document.getElementById("ips-form-arquivo-remove");
|
||||||
@ -296,6 +308,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const reviewRegraEmpresaObsTexto = document.getElementById("ips-review-regra-empresa-obs-texto");
|
const reviewRegraEmpresaObsTexto = document.getElementById("ips-review-regra-empresa-obs-texto");
|
||||||
const reviewError = document.getElementById("ips-review-error");
|
const reviewError = document.getElementById("ips-review-error");
|
||||||
const reviewBackBtn = document.getElementById("ips-review-back-btn");
|
const reviewBackBtn = document.getElementById("ips-review-back-btn");
|
||||||
|
const reviewEditarBtn = document.getElementById("ips-review-editar-btn");
|
||||||
const gerarBtn = document.getElementById("ips-gerar-btn");
|
const gerarBtn = document.getElementById("ips-gerar-btn");
|
||||||
const tabsEl = document.getElementById("ips-tabs");
|
const tabsEl = document.getElementById("ips-tabs");
|
||||||
const panelsEl = document.getElementById("ips-tab-panels");
|
const panelsEl = document.getElementById("ips-tab-panels");
|
||||||
@ -314,6 +327,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
let listSortKey = "criado_em";
|
let listSortKey = "criado_em";
|
||||||
let listSortDir = -1;
|
let listSortDir = -1;
|
||||||
let listPage = 1;
|
let listPage = 1;
|
||||||
|
// Filtros da lista principal, estilo Excel — client-side, sobre o array
|
||||||
|
// já carregado. Cada chave é a mesma de PID_IPS_LISTA_SORT_VALOR (não por
|
||||||
|
// acaso: é o extrator de valor bruto reaproveitado tanto pra ordenar
|
||||||
|
// quanto pra filtrar essa coluna) e vale `null` (sem filtro, mostra tudo)
|
||||||
|
// ou um Set com os valores marcados no popup daquela coluna; combinam
|
||||||
|
// entre si (AND) — ex.: empresa "221" + operadora "3755 - Itamed Saúde"
|
||||||
|
// já restringe pras duas condições juntas.
|
||||||
|
let listFiltros = { codigo_empresa: null, nome_operadora: null, status: null, criado_por: null };
|
||||||
let selectedIds = new Set();
|
let selectedIds = new Set();
|
||||||
let currentPageIds = [];
|
let currentPageIds = [];
|
||||||
let colWidthsLinha = {};
|
let colWidthsLinha = {};
|
||||||
@ -449,6 +470,57 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
getOperadoraKey: () => formOperadora.value,
|
getOperadoraKey: () => formOperadora.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function planilhaOrigemAtual() {
|
||||||
|
const marcado = planilhaOrigemRadios.find((r) => r.checked);
|
||||||
|
return marcado ? marcado.value : "questor";
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarPlanilhaOrigemView() {
|
||||||
|
const questor = planilhaOrigemAtual() === "questor";
|
||||||
|
planilhaQuestorField.hidden = !questor;
|
||||||
|
planilhaUploadField.hidden = questor;
|
||||||
|
// Trocar de modo limpa o outro — evita enviar um arquivo esquecido
|
||||||
|
// junto com uma competência (ou vice-versa), o que o backend recusaria
|
||||||
|
// mesmo (ImportacaoPlanoSaudeCreateSerializer exige exatamente uma
|
||||||
|
// origem).
|
||||||
|
if (questor) {
|
||||||
|
validadorPlanilha.limpar();
|
||||||
|
} else {
|
||||||
|
formCompetencia.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
planilhaOrigemRadios.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", atualizarPlanilhaOrigemView);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Máscara MM/AAAA de digitação — mesmo padrão já usado em
|
||||||
|
// pidIndMascaraCompetencia/pidIndCompetenciaParaIso (indicador-desempenho.js),
|
||||||
|
// copiado aqui em vez de compartilhado (cada arquivo mantém sua própria
|
||||||
|
// cópia dessas funções pequenas, mesmo padrão de comparaValoresUsuario
|
||||||
|
// etc.). Usuário só digita "082026" (ou já com a barra) e o campo forma
|
||||||
|
// "08/2026" sozinho; nunca usar um `<input type="month">` aqui — o
|
||||||
|
// seletor nativo do browser foi considerado horrível pelo usuário.
|
||||||
|
function mascaraCompetencia(valor) {
|
||||||
|
const digitos = (valor || "").replace(/\D/g, "").slice(0, 6);
|
||||||
|
if (digitos.length <= 2) return digitos;
|
||||||
|
return `${digitos.slice(0, 2)}/${digitos.slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function competenciaParaIso(valor) {
|
||||||
|
const match = /^(\d{2})\/(\d{4})$/.exec(valor || "");
|
||||||
|
if (!match) return null;
|
||||||
|
const mes = Number(match[1]);
|
||||||
|
if (mes < 1 || mes > 12) return null;
|
||||||
|
return `${match[2]}-${match[1]}-01`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formCompetencia) {
|
||||||
|
formCompetencia.addEventListener("input", (event) => {
|
||||||
|
event.target.value = mascaraCompetencia(event.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showView(view) {
|
function showView(view) {
|
||||||
listView.hidden = view !== "list";
|
listView.hidden = view !== "list";
|
||||||
formView.hidden = view !== "form";
|
formView.hidden = view !== "form";
|
||||||
@ -483,6 +555,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
criado_em: (imp) => imp.criado_em || "",
|
criado_em: (imp) => imp.criado_em || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Colunas com filtro "estilo Excel" no cabeçalho do histórico — mesma
|
||||||
|
// chave de PID_IPS_LISTA_SORT_VALOR, reaproveitado como extrator do valor
|
||||||
|
// bruto de cada linha pro popup também (ver criarFiltroColuna). "status"
|
||||||
|
// ganha um rótulo amigável no popup (a lista mostra "Em revisão", não
|
||||||
|
// "revisao"), as outras exibem o próprio valor.
|
||||||
|
const PID_IPS_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" };
|
||||||
|
|
||||||
|
function labelValorFiltroLista(campo, valor) {
|
||||||
|
if (campo === "status") return PID_IPS_STATUS_LABELS[valor] || valor;
|
||||||
|
return valor || "(Vazio)";
|
||||||
|
}
|
||||||
|
|
||||||
function renderListPagination(totalPaginas) {
|
function renderListPagination(totalPaginas) {
|
||||||
if (totalPaginas <= 1) {
|
if (totalPaginas <= 1) {
|
||||||
listPagination.innerHTML = "";
|
listPagination.innerHTML = "";
|
||||||
@ -515,11 +599,23 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
if (listBulkCount) listBulkCount.textContent = `${count} selecionada${count === 1 ? "" : "s"}`;
|
if (listBulkCount) listBulkCount.textContent = `${count} selecionada${count === 1 ? "" : "s"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importacaoPassaNosFiltros(imp) {
|
||||||
|
return Object.entries(listFiltros).every(([campo, valoresPermitidos]) => {
|
||||||
|
if (!valoresPermitidos) return true; // null = sem filtro nessa coluna
|
||||||
|
return valoresPermitidos.has(PID_IPS_LISTA_SORT_VALOR[campo](imp));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listEmpty.hidden = importacoes.length !== 0;
|
const filtradas = importacoes.filter(importacaoPassaNosFiltros);
|
||||||
|
listEmpty.hidden = filtradas.length !== 0;
|
||||||
|
listEmpty.textContent =
|
||||||
|
importacoes.length === 0
|
||||||
|
? "Nenhuma importação realizada ainda."
|
||||||
|
: "Nenhuma importação encontrada com esse filtro.";
|
||||||
|
|
||||||
const extrator = PID_IPS_LISTA_SORT_VALOR[listSortKey];
|
const extrator = PID_IPS_LISTA_SORT_VALOR[listSortKey];
|
||||||
const ordenadas = [...importacoes].sort((a, b) => comparaValoresLista(extrator(a), extrator(b)) * listSortDir);
|
const ordenadas = [...filtradas].sort((a, b) => comparaValoresLista(extrator(a), extrator(b)) * listSortDir);
|
||||||
|
|
||||||
const totalPaginas = Math.max(1, Math.ceil(ordenadas.length / IPS_LIST_PAGE_SIZE));
|
const totalPaginas = Math.max(1, Math.ceil(ordenadas.length / IPS_LIST_PAGE_SIZE));
|
||||||
listPage = Math.min(listPage, totalPaginas);
|
listPage = Math.min(listPage, totalPaginas);
|
||||||
@ -1200,7 +1296,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
regracadNovoEmpresaNome.textContent = resultado.nome_empresa;
|
regracadNovoEmpresaNome.textContent = resultado.nome_empresa;
|
||||||
} else {
|
} else {
|
||||||
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--nao-encontrada";
|
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--nao-encontrada";
|
||||||
regracadNovoEmpresaNome.textContent = "Empresa não encontrada no Questor.";
|
regracadNovoEmpresaNome.textContent = "Empresa não encontrada.";
|
||||||
}
|
}
|
||||||
// O backend só busca/salva o código normalizado (sem zero à esquerda,
|
// O backend só busca/salva o código normalizado (sem zero à esquerda,
|
||||||
// ex. "092" -> "92") — reflete isso no campo assim que a resposta
|
// ex. "092" -> "92") — reflete isso no campo assim que a resposta
|
||||||
@ -1317,6 +1413,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
validadorPlanilha.limpar();
|
validadorPlanilha.limpar();
|
||||||
validadorArquivo.limpar();
|
validadorArquivo.limpar();
|
||||||
formError.textContent = "";
|
formError.textContent = "";
|
||||||
|
formCompetencia.value = "";
|
||||||
|
const radioQuestor = planilhaOrigemRadios.find((r) => r.value === "questor");
|
||||||
|
if (radioQuestor) radioQuestor.checked = true;
|
||||||
|
atualizarPlanilhaOrigemView();
|
||||||
regraResolvidaAtual = null;
|
regraResolvidaAtual = null;
|
||||||
renderResumoRegra(null);
|
renderResumoRegra(null);
|
||||||
impEmpresaOps.limpar();
|
impEmpresaOps.limpar();
|
||||||
@ -1354,8 +1454,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : "";
|
const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : "";
|
||||||
|
// "competencia" só vem preenchida quando a planilha padrão foi buscada
|
||||||
|
// do Questor (não upload manual) — ver ImportacaoPlanoSaude.competencia.
|
||||||
|
const competenciaSufixo = importacao.competencia
|
||||||
|
? ` — Competência: ${importacao.competencia.slice(5, 7)}/${importacao.competencia.slice(0, 4)}`
|
||||||
|
: "";
|
||||||
reviewSubtitle.textContent =
|
reviewSubtitle.textContent =
|
||||||
`${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}`;
|
`${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}${competenciaSufixo}`;
|
||||||
if (reviewRegraEmpresaObs) {
|
if (reviewRegraEmpresaObs) {
|
||||||
// As duas regras (empresa e de custeio salva) são mutuamente exclusivas
|
// As duas regras (empresa e de custeio salva) são mutuamente exclusivas
|
||||||
// na criação — nunca as duas preenchidas ao mesmo tempo — mas o bloco é
|
// na criação — nunca as duas preenchidas ao mesmo tempo — mas o bloco é
|
||||||
@ -1380,6 +1485,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
|
if (reviewEditarBtn) {
|
||||||
|
reviewEditarBtn.hidden = importacaoAtual.status !== "concluida";
|
||||||
|
}
|
||||||
const tipos = importacaoAtual.tipos_lancamento || [];
|
const tipos = importacaoAtual.tipos_lancamento || [];
|
||||||
const tabKeys = [...tipos, "auditoria", "alteracoes"];
|
const tabKeys = [...tipos, "auditoria", "alteracoes"];
|
||||||
if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0];
|
if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0];
|
||||||
@ -1459,8 +1567,33 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Colunas sempre editáveis, em qualquer linha — o resto do cadastro
|
||||||
|
// (nome, CPF, código, data...) já vem certo do casamento automático (ou
|
||||||
|
// da busca no Questor) e só devia ser tocado numa linha incluída à mão
|
||||||
|
// (ver linhasIncluidasManualmente() abaixo), nunca numa linha existente
|
||||||
|
// por engano.
|
||||||
|
const PID_IPS_CAMPOS_SEMPRE_EDITAVEIS = ["valor_empresa", "valor"];
|
||||||
|
|
||||||
|
// Uma linha é "incluída manualmente" se tiver um ImportacaoPlanoSaudeAlteracao
|
||||||
|
// tipo="inclusao" ainda não revertido apontando pra ela — mesmo sinal que já
|
||||||
|
// existia pra alimentar a aba "Alterações", não precisou de nenhum campo
|
||||||
|
// novo no backend. Só essas linhas ganham os demais campos editáveis;
|
||||||
|
// qualquer linha que já veio do processamento (upload ou Questor) só edita
|
||||||
|
// Valor Empresa/Valor.
|
||||||
|
function linhasIncluidasManualmente() {
|
||||||
|
const ids = new Set();
|
||||||
|
(importacaoAtual.alteracoes || []).forEach((alt) => {
|
||||||
|
if (alt.tipo === "inclusao" && !alt.revertida && alt.linha) ids.add(alt.linha);
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function panelHtmlParaTipo(tipo) {
|
function panelHtmlParaTipo(tipo) {
|
||||||
const campos = PID_IPS_CAMPOS_LINHA;
|
const campos = PID_IPS_CAMPOS_LINHA;
|
||||||
|
// Importação concluída: nada é editável até reabrir (botão "Editar" no
|
||||||
|
// cabeçalho da Revisão) — nem os valores, nem adicionar/remover linha.
|
||||||
|
const concluida = importacaoAtual.status === "concluida";
|
||||||
|
const incluidasManualmente = linhasIncluidasManualmente();
|
||||||
let linhas = (importacaoAtual.linhas || []).filter((l) => l.tipo_lancamento === tipo);
|
let linhas = (importacaoAtual.linhas || []).filter((l) => l.tipo_lancamento === tipo);
|
||||||
if (linhaSortKey) {
|
if (linhaSortKey) {
|
||||||
linhas = [...linhas].sort((a, b) => comparaValorRevisao(a[linhaSortKey], b[linhaSortKey]) * linhaSortDir);
|
linhas = [...linhas].sort((a, b) => comparaValorRevisao(a[linhaSortKey], b[linhaSortKey]) * linhaSortDir);
|
||||||
@ -1475,22 +1608,27 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const linhasHtml = linhas
|
const linhasHtml = linhas
|
||||||
.map(
|
.map((linha) => {
|
||||||
(linha) => `
|
const editavelLivre = !concluida && incluidasManualmente.has(linha.id);
|
||||||
<tr data-linha-id="${linha.id}">
|
const celulasHtml = campos
|
||||||
<td class="ips-table-actions">
|
.map(([campo]) => {
|
||||||
<button type="button" class="icon-btn ips-remove-linha" data-remover-linha="${linha.id}" aria-label="Remover linha" title="Remover linha">
|
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>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const removerHtml = concluida
|
||||||
|
? ""
|
||||||
|
: `<button type="button" class="icon-btn ips-remove-linha" data-remover-linha="${linha.id}" aria-label="Remover linha" title="Remover linha">
|
||||||
<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>
|
<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>
|
</button>`;
|
||||||
</td>
|
return `
|
||||||
${campos
|
<tr data-linha-id="${linha.id}">
|
||||||
.map(
|
<td class="ips-table-actions">${removerHtml}</td>
|
||||||
([campo]) =>
|
${celulasHtml}
|
||||||
`<td><input type="text" class="ips-table-input" data-linha-campo="${campo}" value="${escapeAttr(linha[campo])}" /></td>`
|
</tr>`;
|
||||||
)
|
})
|
||||||
.join("")}
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@ -1506,12 +1644,22 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
<tbody>${linhasHtml || `<tr class="pa-empty-row"><td colspan="${campos.length + 1}">Nenhuma linha.</td></tr>`}</tbody>
|
<tbody>${linhasHtml || `<tr class="pa-empty-row"><td colspan="${campos.length + 1}">Nenhuma linha.</td></tr>`}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
${concluida ? "" : `
|
||||||
<div class="ips-add-row">
|
<div class="ips-add-row">
|
||||||
<button type="button" class="btn-outline" data-ips-add-linha="${tipo}">
|
<button type="button" class="btn-outline" data-ips-add-linha="${tipo}">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
|
||||||
Adicionar linha
|
Adicionar linha
|
||||||
</button>
|
</button>
|
||||||
</div>`;
|
</div>`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valor sintético (não vem da API) só pra ordenar a coluna "Ação" —
|
||||||
|
// prefixo numérico garante o agrupamento (pendente/resolvido/sem ação)
|
||||||
|
// independente da ordenação alfabética do texto exibido em cada estado.
|
||||||
|
function auditoriaAcaoOrdem(item) {
|
||||||
|
if (item.resolvida) return "1-resolvido";
|
||||||
|
if (PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) return "0-pendente";
|
||||||
|
return "2-sem-acao";
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelHtmlAuditoria() {
|
function panelHtmlAuditoria() {
|
||||||
@ -1521,7 +1669,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
return `<p class="ips-empty">Nenhum item em auditoria — tudo foi lançado automaticamente.</p>`;
|
return `<p class="ips-empty">Nenhum item em auditoria — tudo foi lançado automaticamente.</p>`;
|
||||||
}
|
}
|
||||||
if (auditoriaSortKey) {
|
if (auditoriaSortKey) {
|
||||||
itens = [...itens].sort((a, b) => comparaValorRevisao(a[auditoriaSortKey], b[auditoriaSortKey]) * auditoriaSortDir);
|
const valorOrdenacao =
|
||||||
|
auditoriaSortKey === "acao" ? auditoriaAcaoOrdem : (item) => item[auditoriaSortKey];
|
||||||
|
itens = [...itens].sort(
|
||||||
|
(a, b) => comparaValorRevisao(valorOrdenacao(a), valorOrdenacao(b)) * auditoriaSortDir
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const linhasHtml = itens
|
const linhasHtml = itens
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@ -1530,7 +1682,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
acaoHtml = `<span class="ips-auditoria-resolvido" title="Vinculado a: ${escapeAttr(item.linha_vinculada_nome || "")}">Resolvido${
|
acaoHtml = `<span class="ips-auditoria-resolvido" title="Vinculado a: ${escapeAttr(item.linha_vinculada_nome || "")}">Resolvido${
|
||||||
item.linha_vinculada_nome ? ` — ${item.linha_vinculada_nome}` : ""
|
item.linha_vinculada_nome ? ` — ${item.linha_vinculada_nome}` : ""
|
||||||
}</span>`;
|
}</span>`;
|
||||||
} else if (PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) {
|
} else if (importacaoAtual.status !== "concluida" && PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) {
|
||||||
acaoHtml = `<button type="button" class="btn-outline ips-vincular-btn" data-vincular-auditoria="${item.id}">Vincular pessoa</button>`;
|
acaoHtml = `<button type="button" class="btn-outline ips-vincular-btn" data-vincular-auditoria="${item.id}">Vincular pessoa</button>`;
|
||||||
}
|
}
|
||||||
return `
|
return `
|
||||||
@ -1551,7 +1703,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
<table class="pa-table">
|
<table class="pa-table">
|
||||||
${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}
|
${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th class="ips-table-actions">Ação</th>${campos
|
<tr><th class="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
|
||||||
.map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir }))
|
.map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir }))
|
||||||
.join("")}</tr>
|
.join("")}</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -1584,6 +1738,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
const acaoHtml = alt.revertida
|
const acaoHtml = alt.revertida
|
||||||
? `<span class="ips-alteracao-revertida">Revertida</span>`
|
? `<span class="ips-alteracao-revertida">Revertida</span>`
|
||||||
|
: importacaoAtual.status === "concluida"
|
||||||
|
? `<span class="ips-empty-cell">—</span>`
|
||||||
: `<button type="button" class="btn-outline ips-reverter-btn" data-reverter-alteracao="${alt.id}">Reverter</button>`;
|
: `<button type="button" class="btn-outline ips-reverter-btn" data-reverter-alteracao="${alt.id}">Reverter</button>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@ -1736,6 +1892,21 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reviewEditarBtn) {
|
||||||
|
reviewEditarBtn.addEventListener("click", async () => {
|
||||||
|
reviewEditarBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
importacaoAtual = await pidReabrirImportacaoPlanoSaude(importacaoAtual.id);
|
||||||
|
reviewError.textContent = "";
|
||||||
|
renderTabs();
|
||||||
|
} catch (e) {
|
||||||
|
reviewError.textContent = e.message;
|
||||||
|
} finally {
|
||||||
|
reviewEditarBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (regracadBtn) regracadBtn.addEventListener("click", abrirCadastroRegras);
|
if (regracadBtn) regracadBtn.addEventListener("click", abrirCadastroRegras);
|
||||||
if (regracadFecharBtn) regracadFecharBtn.addEventListener("click", fecharCadastroRegras);
|
if (regracadFecharBtn) regracadFecharBtn.addEventListener("click", fecharCadastroRegras);
|
||||||
if (regracadModal) {
|
if (regracadModal) {
|
||||||
@ -2062,6 +2233,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
gerarBtn.disabled = true;
|
gerarBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await pidGerarArquivoPlanoSaude(importacaoAtual.id);
|
await pidGerarArquivoPlanoSaude(importacaoAtual.id);
|
||||||
|
// Depois de baixar o arquivo, volta pro histórico — mesmo pedido do
|
||||||
|
// usuário pro fluxo de "Nova Importação" (abrirRevisao já leva pra
|
||||||
|
// lá), já que gerar() marca a importação como concluída e não há
|
||||||
|
// mais nada a revisar imediatamente depois de baixar o arquivo.
|
||||||
|
showView("list");
|
||||||
|
await refreshList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reviewError.textContent = e.message;
|
reviewError.textContent = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
@ -2144,7 +2321,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listSortHeaders.forEach((th) => {
|
listSortHeaders.forEach((th) => {
|
||||||
th.addEventListener("click", () => {
|
th.addEventListener("click", (event) => {
|
||||||
|
// Ignora cliques que vieram de dentro do popup de filtro (ver
|
||||||
|
// criarFiltroColuna abaixo) — sem isso, abrir o filtro ou marcar um
|
||||||
|
// checkbox dele também reordenava a coluna, já que o filtro mora
|
||||||
|
// dentro do mesmo <th> clicável.
|
||||||
|
if (event.target.closest(".ips-col-filter")) return;
|
||||||
const key = th.getAttribute("data-sort");
|
const key = th.getAttribute("data-sort");
|
||||||
if (listSortKey === key) {
|
if (listSortKey === key) {
|
||||||
listSortDir *= -1;
|
listSortDir *= -1;
|
||||||
@ -2157,6 +2339,148 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filtro "estilo Excel" por coluna do histórico — botão de funil dentro
|
||||||
|
// do próprio <th> de ordenação, popup com busca + checklist de valores
|
||||||
|
// distintos (reaproveita .checklist-* de components.css). `filtrosColuna`
|
||||||
|
// guarda um `fechar()` por coluna, pra fechar todos ao abrir outro ou ao
|
||||||
|
// clicar fora de qualquer um deles.
|
||||||
|
const filtrosColuna = [];
|
||||||
|
|
||||||
|
function fecharTodosOsFiltrosColuna() {
|
||||||
|
filtrosColuna.forEach((f) => f.fechar());
|
||||||
|
}
|
||||||
|
|
||||||
|
function criarFiltroColuna(campo) {
|
||||||
|
const th = listSortHeaders.find((el) => el.getAttribute("data-sort") === campo);
|
||||||
|
if (!th) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("span");
|
||||||
|
wrapper.className = "ips-col-filter";
|
||||||
|
wrapper.innerHTML = `
|
||||||
|
<button type="button" class="ips-col-filter__btn" aria-label="Filtrar coluna" aria-expanded="false" title="Filtrar">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" stroke-linejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="ips-col-filter__popup" hidden>
|
||||||
|
<input type="text" class="checklist-search" placeholder="Buscar..." />
|
||||||
|
<label class="modal-checkbox checklist-select-all"><input type="checkbox" checked /> Selecionar todos</label>
|
||||||
|
<div class="checklist-box"></div>
|
||||||
|
<div class="ips-col-filter__actions">
|
||||||
|
<button type="button" class="btn-ghost ips-col-filter__limpar">Limpar</button>
|
||||||
|
<button type="button" class="btn-solid ips-col-filter__aplicar">Aplicar</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
th.appendChild(wrapper);
|
||||||
|
|
||||||
|
const btn = wrapper.querySelector(".ips-col-filter__btn");
|
||||||
|
const popup = wrapper.querySelector(".ips-col-filter__popup");
|
||||||
|
const searchInput = wrapper.querySelector(".checklist-search");
|
||||||
|
const selectAllInput = wrapper.querySelector(".checklist-select-all input");
|
||||||
|
const box = wrapper.querySelector(".checklist-box");
|
||||||
|
const limparBtn = wrapper.querySelector(".ips-col-filter__limpar");
|
||||||
|
const aplicarBtn = wrapper.querySelector(".ips-col-filter__aplicar");
|
||||||
|
const extrator = PID_IPS_LISTA_SORT_VALOR[campo];
|
||||||
|
|
||||||
|
let marcados = null; // Set em edição — só existe enquanto o popup está aberto
|
||||||
|
|
||||||
|
function valoresDistintos() {
|
||||||
|
return Array.from(new Set(importacoes.map(extrator))).sort((a, b) =>
|
||||||
|
labelValorFiltroLista(campo, a).localeCompare(labelValorFiltroLista(campo, b), "pt-BR", { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItens() {
|
||||||
|
const termo = searchInput.value.trim().toLowerCase();
|
||||||
|
const visiveis = valoresDistintos().filter((v) => labelValorFiltroLista(campo, v).toLowerCase().includes(termo));
|
||||||
|
box.innerHTML = visiveis.length
|
||||||
|
? visiveis
|
||||||
|
.map(
|
||||||
|
(v) =>
|
||||||
|
`<label class="checklist-item"><input type="checkbox" data-valor-filtro="${escapeAttr(v)}" ${marcados.has(v) ? "checked" : ""} /><span class="checklist-item__info">${escapeHtml(labelValorFiltroLista(campo, v))}</span></label>`
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: `<p class="checklist-empty">Nenhum valor encontrado.</p>`;
|
||||||
|
selectAllInput.checked = visiveis.length > 0 && visiveis.every((v) => marcados.has(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrir() {
|
||||||
|
// `marcados` sempre recomeça a partir do filtro já aplicado (ou de
|
||||||
|
// todos os valores existentes, se a coluna ainda não tem filtro) —
|
||||||
|
// nunca do que ficou de uma edição anterior cancelada sem "Aplicar".
|
||||||
|
marcados = listFiltros[campo] ? new Set(listFiltros[campo]) : new Set(valoresDistintos());
|
||||||
|
searchInput.value = "";
|
||||||
|
renderItens();
|
||||||
|
popup.hidden = false;
|
||||||
|
btn.setAttribute("aria-expanded", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fechar() {
|
||||||
|
popup.hidden = true;
|
||||||
|
marcados = null;
|
||||||
|
btn.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function atualizarBotaoAtivo() {
|
||||||
|
btn.classList.toggle("is-active", !!listFiltros[campo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const estavaAberto = !popup.hidden;
|
||||||
|
fecharTodosOsFiltrosColuna();
|
||||||
|
if (!estavaAberto) abrir();
|
||||||
|
});
|
||||||
|
|
||||||
|
popup.addEventListener("click", (event) => event.stopPropagation());
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", renderItens);
|
||||||
|
|
||||||
|
selectAllInput.addEventListener("change", () => {
|
||||||
|
const termo = searchInput.value.trim().toLowerCase();
|
||||||
|
const visiveis = valoresDistintos().filter((v) => labelValorFiltroLista(campo, v).toLowerCase().includes(termo));
|
||||||
|
if (selectAllInput.checked) visiveis.forEach((v) => marcados.add(v));
|
||||||
|
else visiveis.forEach((v) => marcados.delete(v));
|
||||||
|
renderItens();
|
||||||
|
});
|
||||||
|
|
||||||
|
box.addEventListener("change", (event) => {
|
||||||
|
const input = event.target.closest("[data-valor-filtro]");
|
||||||
|
if (!input) return;
|
||||||
|
const valor = input.getAttribute("data-valor-filtro");
|
||||||
|
if (input.checked) marcados.add(valor);
|
||||||
|
else marcados.delete(valor);
|
||||||
|
renderItens();
|
||||||
|
});
|
||||||
|
|
||||||
|
limparBtn.addEventListener("click", () => {
|
||||||
|
listFiltros[campo] = null;
|
||||||
|
fechar();
|
||||||
|
atualizarBotaoAtivo();
|
||||||
|
listPage = 1;
|
||||||
|
renderList();
|
||||||
|
});
|
||||||
|
|
||||||
|
aplicarBtn.addEventListener("click", () => {
|
||||||
|
const todos = valoresDistintos();
|
||||||
|
// Marcar tudo equivale a "sem filtro" — mantém null nesse caso, senão
|
||||||
|
// um valor novo que apareça depois (operadora nova, por exemplo)
|
||||||
|
// ficaria de fora até o usuário marcá-lo manualmente de novo.
|
||||||
|
const marcouTudo = todos.length === marcados.size && todos.every((v) => marcados.has(v));
|
||||||
|
listFiltros[campo] = marcouTudo ? null : new Set(marcados);
|
||||||
|
fechar();
|
||||||
|
atualizarBotaoAtivo();
|
||||||
|
listPage = 1;
|
||||||
|
renderList();
|
||||||
|
});
|
||||||
|
|
||||||
|
filtrosColuna.push({ fechar });
|
||||||
|
}
|
||||||
|
|
||||||
|
["codigo_empresa", "nome_operadora", "status", "criado_por"].forEach(criarFiltroColuna);
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target.closest(".ips-col-filter")) fecharTodosOsFiltrosColuna();
|
||||||
|
});
|
||||||
|
|
||||||
if (listPagination) {
|
if (listPagination) {
|
||||||
listPagination.addEventListener("click", (event) => {
|
listPagination.addEventListener("click", (event) => {
|
||||||
if (event.target.closest("#ips-list-prev-btn")) {
|
if (event.target.closest("#ips-list-prev-btn")) {
|
||||||
@ -2175,22 +2499,40 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
formError.textContent = "Selecione a empresa e a operadora.";
|
formError.textContent = "Selecione a empresa e a operadora.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const origemQuestor = planilhaOrigemAtual() === "questor";
|
||||||
|
let competenciaIso = null;
|
||||||
|
if (origemQuestor) {
|
||||||
|
competenciaIso = competenciaParaIso(formCompetencia.value);
|
||||||
|
if (!competenciaIso) {
|
||||||
|
formError.textContent = "Informe a competência no formato MM/AAAA para buscar a planilha no Questor.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!formPlanilha.files[0]) {
|
if (!formPlanilha.files[0]) {
|
||||||
formError.textContent = "Anexe a planilha padrão do Questor.";
|
formError.textContent = "Anexe a planilha padrão do Questor.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (validadorPlanilha.ehInvalido()) {
|
||||||
|
formError.textContent = "Corrija a planilha anexada (veja a mensagem abaixo do campo) antes de processar.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!formArquivo.files[0]) {
|
if (!formArquivo.files[0]) {
|
||||||
formError.textContent = "Anexe o arquivo da operadora.";
|
formError.textContent = "Anexe o arquivo da operadora.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (validadorPlanilha.ehInvalido() || validadorArquivo.ehInvalido()) {
|
if (validadorArquivo.ehInvalido()) {
|
||||||
formError.textContent = "Corrija os arquivos anexados (veja a mensagem abaixo de cada um) antes de processar.";
|
formError.textContent = "Corrija o arquivo da operadora (veja a mensagem abaixo do campo) antes de processar.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formError.textContent = "";
|
formError.textContent = "";
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
if (origemQuestor) {
|
||||||
|
formData.append("competencia", competenciaIso);
|
||||||
|
} else {
|
||||||
formData.append("planilha_padrao", formPlanilha.files[0]);
|
formData.append("planilha_padrao", formPlanilha.files[0]);
|
||||||
|
}
|
||||||
formData.append("arquivo_operadora", formArquivo.files[0]);
|
formData.append("arquivo_operadora", formArquivo.files[0]);
|
||||||
montarFormDataDeRegra(formData, regraResolvidaAtual);
|
montarFormDataDeRegra(formData, regraResolvidaAtual);
|
||||||
|
|
||||||
|
|||||||
@ -318,7 +318,7 @@
|
|||||||
<button type="button" class="btn-danger-outline" id="ips-list-bulk-delete-btn">Excluir selecionadas</button>
|
<button type="button" class="btn-danger-outline" id="ips-list-bulk-delete-btn">Excluir selecionadas</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pa-table-wrap">
|
<div class="pa-table-wrap ips-list-table-wrap">
|
||||||
<table class="pa-table" id="ips-list-table">
|
<table class="pa-table" id="ips-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -344,7 +344,7 @@
|
|||||||
<div class="ips-header">
|
<div class="ips-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Nova Importação</h2>
|
<h2>Nova Importação</h2>
|
||||||
<p class="ips-subtitle">Anexe a planilha padrão do Questor e o relatório enviado pela operadora.</p>
|
<p class="ips-subtitle">Busque a planilha padrão do Questor por competência (ou anexe manualmente) e informe o relatório enviado pela operadora.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-outline" id="ips-form-back-btn">Voltar</button>
|
<button type="button" class="btn-outline" id="ips-form-back-btn">Voltar</button>
|
||||||
</div>
|
</div>
|
||||||
@ -391,8 +391,22 @@
|
|||||||
<label>Arquivos</label>
|
<label>Arquivos</label>
|
||||||
<div class="ips-upload-box">
|
<div class="ips-upload-box">
|
||||||
<p class="ips-upload-box__title">1. Planilha padrão (Questor)</p>
|
<p class="ips-upload-box__title">1. Planilha padrão (Questor)</p>
|
||||||
<p class="ips-upload-box__hint">Modelo em branco exportado do Questor, com o cadastro dos beneficiários (sem valores).</p>
|
<p class="ips-upload-box__hint">Cadastro dos beneficiários (sem valores) — buscado direto do Questor, ou anexado manualmente como alternativa.</p>
|
||||||
<div class="ips-file-field">
|
<div class="ips-planilha-origem-opcoes">
|
||||||
|
<label class="modal-checkbox">
|
||||||
|
<input type="radio" name="ips-planilha-origem" id="ips-planilha-origem-questor" value="questor" checked />
|
||||||
|
Buscar automaticamente do Questor
|
||||||
|
</label>
|
||||||
|
<label class="modal-checkbox">
|
||||||
|
<input type="radio" name="ips-planilha-origem" id="ips-planilha-origem-upload" value="upload" />
|
||||||
|
Anexar manualmente
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="ips-planilha-questor-field" class="modal-field">
|
||||||
|
<label for="ips-form-competencia">Competência</label>
|
||||||
|
<input type="text" id="ips-form-competencia" inputmode="numeric" placeholder="MM/AAAA" maxlength="7" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<div class="ips-file-field" id="ips-planilha-upload-field" hidden>
|
||||||
<label class="btn-outline ips-file-field__btn" for="ips-form-planilha">
|
<label class="btn-outline ips-file-field__btn" for="ips-form-planilha">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
Selecionar arquivo
|
Selecionar arquivo
|
||||||
@ -439,6 +453,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ips-header-actions">
|
<div class="ips-header-actions">
|
||||||
<button type="button" class="btn-outline" id="ips-review-back-btn">Voltar</button>
|
<button type="button" class="btn-outline" id="ips-review-back-btn">Voltar</button>
|
||||||
|
<button type="button" class="btn-outline" id="ips-review-editar-btn" hidden>Editar</button>
|
||||||
<button type="button" class="btn-solid" id="ips-gerar-btn">Gerar Arquivo</button>
|
<button type="button" class="btn-solid" id="ips-gerar-btn">Gerar Arquivo</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user