gabrielb d234b9ebe0 docs: CLAUDE.md com contexto completo do projeto para desenvolvimento assistido por IA
Cobre: stack, arquitetura multi-tenant, ordem de inicialização, theming dinâmico,
dark mode, clientes HTTP, estrutura de pastas, rotas, fluxo de login, variáveis de
ambiente, regras de código, acessibilidade, como adicionar telas, pendências e pitfalls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:52:28 -03:00

12 KiB

portal-modumfiscal-web — Contexto para IA

Portal público de autoatendimento fiscal do contribuinte. SaaS multi-tenant: cada prefeitura cliente tem seu próprio subdomínio (tutoia.modumfiscal.com.br) e uma identidade visual distinta (cor primária + foto de fundo + logo).

Projeto irmão: core-modumfiscal-web (backoffice interno da prefeitura, outra repo). Backend compartilhado: API REST Spring Boot em https://sistema.modumfiscal.com.br.


Stack

Dependência Versão Observação
Vue 3.5 <script setup> obrigatório
Vite 8 Plugin @tailwindcss/vite (sem PostCSS)
PrimeVue 4.5 Tema Aura, auto-importado via unplugin-vue-components
@primeuix/themes 2 updatePreset, updateSurfacePalette, definePreset
TailwindCSS 4 @import "tailwindcss" no CSS — sem tailwind.config.js
Vue Router 5 createWebHistory
Pinia 3 pinia-plugin-persistedstate
Axios 1.x Dois clientes: público e autenticado
Zod 4 Validação de formulários (usar nos wizards)

Arquitetura Multi-Tenant

subdomínio (ex: tutoia.modumfiscal.com.br)
    └── getTenant() → 'tutoia'
          └── bootstrapPrefeitura() → GET /api/v1/publico/prefeitura/tutoia
                └── prefeituraStore.$patch({ nomePrefeitura, template, pathLogo, ... })
                      └── App.vue onMounted → applyTemplate('tutoia')
                            └── PrimeVue recebe cor primária laranja (tutoia)

Ordem de inicialização em main.js (crítica — não alterar):

  1. createPinia() + pinia.use(persistedstate)
  2. await bootstrapPrefeitura(pinia) — busca config da prefeitura antes de montar
  3. app.use(router)
  4. app.use(PrimeVue, primeVueConfig)
  5. app.mount('#app')
  6. App.vue onMountedapplyTemplate(prefeitura.template) — aplica cor dinâmica

Por que applyTemplate fica no onMounted e não no bootstrap? updatePreset() exige que o PrimeVue já esteja inicializado. No bootstrap, o app.use(PrimeVue) ainda não rodou. Chamá-la antes causa erro silencioso.


Detecção de Tenant

// src/utils/tenant.js
getTenant()  hostname.split('.')[0]
            localStorage('current_dominio')  // fallback
            'sistema'                         // último fallback

localhost é inválido propositalmente — em dev, o bootstrap falha graciosamente e o app sobe sem tema personalizado (usa blue como padrão).


Theming Dinâmico

Templates disponíveis em src/config/theme.config.js: sistema · tutoia · amber · blue · indigo · violet · emerald · teal · rose

O campo template retornado pela API (ex: "tutoia") determina a paleta primary. A superfície é sempre slate — não mudar.

Para adicionar novo município com foto de fundo:

  1. Colocar a imagem em src/assets/images/bg-NOME.jpeg
  2. Importar em HomeView.vue e adicionar ao heroBgMap
  3. Cadastrar o template correspondente em theme.config.js se precisar de cor nova

Dark Mode

  • Classe .app-dark no <html> ativa o dark mode
  • PrimeVue: darkModeSelector: '.app-dark' em primevue.config.js
  • Tailwind: @variant dark (&:where(.app-dark, .app-dark *)) em main.css
  • O widget AccessibilityWidget.vue gerencia a classe e persiste no localStorage

Toda view nova deve incluir dark: nos utilitários Tailwind. Referência rápida:

