📄 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 temGradingSystem(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 atualprisma/schema.prisma—GradingSystem,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/deleteaddActivity/updateActivity/removeActivitybind(vincular a escopo)unbindlistBindings(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
- MASTER cria fórmula com N atividades + pesos
- Define passing score + recovery threshold
- Vincula fórmula a curso/turma/disciplina
- Diário de classe mostra fórmula aplicada + cálculo
- Aluno/pai vê breakdown da média (como chegou no número)
- Suporta fórmulas compostas (períodos com pesos)
- 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.