diff --git a/app/admin/(protected)/notas/page.tsx b/app/admin/(protected)/notas/page.tsx
index 95cc2db..d552f29 100644
--- a/app/admin/(protected)/notas/page.tsx
+++ b/app/admin/(protected)/notas/page.tsx
@@ -7,7 +7,7 @@ type SearchParams = { [k: string]: string | string[] | undefined };
export default async function NotasPage({ searchParams }: { searchParams: Promise
}) {
await requireAdmin();
const params = await searchParams;
- const grupos = getGrupos(5);
+ const grupos = await getGrupos(5);
const discId = typeof params.disc === "string" ? params.disc : grupos[0]?.id;
const selected = grupos.find((g) => g.id === discId) ?? grupos[0];
diff --git a/app/admin/(protected)/page.tsx b/app/admin/(protected)/page.tsx
index 1d08740..285b5d4 100644
--- a/app/admin/(protected)/page.tsx
+++ b/app/admin/(protected)/page.tsx
@@ -5,8 +5,8 @@ import Link from "next/link";
export default async function AdminOverview() {
await requireAdmin();
- const grupos = getGrupos(5);
- const turmas = getTurmas();
+ const grupos = await getGrupos(5);
+ const turmas = await getTurmas();
const totalNotas = grupos.reduce((a, g) => a + g.notas.length, 0);
const aprovacaoMedia = grupos.reduce((a, g) => a + g.approval, 0) / grupos.length;
diff --git a/app/admin/(protected)/turmas/page.tsx b/app/admin/(protected)/turmas/page.tsx
index 6d72fe8..2a6e9b6 100644
--- a/app/admin/(protected)/turmas/page.tsx
+++ b/app/admin/(protected)/turmas/page.tsx
@@ -4,8 +4,8 @@ import { NovaTurmaForm, NovaDisciplinaForm } from "./Forms";
export default async function TurmasPage() {
await requireAdmin();
- const turmas = getTurmas();
- const grupos = getGrupos(5);
+ const turmas = await getTurmas();
+ const grupos = await getGrupos(5);
return (
diff --git a/app/page.tsx b/app/page.tsx
index 4bc0faa..267820f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -7,7 +7,7 @@ import { AcademicArticleJsonLd } from "@/components/Seo/AcademicArticleJsonLd";
import { PrintHeader } from "@/components/PrintHeader";
export default async function HomePage() {
- const grupos = getGrupos(5.0);
+ const grupos = await getGrupos(5.0);
const meta = getMetadata();
const autores = getAutores();
diff --git a/lib/data.ts b/lib/data.ts
index 09c981e..3a5f964 100644
--- a/lib/data.ts
+++ b/lib/data.ts
@@ -1,16 +1,14 @@
+import { cache } from "react";
import seed from "@/data/seed.json";
-import type { Grupo, NotaRow, SeedData, Turma, Disciplina } from "./types";
+import type { Grupo, NotaRow, SeedData, Turma } from "./types";
import { approvalRate, describe } from "./stats";
import { isSupabaseEnabled } from "./supabase";
+import { createSupabaseServer } from "./supabase-server";
const isDemo = !isSupabaseEnabled;
// Brand chart palette — matches the OKLCH tokens in globals.css
// VGA → teal · X → primary navy · Y → purple
-// Nota: Cálculo I usa a cor herdada do token editorial --accent (mesmo
-// valor hoje: oklch(70% 0.18 35)). Duplicamos aqui pra não importar globals.css;
-// se um dia o accent editorial divergir do accent da disciplina, basta
-// atualizar este map sem mexer no design system.
const DISCIPLINA_COLORS: Record = {
"Cálculo I": "oklch(70% 0.18 35)",
VGA: "oklch(64% 0.13 185)",
@@ -35,45 +33,160 @@ export function getAutores() {
return typedSeed.autores;
}
-export function getTurmas(): Turma[] {
- return typedSeed.turmas;
-}
+/* ---------------- Shared builder ---------------- */
-export function getTurma(id: string): Turma | undefined {
- return typedSeed.turmas.find((t) => t.id === id);
+/** Monta um Grupo a partir de metadados de turma/disciplina + notas brutas.
+ * `notasRaw` carrega o id real da linha (Supabase) ou um id determinístico
+ * (seed) para o admin operar. */
+function buildGrupo(
+ turmaId: string,
+ turmaNome: string,
+ discId: string,
+ discNome: string,
+ discCodigo: string,
+ notasRaw: NotaRow[],
+ cutoff: number,
+): Grupo {
+ const notas = notasRaw.map((r) => r.nota_final);
+ return {
+ id: `${turmaId}__${discId}`,
+ short: `${turmaNome.replace("Turma ", "")} · ${discCodigo === "VGA" ? "VGA" : "Cálc. I"}`,
+ label: `${turmaNome} · ${discNome}`,
+ turmaColor: TURMA_COLORS[turmaId] ?? "oklch(35% 0.08 250)",
+ disciplinaColor: DISCIPLINA_COLORS[discCodigo] ?? "oklch(70% 0.18 35)",
+ notas,
+ notasRaw,
+ stats: describe(notas),
+ approval: approvalRate(notas, cutoff),
+ };
}
-export function getGrupos(cutoff = 5.0): Grupo[] {
+/* ---------------- Seed (demo) source ---------------- */
+
+function getGruposFromSeed(cutoff: number): Grupo[] {
const grupos: Grupo[] = [];
typedSeed.turmas.forEach((turma) => {
turma.disciplinas.forEach((disc) => {
- const stats = describe(disc.notas);
const notasRaw: NotaRow[] = disc.notas.map((n, i) => ({
// ID determinístico: estável entre reloads, suficiente para o admin
- // operar no client. Em prod, este campo é sobrescrito pelo `id`
- // real do Supabase quando `getGruposFromSupabase()` for implementado.
+ // operar no client em modo demo (mutações são no-op no servidor).
id: `seed-${turma.id}-${disc.id}-${i}`,
aluno_id: `aluno_${i + 1}`,
nota_final: n,
}));
- grupos.push({
- id: `${turma.id}__${disc.id}`,
- short: `${turma.nome.replace("Turma ", "")} · ${disc.codigo === "VGA" ? "VGA" : "Cálc. I"}`,
- label: `${turma.nome} · ${disc.nome}`,
- turmaColor: TURMA_COLORS[turma.id] ?? "oklch(35% 0.08 250)",
- disciplinaColor: DISCIPLINA_COLORS[disc.codigo] ?? "oklch(70% 0.18 35)",
- notas: disc.notas,
- notasRaw,
- stats,
- approval: approvalRate(disc.notas, cutoff),
- });
+ grupos.push(
+ buildGrupo(turma.id, turma.nome, disc.id, disc.codigo, disc.nome, notasRaw, cutoff),
+ );
});
});
return grupos;
}
-export function getGrupoById(id: string, cutoff = 5.0): Grupo | undefined {
- return getGrupos(cutoff).find((g) => g.id === id);
+function getTurmasFromSeed(): Turma[] {
+ return typedSeed.turmas;
+}
+
+/* ---------------- Supabase (produção) source ---------------- */
+
+/** Formato retornado pelo nested select do Supabase. */
+type DiscRow = {
+ id: string;
+ nome: string;
+ codigo: string;
+ turma_id: string;
+ notas: { id: number; aluno_id: string; nota_final: number }[];
+};
+type TurmaRow = {
+ id: string;
+ nome: string;
+ ano: number | null;
+ descricao: string | null;
+ disciplinas: DiscRow[];
+};
+
+/** Busca turmas → disciplinas → notas em uma única query aninhada.
+ * `cache()` deduplica a chamada dentro do mesmo render (várias
+ * páginas/componentes podem pedir os grupos sem refetch). */
+const fetchTurmasRows = cache(async (): Promise => {
+ const supabase = await createSupabaseServer();
+ if (!supabase) return null;
+ const { data, error } = await supabase
+ .from("turmas")
+ .select(
+ `id, nome, ano, descricao,
+ disciplinas ( id, nome, codigo, turma_id,
+ notas ( id, aluno_id, nota_final ) )`,
+ )
+ .order("id", { ascending: true });
+ if (error) {
+ console.error("[data] Supabase fetch falhou, caindo pro seed:", error.message);
+ return null;
+ }
+ return (data as TurmaRow[]) ?? null;
+});
+
+function rowsToGrupos(rows: TurmaRow[], cutoff: number): Grupo[] {
+ const grupos: Grupo[] = [];
+ rows.forEach((turma) => {
+ (turma.disciplinas ?? []).forEach((disc) => {
+ const notasRaw: NotaRow[] = (disc.notas ?? []).map((n) => ({
+ id: String(n.id), // bigserial → string (o admin sempre lida com string)
+ aluno_id: n.aluno_id,
+ nota_final: Number(n.nota_final),
+ }));
+ grupos.push(
+ buildGrupo(turma.id, turma.nome, disc.id, disc.nome, disc.codigo, notasRaw, cutoff),
+ );
+ });
+ });
+ return grupos;
+}
+
+function rowsToTurmas(rows: TurmaRow[]): Turma[] {
+ return rows.map((t) => ({
+ id: t.id,
+ nome: t.nome,
+ ano: t.ano,
+ descricao: t.descricao ?? "",
+ disciplinas: (t.disciplinas ?? []).map((d) => ({
+ id: d.id,
+ turma_id: d.turma_id,
+ nome: d.nome,
+ codigo: d.codigo,
+ cor: DISCIPLINA_COLORS[d.codigo] ?? "#ff6b3d",
+ notas: (d.notas ?? []).map((n) => Number(n.nota_final)),
+ })),
+ }));
+}
+
+/* ---------------- API pública (async) ----------------
+ * Em produção lê do Supabase; em demo (ou se a query falhar/vier vazia)
+ * cai no seed.json. Isso fecha o circuito admin → banco → site público:
+ * as mutações do admin passam a refletir na home após revalidatePath("/"). */
+
+export async function getGrupos(cutoff = 5.0): Promise {
+ if (isSupabaseEnabled) {
+ const rows = await fetchTurmasRows();
+ if (rows && rows.length > 0) return rowsToGrupos(rows, cutoff);
+ // rows null (erro) ou vazio → fallback seed pra página nunca quebrar.
+ }
+ return getGruposFromSeed(cutoff);
+}
+
+export async function getGrupoById(id: string, cutoff = 5.0): Promise {
+ return (await getGrupos(cutoff)).find((g) => g.id === id);
+}
+
+export async function getTurmas(): Promise {
+ if (isSupabaseEnabled) {
+ const rows = await fetchTurmasRows();
+ if (rows && rows.length > 0) return rowsToTurmas(rows);
+ }
+ return getTurmasFromSeed();
+}
+
+export async function getTurma(id: string): Promise {
+ return (await getTurmas()).find((t) => t.id === id);
}
-export type { Turma, Disciplina, NotaRow };
+export type { Turma, Disciplina, NotaRow } from "./types";