Light Dark
bg-white dark:bg-slate-900
bg-slate-50 dark:bg-slate-950
bg-slate-100 dark:bg-slate-800
border-slate-200 dark:border-slate-700
text-slate-800 dark:text-slate-100
text-slate-600 dark:text-slate-300
text-slate-500 dark:text-slate-400
hover:bg-slate-100 dark:hover:bg-slate-800

Clientes HTTP

// src/config/apiClient.js
apiClientPublico   // sem Authorization — para bootstrap e serviços públicos
apiClient          // com Bearer token + headers tenant — para /portal/*

Interceptors adicionam automaticamente X-Municipio e X-Dominio em toda requisição. apiClient redireciona para / em resposta 401.

Regra: nunca chamar apiClient diretamente em componente — sempre via Service.


Estrutura de Pastas

src/
├── assets/
│   ├── main.css              ← Tailwind v4 directives + @variant dark
│   ├── layout/layout.scss    ← Focus ring, skip link, prefers-reduced-motion, classes a11y
│   └── images/               ← Logos e fotos de fundo (importação estática Vite)
├── bootstrap/
│   └── prefeituraBoot.js     ← Busca config da prefeitura antes de montar o app
├── components/
│   ├── common/
│   │   ├── AppHeader.vue     ← Header público (logo dinâmica, nav)
│   │   ├── AppFooter.vue     ← Footer com links institucionais
│   │   ├── ServiceCard.vue   ← Card reutilizável de serviço (público e autenticado)
│   │   └── AccessibilityWidget.vue  ← Botão flutuante: fonte, dark, alto contraste
│   └── auth/
│       └── DocumentoInput.vue  ← Input CPF/CNPJ com máscara automática
├── composables/
│   └── useMotion.js          ← prefers-reduced-motion reativo
├── config/
│   ├── apiClient.js          ← Axios clients (público + autenticado)
│   ├── primevue.config.js    ← Aura preset + locale pt-BR
│   └── theme.config.js       ← Paletas e applyTemplate()
├── layouts/
│   ├── PublicLayout.vue      ← Header + Footer + RouterView (rotas públicas)
│   └── PortalLayout.vue      ← Header autenticado com nav + Sair
├── router/index.js           ← Rotas + guard requiresAuth
├── services/
│   ├── prefeituraService.js  ← GET /publico/prefeitura/{dominio}
│   └── authService.js        ← Keycloak PKCE (estrutura pronta, integração pendente)
├── stores/
│   ├── prefeituraStore.js    ← Config da prefeitura (persiste localStorage)
│   └── authStore.js          ← Token + userInfo (persiste localStorage)
├── utils/
│   └── tenant.js             ← getTenant / setTenant / clearTenant
└── views/
    ├── public/               ← Home, Login, PrimeiroAcesso, Credenciamento
    ├── servicos/             ← ServicosHub, Certidao, Iptu
    └── portal/               ← Painel, Debitos, Certidoes, Alvaras, Pagamentos, Dados

Rotas

Path Nome Layout Auth
/ home Public
/login login Public
/primeiro-acesso primeiro-acesso Public
/credenciamento credenciamento Public
/servicos servicos Public
/servicos/certidao certidao Public
/servicos/iptu iptu Public
/portal/painel painel Portal
/portal/debitos debitos Portal
/portal/certidoes certidoes-portal Portal
/portal/alvaras alvaras Portal
/portal/pagamentos pagamentos Portal
/portal/dados dados Portal

Guard no router: rotas requiresAuth: true redirecionam para home se não autenticado.


Fluxo de Login (2 etapas)

Home (captura CPF/CNPJ)
  └── router.push({ name: 'login', query: { doc: documento } })
        └── LoginView.vue exibe o doc mascarado + campo senha
              └── entrar() → Keycloak PKCE (a implementar em authService.js)
                    └── authStore.setSession(token, userInfo)
                          └── router.push({ name: 'painel' })

O documento trafega como query param (?doc=01234567890) — apenas os dígitos, sem formatação. LoginView faz a máscara localmente.


Variáveis de Ambiente

# .env.development.local  (GIT IGNORADO — crie localmente)
VITE_API_URL=https://sistema.modumfiscal.com.br
VITE_KEYCLOAK_URL=https://keycloakprod.modumfiscal.com.br
VITE_KEYCLOAK_REALM=modumfiscal-dev
VITE_KEYCLOAK_CLIENT_ID=portal-modumfiscal-web

