Skip to content

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

RegraDetalhes
Background processingObrigatório para > 50 registros. ≤ 50 pode ser síncrono.
Batch size50 registros por lote (BATCH_SIZE = 50)
EdgeRuntime.waitUntilUsar para processamento assíncrono; fallback await se indisponível
CancelamentoVerificar status da sessão a cada batch; abortar se cancelado
Normalização ExcelJSToda célula deve passar por safeExtractValue() antes do uso

2. Persistência (sobreviver a F5 e navegação)

2.1 Armazenamento duplo obrigatório

StorageO que guardarPropósito
localStoragesessionId (UUID da sessão backend)Reconexão ao polling após reload/navegação
sessionStorageWizard 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

typescript
// localStorage
`olp_import_${modulo}_session_id`      // ex: olp_import_resultados_session_id

// sessionStorage  
`olp_import_${modulo}_wizard_state`    // ex: olp_import_alunos_wizard_state

3. UX Obrigatória

3.1 Indicador global de progresso

Criar hook reutilizável useImportacao${Modulo}Ativa() que:

  • Verifica localStorage por 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:

AbordagemQuando usar
Tab (não modal)Quando importação é feature principal da tela
Sheet/Drawer com dismissQuando importação é ação secundária (botão em outra tela)
Dialog com onInteractOutside={e => e.preventDefault()} removidoPermitir fechar, mas manter polling via hook global

3.3 Tempo estimado (ETA)

typescript
// 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)

typescript
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:

typescript
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):

typescript
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

typescript
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çãoObrigatóriaExemplo
Colunas obrigatórias mapeadasMatrícula + Pontuação
Registros com campos vaziosMatrícula em branco → rejeitado
Duplicatas na planilhaMesma matrícula 2x → warning
Tipo de dado inválidoPontuação = "abc" → rejeitado
Pontuação ≥ 0Pontuação negativa → rejeitado
Pontuação ≤ máximo do nível⚡ RecomendadoExcede 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:

sql
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   TIMESTAMPTZ

6.2 Actions padronizadas

ActionParâmetrosRetorno
start_import_session{ escola_id, dados, ...config }{ sessionId }
get_import_session_status{ sessionId }{ status, progresso, mensagem, ... }
cancel_import_session{ sessionId }{ success }

6.3 Polling

typescript
const POLL_INTERVAL = 2000; // 2 segundos
const MAX_STALE_POLLS = 30; // 30 × 2s = 60s sem progresso → stale

7. Testes Obrigatórios

7.1 Unit (Vitest)

ÁreaTestes mínimos
safeExtractValuePrimitivos, fórmulas, richText, error, null, nested
safeParseNumberInteiro, decimal BR, string, espaços, NaN
Filtro de previewRegistros válidos, inválidos, edge cases (zero, NaN)
Detecção de colunasPatterns conhecidos, fallback, case-insensitive
Detecção de duplicatasMatrícula repetida, case-insensitive

7.2 Integration (Vitest)

ÁreaTestes mínimos
Hook de sessãoPolling, recovery, stale detection, cancelamento
sessionStorageSalvar/restaurar wizard state
localStorageSalvar/restaurar sessionId

7.3 E2E (Playwright)

CenárioPassos
Fluxo completoUpload → detecção → preview → confirmar → resultado
F5 durante processamentoUpload → iniciar → F5 → reconexão → resultado
CancelamentoUpload → iniciar → cancelar → confirmação
Planilha inválidaUpload com colunas erradas → feedback de erro

8. Checklist — Novo Sistema de Importação

markdown
## 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, cancelamento

9. Sistemas Existentes — Status de Conformidade

SistemaConformidadeGaps 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