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>
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):
createPinia()+pinia.use(persistedstate)await bootstrapPrefeitura(pinia)— busca config da prefeitura antes de montarapp.use(router)app.use(PrimeVue, primeVueConfig)app.mount('#app')App.vue onMounted→applyTemplate(prefeitura.template)— aplica cor dinâmica
Por que
applyTemplatefica noonMountede não no bootstrap?updatePreset()exige que o PrimeVue já esteja inicializado. No bootstrap, oapp.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:
- Colocar a imagem em
src/assets/images/bg-NOME.jpeg - Importar em
HomeView.vuee adicionar aoheroBgMap - Cadastrar o
templatecorrespondente emtheme.config.jsse precisar de cor nova
Dark Mode
- Classe
.app-darkno<html>ativa o dark mode - PrimeVue:
darkModeSelector: '.app-dark'emprimevue.config.js - Tailwind:
@variant dark (&:where(.app-dark, .app-dark *))emmain.css - O widget
AccessibilityWidget.vuegerencia a classe e persiste nolocalStorage
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 (excetoprefeituraStoreque 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 —
@applyproibido — SCSS só emassets/layout/ dark:em toda view nova — ver tabela acima- Máximo 300 linhas por componente — extrair subcomponentes se ultrapassar
- Nunca
apiClientdireto em componente — sempre via Service router.push({ name: 'home' })para "voltar" — nuncarouter.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 18pxa11y-font-xl→ fonte 20pxa11y-contrast→filter: 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
- Criar
src/views/servicos/MinhaView.vue - Adicionar rota em
src/router/index.jsdentro do blocoPublicLayout - Incluir botão voltar (
router.push({ name: 'home' })) no topo - Adicionar
dark:em todos os utilitários Tailwind - (Opcional) Adicionar no
servicosPublicosdeHomeView.vue
Como Adicionar Nova Tela Autenticada
- Criar
src/views/portal/MinhaView.vue - Adicionar rota em
src/router/index.jsdentro do blocoPortalLayoutcommeta: { requiresAuth: true }já herdado do pai - Criar serviço em
src/services/que useapiClient(nuncaapiClientPublico) - Adicionar no
navItemsdePortalLayout.vuese entrar na navegação principal - 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 |