From 7ac5d2f9ecd94762ae65a4db35d8a597dcb483d2 Mon Sep 17 00:00:00 2001 From: net0well Date: Tue, 28 Jul 2026 22:27:32 -0300 Subject: [PATCH] =?UTF-8?q?feat(web):=20identidade=20visual=20azul,=20e=20?= =?UTF-8?q?corrigir=20a=20manifesta=C3=A7=C3=A3o=20que=20nunca=20saiu=20da?= =?UTF-8?q?=20m=C3=A1quina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A interface abria branca e sem hierarquia, e o título da aba dizia "EcontabilWeb". Identidade Identidade Azul da paleta `azure` como cor de marca — o mais saturado do Material 3 que sustenta AA sobre branco. O fundo da aplicação deixa de ser branco para o cartão ler como cartão. As 24 cores hardcoded viram tokens. Cada tela trazia o seu `rgb(0 0 0 / 60%)` e o seu tom de âmbar; bastava uma divergir para a interface parecer montada por pessoas diferentes, e mudar a identidade exigia caçar literal por literal. Tabelas e avisos passam a ter tratamento único e global, no lugar de cinco cópias quase iguais. A grade ganha cabeçalho que gruda ao rolar, listras e realce na linha sob o cursor — o que sustenta a leitura de doze colunas de imposto. O aviso vira faixa com fundo e barra lateral: âmbar sobre branco sumia no meio da página. O login recebe uma coluna de apresentação e um SVG de fundo desenhado no próprio repositório. A cor sólida é declarada antes da imagem para a tela abrir azul se o SVG demorar, em vez de piscar branco. Fontes empacotadas A compilação da imagem do frontend dependia de alcançar fonts.googleapis.com e quebrou assim, com "Inlining of fonts failed". Em runtime a dependência era pior: rede que não alcança o domínio renderizaria a interface em fonte de sistema, com cada ícone do Material aparecendo como o nome dele escrito por extenso. Roboto e os ícones passam a vir do pacote. Manifestação de ciência O evento nunca chegou à SEFAZ. A configuração pedia a versão 1.00 do RecepcaoEvento, que a biblioteca implementa apenas no .NET Framework — sob .NET a chamada morria antes de sair da máquina. Quem tem implementação é o `RecepcaoEventoManifestacaoDestinatario4`, do layout nacional vigente. Com a chamada saindo, a SEFAZ recusou com cStat 493 por schema. A justificativa era enviada como string vazia, e `xJust` pertence ao evento de operação não realizada: no schema da ciência ele não é admitido, e vazio vira ``. O parâmetro já tinha `null` como padrão na biblioteca. O ciclo estava desligado desde a investigação do consumo indevido, e foi religá-lo que expôs as duas falhas. --- README.md | 6 +- RELEASE.md | 1 - .../Sefaz/ZeusFiscalService.cs | 14 +- src/econtabil-web/README.md | 7 +- src/econtabil-web/angular.json | 6 +- src/econtabil-web/package-lock.json | 17 ++ src/econtabil-web/package.json | 2 + src/econtabil-web/public/login-fundo.svg | 53 +++++ .../src/app/core/layout/shell.component.ts | 84 +++++++- .../certificados/certificados.component.ts | 22 +-- .../features/dashboard/dashboard.component.ts | 26 +-- .../documentos/documento-detalhe.component.ts | 26 +-- .../documentos/documentos.component.ts | 36 +--- .../features/empresas/empresas.component.ts | 28 +-- .../src/app/features/login/login.component.ts | 113 ++++++++++- .../app/shared/badge-situacao.component.ts | 2 +- .../app/shared/badge-validade.component.ts | 4 +- src/econtabil-web/src/index.html | 22 ++- src/econtabil-web/src/styles.scss | 186 ++++++++++++++++-- 19 files changed, 485 insertions(+), 170 deletions(-) create mode 100644 src/econtabil-web/public/login-fundo.svg diff --git a/README.md b/README.md index eacc7b1..4e91df3 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,11 @@ Uma ressalva que vale para qualquer instalação: o ponteiro de NSU pertence ao aplicação. Se outro sistema fiscal já consome a fila daquele CNPJ, a carga inicial não traz histórico — os NSUs anteriores já foram entregues a ele. Não é defeito, é como a SEFAZ define a fila. +A manifestação de ciência também já foi exercitada em produção: o evento 210210 foi aceito pela SEFAZ e +o protocolo ficou gravado no documento. + As pendências conhecidas — todas registradas como decisão, não esquecimento — estão em -[`RELEASE.md`](RELEASE.md). A principal: a manifestação de ciência ainda não foi exercitada contra a -SEFAZ de produção. +[`RELEASE.md`](RELEASE.md). ### Fluxo mínimo para testar de ponta a ponta diff --git a/RELEASE.md b/RELEASE.md index 909734e..d561261 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -53,7 +53,6 @@ daqui só criaria referência que ninguém consegue abrir. |---|---| | Imagem da API com 375 MB contra os 250 MB do NFR | Deploy mais lento e mais banda por release; sem impacto em execução | | Comando de rotação da chave mestra não implementado | Rotacionar `ECONTABIL_CRYPTO_KEY` exige o procedimento manual do `RUNBOOK.md`, com janela de parada | -| Manifestação não exercitada contra a SEFAZ de produção | O caminho de ciência só foi provado por teste; a primeira execução real pode revelar rejeição não prevista | | Consulta avulsa por chave (`consChNFe`) | Nota conhecida por chave só entra no acervo se aparecer na fila de NSU | | E2E de navegador e teste de carga | Regressão de fluxo completo e limite de carga dependem de verificação manual | | Sem tela de usuários no frontend | Criar, editar e desativar operador só por API — `curl` ou Swagger | diff --git a/src/eContabil.Infrastructure/Sefaz/ZeusFiscalService.cs b/src/eContabil.Infrastructure/Sefaz/ZeusFiscalService.cs index 125dfd7..814de47 100644 --- a/src/eContabil.Infrastructure/Sefaz/ZeusFiscalService.cs +++ b/src/eContabil.Infrastructure/Sefaz/ZeusFiscalService.cs @@ -130,7 +130,11 @@ public async Task> ManifestarCienciaAsync( chavesNFe: [.. manifestacao.Chaves], nFeTipoEventoManifestacaoDestinatario: NFeTipoEvento.TeMdCienciaDaOperacao, cpfcnpj: manifestacao.Certificado.CnpjTitular, - justificativa: string.Empty, + + // Nulo, e não string vazia: a justificativa é do evento de operação não realizada, e o + // schema da ciência não admite o campo. Vazio vira `` no XML, que a SEFAZ + // recusa com cStat 493 — "evento não atende o schema XML específico". + justificativa: null, dhEvento: null); var retorno = TradutorDeEventoSefaz.Traduzir(resposta.Retorno); @@ -166,8 +170,14 @@ public async Task> ManifestarCienciaAsync( // Deixá-lo no padrão faz o serviço ser reportado como "não disponível para a UF". ModeloDocumento = ModeloDocumento.NFe, + // A Distribuição DF-e só existe em 1.00: é a versão única do serviço, não um padrão herdado. VersaoNFeDistribuicaoDFe = VersaoServico.Versao100, - VersaoRecepcaoEventoManifestacaoDestinatario = VersaoServico.Versao100, + + // O evento de manifestação, ao contrário, precisa de 4.00. A biblioteca implementa o + // `RecepcaoEvento` 1.00 apenas no .NET Framework e, sob .NET, a chamada morria antes de sair da + // máquina com "RecepcaoEvento 1.0 não está implementado para .net standard". Quem tem + // implementação aqui é o `RecepcaoEventoManifestacaoDestinatario4`, do layout nacional vigente. + VersaoRecepcaoEventoManifestacaoDestinatario = VersaoServico.Versao400, TimeOut = _opcoes.TimeoutMs, ValidarSchemas = _opcoes.ValidarSchemas }; diff --git a/src/econtabil-web/README.md b/src/econtabil-web/README.md index 3825037..e47f6d7 100644 --- a/src/econtabil-web/README.md +++ b/src/econtabil-web/README.md @@ -1,6 +1,9 @@ -# EcontabilWeb +# eContabil — Notas Fiscais -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.19. +Frontend da plataforma. A documentação do produto, do fluxo de captura e da operação está no +[`README.md`](../../README.md) e no [`RUNBOOK.md`](../../RUNBOOK.md) da raiz. + +Gerado com [Angular CLI](https://github.com/angular/angular-cli) 21.2.19. ## Development server diff --git a/src/econtabil-web/angular.json b/src/econtabil-web/angular.json index 5feddd2..7ea0ad0 100644 --- a/src/econtabil-web/angular.json +++ b/src/econtabil-web/angular.json @@ -30,6 +30,10 @@ } ], "styles": [ + "@fontsource/roboto/300.css", + "@fontsource/roboto/400.css", + "@fontsource/roboto/500.css", + "material-icons/iconfont/filled.css", "src/styles.scss" ] }, @@ -38,7 +42,7 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", + "maximumWarning": "600kB", "maximumError": "1MB" }, { diff --git a/src/econtabil-web/package-lock.json b/src/econtabil-web/package-lock.json index f24a499..885c24a 100644 --- a/src/econtabil-web/package-lock.json +++ b/src/econtabil-web/package-lock.json @@ -17,7 +17,9 @@ "@angular/material": "^21.2.14", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@fontsource/roboto": "^5.3.0", "@ngrx/signals": "^21.1.1", + "material-icons": "^1.13.14", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -1678,6 +1680,15 @@ } } }, + "node_modules/@fontsource/roboto": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.3.0.tgz", + "integrity": "sha512-BapRJOWYP+LZ21zp+wBQjfpPYKRoxc4LspJ/RLuI+HSMBD5u/X4O+ESDrSvEqDSy0rAl7GwBJ+09mdc16cVQ1Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@gar/promise-retry": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", @@ -6467,6 +6478,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/material-icons": { + "version": "1.13.14", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.14.tgz", + "integrity": "sha512-kZOfc7xCC0rAT8Q3DQixYAeT+tBqZnxkseQtp2bxBxz7q5pMAC+wmit7vJn1g/l7wRU+HEPq23gER4iPjGs5Cg==", + "license": "Apache-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/src/econtabil-web/package.json b/src/econtabil-web/package.json index b3be2f0..67068f9 100644 --- a/src/econtabil-web/package.json +++ b/src/econtabil-web/package.json @@ -20,7 +20,9 @@ "@angular/material": "^21.2.14", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@fontsource/roboto": "^5.3.0", "@ngrx/signals": "^21.1.1", + "material-icons": "^1.13.14", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, diff --git a/src/econtabil-web/public/login-fundo.svg b/src/econtabil-web/public/login-fundo.svg new file mode 100644 index 0000000..362509c --- /dev/null +++ b/src/econtabil-web/public/login-fundo.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/econtabil-web/src/app/core/layout/shell.component.ts b/src/econtabil-web/src/app/core/layout/shell.component.ts index 2cd439e..e7b598b 100644 --- a/src/econtabil-web/src/app/core/layout/shell.component.ts +++ b/src/econtabil-web/src/app/core/layout/shell.component.ts @@ -58,10 +58,17 @@ const MENU: readonly ItemDeMenu[] = [ menu - eContabil + + receipt_long + eContabil + + - {{ store.usuario()?.nome }} + + {{ store.usuario()?.nome }} + {{ store.usuario()?.role }} + + +

