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 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⚡ 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✅ ~98%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

10. Auto-criação de entidades dependentes (canônico) ​

Princípio: O usuário não deve ser obrigado a criar entidades intermediárias (ex: turmas) manualmente quando elas podem ser inferidas a partir dos dados importados. A regra é zero-fricção sempre que possível, fail-loud quando não.

10.1 Inferência no frontend ​

Antes de enviar dados ao backend, o frontend tenta criar todas as entidades dependentes detectáveis. Para a importação de alunos, isso significa criar automaticamente as turmas inferidas pela combinação Série + Turma do arquivo (ex: "4º Ano" + "4A" → turma 4º EF - A).

Regras:

  • Se o parse principal falhar, tentar inferência adicional combinando colunas auxiliares (ex: Série do aluno) antes de desistir.
  • Toda falha de criação deve ser coletada em uma lista agregada ({ turmaValor, motivo }[]), nunca silenciada com continue.
  • Após a criação em lote, refrescar o cache local (ex: listarTurmas()) para evitar race com mapeamentos subsequentes.

10.2 Idempotência no backend ​

Endpoints de criação de entidades auto-criáveis devem ser idempotentes em violações de constraint UNIQUE (Postgres 23505):

typescript
if (error.code === '23505') {
  // Buscar a entidade existente e retornar como sucesso
  const { data: existente } = await supabase
    .from(tabela)
    .select('*')
    .eq('escola_id', user.escola_id)
    .eq(/* chaves únicas */)
    .maybeSingle();

  if (existente) {
    return { success: true, data: existente, recovered_duplicate: true };
  }
}

Isso protege contra:

  • Race conditions (criação concorrente da mesma entidade)
  • Retentativas em redes instáveis
  • Re-execução do fluxo após F5 / reload

10.3 Não bloquear o fluxo principal ​

Erros de auto-criação não bloqueiam o início da importação. O comportamento canônico:

  1. Tentar criar todas as entidades inferíveis.
  2. Se sobrarem erros, exibir toast informativo (olpToast.info) — não erro, pois o fluxo continua.
  3. Os registros que dependem das entidades não-criadas vão para rejeitados no relatório final com motivo específico (ex: "Linha N: turma '4A' inválida — série não existe no catálogo").
  4. Usuário decide se retoma a importação corrigindo a planilha ou aceita os registros parciais.

10.4 Quando exigir ação manual ​

Apenas em casos patológicos:

  • Coluna obrigatória completamente ausente da planilha (ex: nenhuma coluna mapeável a "Turma")
  • 100% dos registros caem em invalid_pattern ou no_turma

Nesses cenários, voltar ao step de mapeamento com mensagem clara.

10.5 Mapeamento canônico de turmas (planilha modelo) ​

Validado contra modelo_importacao_alunos.xlsx. Anos 4–9 são exclusivos do EF, então o segmento é inferido. Anos 1–3 são ambíguos e exigem indicação explícita do segmento — caso contrário, vão para invalid_pattern (sem heurística por idade do aluno).

Valor no arquivoanosegmentoidentificadorstatus
8A, 8B8EF (inferido)A/Bunmatched (auto-criar)
9A EF9EF (explícito)Aunmatched (auto-criar)
3ª A / Ensino Médio3EM (explícito)Aunmatched (auto-criar)
4B Vespertino4EF (inferido)Bunmatched (auto-criar)
9º B / EF9EF (explícito)Bunmatched (auto-criar)
5ª C / Ensino Fundamental5EF (explícito)Cunmatched (auto-criar)
1A1—Ainvalid_pattern (correto bloquear)

UX canônica do preview (R-007):

  • Card usa line-clamp-2 + title (tooltip nativo). Nunca esconder o segmento por truncamento.
  • 4 KPIs sem sobreposição: Vinculadas (matched), Serão criadas (unmatched), Padrão inválido (invalid_pattern), Alunos sem turma (somente invalid_pattern + no_turma).
  • Banner azul: lista turmas que serão auto-criadas. Banner vermelho dedicado: lista valores invalid_pattern literais.

10.6 Referências ​

  • Importação de Alunos: src/components/importacao-alunos/index.tsx → criarTurmasNaoCadastradas
  • Backend idempotente: supabase/functions/gestao-turmas/index.ts → createTurma
  • Regressões: supabase/functions/gestao-alunos/__tests__/regressions.md (R-006, R-007)

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