Os arquivos .env.development e .env.production estão no repo mas vazios — servem de template. Os valores reais ficam apenas nos arquivos .local.


Regras Críticas de Código

  • <script setup> obrigatório — Options API proibida (exceto prefeituraStore que usa Options API por compatibilidade com o padrão do core)
  • PrimeVue auto-importado — não adicionar import Button from 'primevue/button' manualmente
  • CSS: TailwindCSS inline no template — @apply proibido — SCSS só em assets/layout/
  • dark: em toda view nova — ver tabela acima
  • Máximo 300 linhas por componente — extrair subcomponentes se ultrapassar
  • Nunca apiClient direto em componente — sempre via Service
  • router.push({ name: 'home' }) para "voltar" — nunca router.back() em fluxos críticos (histórico inesperado)
  • Componentes PrimeVue com dark: manual nos wrappers — PrimeVue já adapta seus internos, mas divs externas precisam de dark: Tailwind

Acessibilidade

Widget flutuante (AccessibilityWidget.vue) disponível em todas as telas via App.vue. Persiste preferências no localStorage (chaves: a11y-fonte, a11y-contraste, a11y-escuro).

Classes aplicadas no <html>:

  • a11y-font-lg → fonte 18px
  • a11y-font-xl → fonte 20px
  • a11y-contrastfilter: contrast(1.45)
  • app-dark → dark mode (PrimeVue + Tailwind)

ARIA implementada: skip link, aria-label, aria-current, role="alert", aria-live, aria-hidden em ícones decorativos, role="contentinfo" no footer.


Como Adicionar Nova Tela Pública

  1. Criar src/views/servicos/MinhaView.vue
  2. Adicionar rota em src/router/index.js dentro do bloco PublicLayout
  3. Incluir botão voltar (router.push({ name: 'home' })) no topo
  4. Adicionar dark: em todos os utilitários Tailwind
  5. (Opcional) Adicionar no servicosPublicos de HomeView.vue

Como Adicionar Nova Tela Autenticada

  1. Criar src/views/portal/MinhaView.vue
  2. Adicionar rota em src/router/index.js dentro do bloco PortalLayout com meta: { requiresAuth: true } já herdado do pai
  3. Criar serviço em src/services/ que use apiClient (nunca apiClientPublico)
  4. Adicionar no navItems de PortalLayout.vue se entrar na navegação principal
  5. Adicionar dark: em todos os utilitários Tailwind

O Que Está Pendente

Feature Arquivos envolvidos Observações
Keycloak PKCE real authService.js, LoginView.vue Estrutura pronta, falta integrar pkce-challenge
Wizard Credenciamento CredenciamentoView.vue 6 etapas: dados pessoais, endereço, documentos, representante, confirmação
Wizard Primeiro Acesso PrimeiroAcessoView.vue 4 etapas: identificação, código, nova senha, confirmação
Certidão (implementação real) CertidaoView.vue Input CPF/CNPJ → POST API → download PDF
IPTU (implementação real) IptuView.vue Input inscrição imobiliária → GET débitos → carnê PDF
Endpoint avisos carousel HomeView.vue GET /api/v1/publico/avisos/{dominio} (dados mockados atualmente)
Campo pathBackground na API prefeituraBoot.js Backend ainda não retorna — mapeamento estático por template como POC

Comandos

npm run dev      # dev server (http://localhost:5173)
npm run build    # build produção
npm run lint     # ESLint + fix

Pitfalls Conhecidos

Erro Correto
applyTemplate() no bootstrap Chamar no App.vue onMounted — PrimeVue precisa estar inicializado
router.back() no botão voltar router.push({ name: 'home' }) — back usa histórico do browser
import Button from 'primevue/button' Não importar — PrimeVue é auto-importado
@apply no template TailwindCSS inline — @apply só em layout.scss e nunca
Utilitário Tailwind sem dark: Sempre adicionar par dark: para qualquer bg/text/border
apiClient direto na view Criar Service e chamar pela view
Path relativo de logo/foto da API resolverUrl() em prefeituraBoot.js converte para URL absoluta