// Révision d'une carte : SM-2 + événement de maîtrise + journal. import { NextResponse } from "next/server"; import { z } from "zod"; import { apiError, parseBody } from "@/lib/api.ts"; import { assertSameOrigin, requireUser } from "@/lib/auth/session.ts"; import { get, run } from "@/lib/db/index.ts"; import { initialCardState, reviewCard, type ReviewQuality } from "@/lib/learning/sm2.ts"; import { recordMasteryEvent } from "@/lib/learning/mastery.ts"; import { logActivity } from "@/lib/usage.ts"; const schema = z.object({ cardId: z.number().int().positive(), q: z.union([z.literal(2), z.literal(3), z.literal(4), z.literal(5)]), suspend: z.boolean().optional(), favorite: z.boolean().optional(), }); export async function POST(req: Request) { try { await assertSameOrigin(); const user = await requireUser(); const body = await parseBody(req, schema); const card = get<{ id: number; course_code: string; concept_id: number | null; owner_user_id: number | null }>( "SELECT id, course_code, concept_id, owner_user_id FROM flashcards WHERE id = ?", body.cardId ); if (!card || (card.owner_user_id && card.owner_user_id !== user.id)) { return NextResponse.json({ error: "Carte introuvable." }, { status: 404 }); } const enrolled = get("SELECT 1 as ok FROM enrollments WHERE user_id = ? AND course_code = ?", user.id, card.course_code); if (!enrolled) return NextResponse.json({ error: "Accès refusé." }, { status: 403 }); const state = get<{ ef: number; interval_days: number; reps: number; lapses: number }>( "SELECT ef, interval_days, reps, lapses FROM card_states WHERE user_id = ? AND card_id = ?", user.id, body.cardId ); const prev = state ? { ef: state.ef, intervalDays: state.interval_days, reps: state.reps, lapses: state.lapses } : initialCardState(); const next = reviewCard(prev, body.q as ReviewQuality); const dueExpr = next.dueInDays === 0 ? "datetime('now', '+10 minutes')" : `datetime('now', '+${next.dueInDays} days')`; run( `INSERT INTO card_states (user_id, card_id, ef, interval_days, reps, lapses, due_at, suspended, favorite, last_reviewed_at) VALUES (?, ?, ?, ?, ?, ?, ${dueExpr}, ?, ?, datetime('now')) ON CONFLICT(user_id, card_id) DO UPDATE SET ef = excluded.ef, interval_days = excluded.interval_days, reps = excluded.reps, lapses = excluded.lapses, due_at = excluded.due_at, suspended = excluded.suspended, favorite = excluded.favorite, last_reviewed_at = excluded.last_reviewed_at`, user.id, body.cardId, next.ef, next.intervalDays, next.reps, next.lapses, body.suspend ? 1 : 0, body.favorite ? 1 : 0 ); run( "INSERT INTO review_log (user_id, card_id, q, interval_before) VALUES (?, ?, ?, ?)", user.id, body.cardId, body.q, prev.intervalDays ); if (card.concept_id) { recordMasteryEvent({ userId: user.id, conceptId: card.concept_id, kind: "flashcard", correct: body.q >= 4, difficulty: body.q === 3 ? 3 : 2, }); } logActivity(user.id, "flashcards", card.course_code, 20); return NextResponse.json({ ok: true, nextDueInDays: next.dueInDays, ef: next.ef }); } catch (e) { return apiError(e); } }