+ Esqueceu a senha? Peça a redefinição a um administrador do escritório — não há + recuperação por e-mail. +

`, styles: ` + /* + * A cor de fundo vem antes da imagem, e não junto dela: se o SVG demorar ou falhar, a tela abre + * azul em vez de piscar branco. É o mesmo azul que domina o desenho, então a troca não aparece. + */ .tela { - display: grid; - place-items: center; min-height: 100vh; - padding: 1rem; + padding: 2rem 1.5rem; + background-color: #0b4a8f; + background-image: url('/login-fundo.svg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + justify-content: center; + gap: 4rem; + } + + .apresentacao { + max-width: 32rem; + color: #fff; + justify-self: end; + } + + .marca { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 1.5rem; + font-size: 1.25rem; + font-weight: 500; + letter-spacing: 0.02em; + } + + .apresentacao h1 { + font-size: clamp(1.75rem, 2.6vw, 2.5rem); + line-height: 1.2; + font-weight: 400; + margin: 0 0 1rem; + color: #fff; + } + + .chamada { + margin: 0; + font-size: 1rem; + line-height: 1.6; + + /* Branco puro no corpo do texto cansa sobre fundo escuro; o leve rebaixamento mantém AA. */ + color: rgb(255 255 255 / 82%); } .cartao { width: min(24rem, 100%); + justify-self: start; + padding-bottom: 0.5rem; } form { @@ -92,8 +154,47 @@ import { AuthStore } from '../../core/auth/auth.store'; } .erro { - color: var(--mat-sys-error, #b3261e); + color: var(--ec-erro); + background: var(--ec-erro-fundo); + border-radius: 8px; + padding: 0.5rem 0.75rem; margin: 0 0 0.5rem; + font-size: 0.875rem; + } + + .ajuda { + margin: 1rem 0 0; + font-size: 0.8125rem; + line-height: 1.5; + color: var(--ec-texto-secundario); + } + + /* Abaixo de 62rem a coluna de texto espremeria o formulário; ela sai e sobra a marca. */ + @media (width < 62rem) { + .tela { + grid-template-columns: minmax(0, 24rem); + justify-content: center; + gap: 2rem; + } + + .apresentacao { + justify-self: center; + text-align: center; + } + + .apresentacao h1, + .chamada { + display: none; + } + + .marca { + justify-content: center; + margin: 0; + } + + .cartao { + justify-self: center; + } } `, }) diff --git a/src/econtabil-web/src/app/shared/badge-situacao.component.ts b/src/econtabil-web/src/app/shared/badge-situacao.component.ts index c91f9e4..b1c9376 100644 --- a/src/econtabil-web/src/app/shared/badge-situacao.component.ts +++ b/src/econtabil-web/src/app/shared/badge-situacao.component.ts @@ -37,7 +37,7 @@ const ROTULOS: Readonly> = { .cancelada, .denegada { background: #fdeaea; - color: #7f1d1d; + color: var(--ec-erro); } .encerrada { diff --git a/src/econtabil-web/src/app/shared/badge-validade.component.ts b/src/econtabil-web/src/app/shared/badge-validade.component.ts index 4cbea07..fc0bb96 100644 --- a/src/econtabil-web/src/app/shared/badge-validade.component.ts +++ b/src/econtabil-web/src/app/shared/badge-validade.component.ts @@ -31,12 +31,12 @@ const MS_POR_DIA = 86_400_000; .vencendo { background: #fef3c7; - color: #78350f; + color: var(--ec-aviso); } .vencido { background: #fdeaea; - color: #7f1d1d; + color: var(--ec-erro); } `, }) diff --git a/src/econtabil-web/src/index.html b/src/econtabil-web/src/index.html index a97b6ed..f6f9590 100644 --- a/src/econtabil-web/src/index.html +++ b/src/econtabil-web/src/index.html @@ -1,18 +1,22 @@ - + - EcontabilWeb + eContabil - Notas Fiscais + + + + + - - - - + + diff --git a/src/econtabil-web/src/styles.scss b/src/econtabil-web/src/styles.scss index 8ba3af6..2821674 100644 --- a/src/econtabil-web/src/styles.scss +++ b/src/econtabil-web/src/styles.scss @@ -1,39 +1,185 @@ -// Include theming for Angular Material with `mat.theme()`. -// This Sass mixin will define CSS variables that are used for styling Angular Material -// components according to the Material 3 design spec. -// Learn more about theming and how to use it for your application's -// custom components at https://material.angular.dev/guide/theming @use '@angular/material' as mat; +// Roboto e os ícones do Material entram pelo `styles` do angular.json, empacotados com a aplicação. +// O `` para fonts.googleapis.com fazia a compilação da imagem depender de alcançar a internet — +// e ela quebrou exatamente assim, com "Inlining of fonts failed". Em runtime a dependência era pior: +// escritório atrás de proxy corporativo veria a interface em fonte de sistema, e cada ícone do Material +// apareceria como o nome dele escrito por extenso. + html { height: 100%; + @include mat.theme( ( - color: ( - primary: mat.$azure-palette, - tertiary: mat.$blue-palette, - ), + // Azul como cor de marca. `azure` é o azul mais saturado da paleta do Material 3 e sustenta + // contraste AA sobre branco no texto e nos ícones; `blue` fica na terciária, para realces que não + // devem competir com a ação principal da tela. + color: (primary: mat.$azure-palette, tertiary: mat.$blue-palette), typography: Roboto, - density: 0, + density: 0 ) ); } +/** + * Cores da aplicação que o tema do Material não cobre. + * + * Antes cada componente escrevia o seu próprio `rgb(0 0 0 / 60%)` e o seu tom de âmbar para aviso. + * Bastava um divergir para a interface parecer montada por pessoas diferentes — e mudar a identidade + * exigia caçar literal por literal. Aqui elas têm nome, e o componente pede pelo significado. + */ +:root { + --ec-fundo: #f4f6fa; + + // Realce de bloco — barra de totais, cabeçalho de tabela. Claro o bastante para não competir com o + // conteúdo, escuro o bastante para delimitar sem precisar de borda. + --ec-superficie-suave: #eef2f8; + + --ec-borda: rgb(15 23 42 / 10%); + --ec-borda-forte: rgb(15 23 42 / 18%); + + --ec-texto: #0f172a; + --ec-texto-secundario: rgb(15 23 42 / 62%); + --ec-texto-terciario: rgb(15 23 42 / 45%); + + --ec-marca: #0b4a8f; + --ec-marca-clara: #1565c0; + --ec-marca-fundo: #e8f1fb; + + // Âmbar é "precisa de atenção, mas nada quebrou" — o caso do documento que só tem resumo. + --ec-aviso: #8a5300; + --ec-aviso-fundo: #fff7e6; + --ec-erro: #b3261e; + --ec-erro-fundo: #fdecea; + --ec-sucesso: #1b5e20; + + --ec-sombra-cartao: 0 1px 2px rgb(15 23 42 / 6%), 0 1px 3px rgb(15 23 42 / 4%); + --ec-raio: 12px; +} + body { - // Default the application to a light color theme. This can be changed to - // `dark` to enable the dark color theme, or to `light dark` to defer to the - // user's system settings. color-scheme: light; - // Set a default background, font and text colors for the application using - // Angular Material's system-level CSS variables. Learn more about these - // variables at https://material.angular.dev/guide/system-variables - background-color: var(--mat-sys-surface); - color: var(--mat-sys-on-surface); + background-color: var(--ec-fundo); + color: var(--ec-texto); font: var(--mat-sys-body-medium); - // Reset the user agent margin. margin: 0; height: 100%; } -/* You can add global styles to this file, and also import other style files */ + +h1 { + font-size: 1.5rem; + font-weight: 500; + letter-spacing: -0.01em; + margin: 0 0 1rem; +} + +/** + * Cartões com borda e sombra discreta, no lugar da elevação alta do Material. + * + * O painel empilha muitos cartões na mesma tela; com sombra pronunciada a página vira um relevo em que + * nada se destaca, porque tudo se destaca. A borda separa o cartão do fundo e a sombra dá só a + * profundidade mínima para ele não parecer uma caixa desenhada. + */ +mat-card { + border: 1px solid var(--ec-borda); + box-shadow: var(--ec-sombra-cartao) !important; + border-radius: var(--ec-raio) !important; +} + +mat-card-title { + font-size: 1rem !important; + font-weight: 500 !important; +} + +mat-card-subtitle { + color: var(--ec-texto-secundario) !important; +} + +/** + * Grade de dados. + * + * Cinco telas montam tabela com a mesma classe e, até aqui, cada uma repetia a própria borda e o próprio + * espaçamento. Centralizar aqui resolve a divergência e dá de graça o que nenhuma tinha: cabeçalho que + * gruda ao rolar, listras alternadas e realce na linha sob o cursor — o que sustenta a leitura de uma + * grade com dezenas de linhas e doze colunas. + */ +.grade { + width: 100%; + border-collapse: collapse; +} + +.grade thead th { + position: sticky; + top: 0; + z-index: 1; + + background: var(--ec-superficie-suave); + color: var(--ec-texto-secundario); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + text-align: left; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--ec-borda-forte); +} + +.grade tbody td { + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--ec-borda); +} + +.grade tbody tr:nth-child(even) { + background: rgb(15 23 42 / 2%); +} + +.grade tbody tr:hover { + background: var(--ec-marca-fundo); +} + +.grade tbody tr:last-child td { + border-bottom: 0; +} + +/** + * Aviso: o estado em que nada quebrou, mas o usuário precisa saber de alguma coisa. + * + * É o documento que veio só como resumo, o período longo demais, o filtro que exclui notas sem XML. Só + * a cor do texto não sustentava esses recados — âmbar sobre branco some no meio da página. A faixa com + * fundo e barra à esquerda separa o aviso do conteúdo sem gritar como um erro. + */ +.aviso { + color: var(--ec-aviso); + background: var(--ec-aviso-fundo); + border-left: 3px solid var(--ec-aviso); + border-radius: 0 8px 8px 0; + padding: 0.75rem 1rem; + margin: 0 0 1rem; + font-size: 0.875rem; + line-height: 1.5; +} + +/** Estado vazio: informa sem alarmar, então não usa a cor de aviso. */ +.vazio { + color: var(--ec-texto-secundario); + text-align: center; + padding: 2.5rem 1rem; +} + +/** Números em fonte tabular: coluna de valor só alinha na vírgula se todo dígito ocupar o mesmo espaço. */ +.numero-celula, +.valor, +.numero { + font-variant-numeric: tabular-nums; +} + +/** Conteúdo lido apenas por leitor de tela — legenda de tabela, sobretudo. */ +.oculto { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); +}