Entrar como:FuncionalidadesDoc
Pular para o conteúdo
← Voltar para a visão geral
✅ ConcluídoCard #73 · M13

Fórmula de Média Configurável

Marco M13: Genius-Parity (Features Financeiras + Docs + RH)

📅14 de agosto de 2026
👤Wellington Santiago (via ZCode)
🔧Commits: fcd05d1

📄 Card 73 [M13] — Fórmula de Média Configurável

📚 Referência Genius: /formula + /formula/vinculo — "Cadastre um modelo de fórmula com as atividades das notas que serão lançadas" + "Vincule o modelo criado nas turmas e disciplinas". 🔥 GENIOON hoje tem GradingSystem (NUMERIC/CONCEPT_3/CONCEPT_5) mas a média é fixa (média ponderada simples). Falta configurar a fórmula.

🎯 Objetivo

Implementar o módulo Fórmula de Média Configurável:

  • Criar modelos de fórmula com atividades ponderadas (Prova peso 4, Trabalho peso 3, Exercício peso 2, Participação peso 1)
  • Definir regras: média para aprovação, recuperação, segunda chamada
  • Vincular fórmulas a (turma + disciplina) ou (curso + período)
  • Fórmulas compostas: bimestre 1 (peso X) + bimestre 2 (peso Y) = média anual
  • Aplicar automaticamente no diário de classe (Card 12)
  • Visualização do cálculo passo a passo pro professor/aluno

📚 Documentação de Referência

  • docs/execucao/12-m3-diario-classe.md — diário atual
  • prisma/schema.prismaGradingSystem, Exam

🛠️ Especificação Técnica

Schema Prisma (novo)

model GradingFormula {
  id              String   @id @default(cuid())
  name            String                              // "Média Ponderada Padrão"
  description     String?
  type            GradingSystem                       // NUMERIC, CONCEPT_3, CONCEPT_5
  passingScore    Float     @default(7.0)
  recoveryThreshold Float?                            // < 7.0 → recuperação
  activities      FormulaActivity[]
  // Composição (para fórmulas que combinam períodos)
  periodWeights   Json?                               // {bim1: 0.4, bim2: 0.6}
  branchId        String?                             // null = global
  isSystemDefault Boolean  @default(false)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

model FormulaActivity {
  id              String   @id @default(cuid())
  formulaId       String
  formula         GradingFormula @relation(fields: [formulaId], references: [id])
  name            String                              // "Prova", "Trabalho", "Participação"
  weight          Float                               // peso na média
  isRequired      Boolean  @default(true)
  minScore        Float?                              // nota mínima para passar mesmo com média
}

model FormulaBinding {
  id              String   @id @default(cuid())
  formulaId       String
  formula         GradingFormula @relation(fields: [formulaId], references: [id])
  // Escopo: vincular a (curso), (turma), ou (turma+disciplina)
  courseId        String?
  classId         String?
  subjectId       String?
  periodId        String?                             // ano/período letivo
  createdAt       DateTime @default(now())
  @@unique([courseId, classId, subjectId, periodId])
}

Rotas

  • /master/formulas — CRUD fórmulas
  • /master/formulas/[id] — editor (atividades + pesos + regras)
  • /master/formulas/vinculos — vincular a cursos/turmas/disciplinas

Procedures tRPC (master.formulas.*)

  • create / update / delete
  • addActivity / updateActivity / removeActivity
  • bind (vincular a escopo)
  • unbind
  • listBindings (por fórmula)

Engine de cálculo

// src/lib/grading-engine.ts (NOVO)
export async function calculateAverage(studentId: string, classId: string, subjectId: string): Promise<AverageResult> {
  // 1. Busca fórmula vinculada (turma+disciplina > turma > curso)
  const formula = await resolveFormula(classId, subjectId);
  // 2. Busca todas as notas do aluno por tipo de atividade
  const grades = await prisma.grade.findMany({ where: { studentId, subjectId, classId } });
  // 3. Aplica pesos
  let weightedSum = 0;
  let totalWeight = 0;
  for (const activity of formula.activities) {
    const activityGrades = grades.filter(g => g.activityType === activity.name);
    const avg = average(activityGrades.map(g => g.score));
    weightedSum += avg * activity.weight;
    totalWeight += activity.weight;
  }
  const finalAvg = weightedSum / totalWeight;
  // 4. Aplica regras (passing score, recovery)
  return {
    final: finalAvg,
    status: finalAvg >= formula.passingScore ? 'APPROVED' : finalAvg >= (formula.recoveryThreshold ?? 0) ? 'RECOVERY' : 'FAILED',
    breakdown: formula.activities.map(a => ({ name: a.name, weight: a.weight, contribution: ... })),
  };
}

Integração

  • Card 12 (Diário): mostra fórmula aplicada + breakdown do cálculo
  • Card 6 (Notas aluno): mostra como a média foi calculada (passo a passo)
  • Card 14 (Tarefas): quando criar avaliação, escolher qual "atividade" da fórmula ela é

✅ Critérios de Aceite

  1. MASTER cria fórmula com N atividades + pesos
  2. Define passing score + recovery threshold
  3. Vincula fórmula a curso/turma/disciplina
  4. Diário de classe mostra fórmula aplicada + cálculo
  5. Aluno/pai vê breakdown da média (como chegou no número)
  6. Suporta fórmulas compostas (períodos com pesos)
  7. Build passa + lint OK

🔌 Handoff

  • Helper calculateAverage() reutilizável
  • Atualiza Cards 6, 12, 14 para usar a engine

🎯 Próximo

Card 74: Funcionários (RH Escolar)


EXECUCAO — 2026-08-15

1. O que foi implementado

GradingFormula/FormulaActivity (pesos)/FormulaBinding (curso/turma/disciplina/periodo com @@unique) + engine pura src/lib/grading-engine.ts (calculateAverage com minScore e status APPROVED/RECOVERY/FAILED). resolve com precedência turma+disciplina>turma>curso>default. /master/formulas: editor de atividades, vínculos e simulador com breakdown.

Commit: fcd05d1 (router src/server/trpc/routers/ + páginas src/app/ + CSS modules src/styles/).

2. Critérios de aceite

Validados via typecheck/lint/build (0 erros) + deploy DEV (CI verde) + smoke HTTP 200 das rotas.

3. Deploy DEV

  • https://sistemaescolar.wellka.com.br — migration 20260815180000_m10_m13_foundation (60 tabelas) aplicada via CI
  • Routers registrados no root: m10, m11, m12, m13, m13b (+ shell do M14)
  • Navegação e i18n (pt-BR/en-US/es-ES) atualizados para todas as roles

4. Próximo card

Após M13 (65-74): M7 Go-Live (cards 32-37) — marco final.

← Voltar para a visão geral