Padrão Obrigatório — Sistemas de Importação
Aplicável a: toda feature de importação de dados via arquivo (Excel/CSV) na Plataforma OLP.
Referências: Importação de Alunos (gold standard), Importação de Resultados.
Atualizado em: 2026-03-18
1. Princípios Gerais
| Regra | Detalhes |
|---|---|
| Background processing | Obrigatório para > 50 registros. ≤ 50 pode ser síncrono. |
| Batch size | 50 registros por lote (BATCH_SIZE = 50) |
EdgeRuntime.waitUntil | Usar para processamento assíncrono; fallback await se indisponível |
| Cancelamento | Verificar status da sessão a cada batch; abortar se cancelado |
| Normalização ExcelJS | Toda célula deve passar por safeExtractValue() antes do uso |
2. Persistência (sobreviver a F5 e navegação)
2.1 Armazenamento duplo obrigatório
| Storage | O que guardar | Propósito |
|---|---|---|
localStorage | sessionId (UUID da sessão backend) | Reconexão ao polling após reload/navegação |
sessionStorage | Wizard state (fileName, headers, mapping, preview) | Restaurar UI do wizard em F5 |
2.2 Ciclo de vida
┌─ Upload do arquivo ──► sessionStorage.set(wizardState)
├─ Iniciar importação ──► localStorage.set(sessionId)
├─ F5 ou navegação ──► Montar componente:
│ ├─ localStorage.get(sessionId) → retomar polling
│ └─ sessionStorage.get(wizardState) → restaurar wizard
├─ Conclusão ──► Limpar ambos
└─ Unmount ──► Limpar sessionStorage (manter localStorage se polling ativo)2.3 Chaves padronizadas
// localStorage
`olp_import_${modulo}_session_id` // ex: olp_import_resultados_session_id
// sessionStorage
`olp_import_${modulo}_wizard_state` // ex: olp_import_alunos_wizard_state3. UX Obrigatória
3.1 Indicador global de progresso
Criar hook reutilizável useImportacao${Modulo}Ativa() que:
- Verifica
localStoragepor sessão ativa - Faz polling do status se encontrar
- Retorna
{ isActive, progress, message }para qualquer componente consumir - Permite barra de progresso fora do modal/tab de importação
3.2 Navegação livre durante processamento
O usuário NÃO deve ficar preso ao modal/tab enquanto a importação roda em background. Opções:
| Abordagem | Quando usar |
|---|---|
| Tab (não modal) | Quando importação é feature principal da tela |
| Sheet/Drawer com dismiss | Quando importação é ação secundária (botão em outra tela) |
Dialog com onInteractOutside={e => e.preventDefault()} removido | Permitir fechar, mas manter polling via hook global |
3.3 Tempo estimado (ETA)
// Média móvel dos últimos N batches
const avgBatchTime = recentBatchTimes.reduce((a, b) => a + b, 0) / recentBatchTimes.length;
const remainingBatches = Math.ceil((total - processed) / BATCH_SIZE);
const etaSeconds = Math.round(remainingBatches * avgBatchTime / 1000);3.4 Confirmação de cancelamento
Usar AlertDialog (shadcn/ui) com texto claro:
- Título: "Cancelar importação?"
- Descrição: "X de Y registros já foram processados. O cancelamento não desfaz os registros já importados."
- Ações: "Continuar importação" (default) | "Cancelar" (destructive)
3.5 Detecção de sessão travada (stale)
const MAX_STALE_POLLS = 30; // ~60s com polling de 2s
if (stalePollCount >= MAX_STALE_POLLS) {
// Exibir aviso: "A importação parece travada. Tente novamente."
}4. Detecção e Mapeamento de Colunas
4.1 Auto-detect com regex
Cada módulo define seus patterns de detecção:
const COLUMN_PATTERNS: Record<string, RegExp[]> = {
matricula: [/matr[íi]cula/i, /^mat$/i, /codigo.?aluno/i, /registro/i, /^ra$/i],
pontuacao: [/pontua[çc][aã]o/i, /^nota$/i, /pontos/i, /score/i, /acertos/i],
nome: [/nome/i, /aluno/i, /estudante/i],
nivel: [/n[íi]vel/i, /level/i, /categoria/i],
};4.2 Fallback manual
Se auto-detect não encontrar uma coluna obrigatória, exibir Select com todas as colunas do arquivo para mapeamento manual.
4.3 safeExtractValue — Obrigatório
Todo valor de célula lido via ExcelJS DEVE ser normalizado por safeExtractValue() (em src/lib/xlsx-utils.ts):
export function safeExtractValue(cell: unknown): string | number | boolean | null {
if (cell == null) return null;
if (typeof cell !== 'object') return cell as string | number | boolean;
const obj = cell as Record<string, unknown>;
// Fórmulas: { formula: '...', result: 6 }
if ('result' in obj) return safeExtractValue(obj.result);
// Rich text: { richText: [{ text: '...' }] }
if ('richText' in obj && Array.isArray(obj.richText)) {
return obj.richText.map((seg: any) => seg?.text ?? '').join('');
}
// Error: { error: '#REF!' }
if ('error' in obj) return null;
return String(cell);
}4.4 safeParseNumber — Para colunas numéricas
function safeParseNumber(value: unknown): number {
if (typeof value === 'number') return value;
if (value == null) return NaN;
const cleaned = String(value).replace(',', '.').trim();
return cleaned === '' ? NaN : parseFloat(cleaned);
}4.5 Template Excel para download
Oferecer botão "Baixar modelo" com planilha pré-formatada contendo:
- Headers corretos (auto-detectáveis)
- 2-3 linhas de exemplo
- Aba com instruções (opcional)
5. Validações
5.1 Frontend (preview)
| Validação | Obrigatória | Exemplo |
|---|---|---|
| Colunas obrigatórias mapeadas | ✅ | Matrícula + Pontuação |
| Registros com campos vazios | ✅ | Matrícula em branco → rejeitado |
| Duplicatas na planilha | ✅ | Mesma matrícula 2x → warning |
| Tipo de dado inválido | ✅ | Pontuação = "abc" → rejeitado |
| Pontuação ≥ 0 | ✅ | Pontuação negativa → rejeitado |
| Pontuação ≤ máximo do nível | ⚡ Recomendado | Excede máximo → warning visual |
5.2 Backend (zero-trust)
O backend DEVE revalidar tudo, independente do que o frontend filtrou:
- Matrícula existe na escola
- Aluno pertence à série participante
- Nível correto para a série
- Pontuação dentro do limite
- Duplicatas tratadas (upsert ou rejeição)
5.3 Relatório de rejeitados
O resultado final deve conter:
- Total processados / sucesso / erro / ignorados
- Lista de rejeitados com motivo (ex: "Matrícula 12345: série não participante")
- Opção de baixar relatório de erros
6. Backend Contract
6.1 Tabela de sessão
Toda importação usa uma tabela importacao_*_sessoes com schema mínimo:
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
escola_id UUID NOT NULL REFERENCES escolas(id),
usuario_id UUID NOT NULL REFERENCES usuarios(id),
status TEXT DEFAULT 'pendente', -- pendente | processando | concluida | erro | cancelada
progresso INTEGER DEFAULT 0, -- 0-100
total_registros INTEGER DEFAULT 0,
processados INTEGER DEFAULT 0,
sucesso INTEGER DEFAULT 0,
erro INTEGER DEFAULT 0,
ignorados INTEGER DEFAULT 0,
mensagem TEXT,
dados_importacao JSONB, -- dados do arquivo
resultados JSONB, -- detalhes de erros/rejeições
criado_em TIMESTAMPTZ DEFAULT now(),
iniciado_em TIMESTAMPTZ,
finalizado_em TIMESTAMPTZ6.2 Actions padronizadas
| Action | Parâmetros | Retorno |
|---|---|---|
start_import_session | { escola_id, dados, ...config } | { sessionId } |
get_import_session_status | { sessionId } | { status, progresso, mensagem, ... } |
cancel_import_session | { sessionId } | { success } |
6.3 Polling
const POLL_INTERVAL = 2000; // 2 segundos
const MAX_STALE_POLLS = 30; // 30 × 2s = 60s sem progresso → stale7. Testes Obrigatórios
7.1 Unit (Vitest)
| Área | Testes mínimos |
|---|---|
safeExtractValue | Primitivos, fórmulas, richText, error, null, nested |
safeParseNumber | Inteiro, decimal BR, string, espaços, NaN |
| Filtro de preview | Registros válidos, inválidos, edge cases (zero, NaN) |
| Detecção de colunas | Patterns conhecidos, fallback, case-insensitive |
| Detecção de duplicatas | Matrícula repetida, case-insensitive |
7.2 Integration (Vitest)
| Área | Testes mínimos |
|---|---|
| Hook de sessão | Polling, recovery, stale detection, cancelamento |
sessionStorage | Salvar/restaurar wizard state |
localStorage | Salvar/restaurar sessionId |
7.3 E2E (Playwright)
| Cenário | Passos |
|---|---|
| Fluxo completo | Upload → detecção → preview → confirmar → resultado |
| F5 durante processamento | Upload → iniciar → F5 → reconexão → resultado |
| Cancelamento | Upload → iniciar → cancelar → confirmação |
| Planilha inválida | Upload com colunas erradas → feedback de erro |
8. Checklist — Novo Sistema de Importação
## Infraestrutura
□ Tabela `importacao_*_sessoes` criada (schema mínimo seção 6.1)
□ Edge Function com actions: start, status, cancel (seção 6.2)
□ Batches de 50 com `EdgeRuntime.waitUntil`
□ Verificação de cancelamento a cada batch
□ Log de transação via `registrarLog()` ao concluir
## Frontend — Persistência
□ sessionId em localStorage
□ Wizard state em sessionStorage
□ Recovery automático ao montar componente
□ Cleanup ao concluir/unmount
## Frontend — UX
□ Hook `useImportacao${Modulo}Ativa()` (indicador global)
□ Usuário pode navegar durante processamento
□ AlertDialog de confirmação no cancelamento
□ ETA com média móvel
□ Detecção de stale (30 polls sem progresso)
## Frontend — Validação
□ safeExtractValue em todas as células
□ safeParseNumber em colunas numéricas
□ Detecção de duplicatas no preview
□ Feedback visual para registros inválidos
□ Template Excel para download
## Backend — Validação
□ Zero-trust: revalidar tudo server-side
□ Relatório de rejeitados com motivos
□ Upsert ou rejeição explícita de duplicatas
## Testes
□ Unit: safeExtractValue, parsing, filtros, detecção
□ Integration: hook de sessão, storage
□ E2E: fluxo completo, F5 recovery, cancelamento9. Sistemas Existentes — Status de Conformidade
| Sistema | Conformidade | Gaps principais |
|---|---|---|
| Importação de Alunos | ✅ ~95% | Falta template download |
| Importação de Resultados | ⚠️ ~50% | Sem sessionStorage wizard, modal bloqueia usuário, sem indicador global, sem AlertDialog cancelamento, sem ETA, sem validação duplicatas |
Referências
- Importação de Alunos:
src/components/importacao-alunos/index.tsx - Hook de Alunos:
src/hooks/useImportacaoSessao.ts - Importação de Resultados:
src/components/resultados-inserir-dialog.tsx - Hook de Resultados:
src/hooks/useImportacaoResultados.ts - Backend Alunos:
supabase/functions/gestao-alunos/index.ts - Backend Resultados:
supabase/functions/gestao-resultados/index.ts safeExtractValue:src/lib/xlsx-utils.ts