Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
import prisma from '@/lib/prisma'
|
|
import { isCardMastered } from '@/lib/flashcards/sm2'
|
|
|
|
export interface DeckSummary {
|
|
id: string
|
|
name: string
|
|
notebookId: string | null
|
|
notebookName: string | null
|
|
totalCards: number
|
|
dueCount: number
|
|
masteredCount: number
|
|
lastReviewedAt: string | null
|
|
nextReviewAt: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
export async function listDeckSummaries(userId: string): Promise<DeckSummary[]> {
|
|
const now = new Date()
|
|
const decks = await prisma.flashcardDeck.findMany({
|
|
where: { userId },
|
|
include: {
|
|
notebook: { select: { name: true } },
|
|
flashcards: {
|
|
select: {
|
|
id: true,
|
|
interval: true,
|
|
nextReviewAt: true,
|
|
reviews: {
|
|
orderBy: { reviewedAt: 'desc' },
|
|
take: 1,
|
|
select: { reviewedAt: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: { updatedAt: 'desc' },
|
|
})
|
|
|
|
return decks.map((deck) => {
|
|
const totalCards = deck.flashcards.length
|
|
const dueCount = deck.flashcards.filter((c) => c.nextReviewAt <= now).length
|
|
// Maîtrisée = interval >= 7 jours (une semaine de bonne mémorisation)
|
|
const masteredCount = deck.flashcards.filter((c) => c.interval >= 7).length
|
|
const lastReview = deck.flashcards
|
|
.flatMap((c) => c.reviews.map((r) => r.reviewedAt))
|
|
.sort((a, b) => b.getTime() - a.getTime())[0]
|
|
|
|
// Date de la prochaine carte à réviser (la plus proche dans le futur)
|
|
const nextReviewAt = deck.flashcards
|
|
.map((c) => c.nextReviewAt)
|
|
.sort((a, b) => a.getTime() - b.getTime())[0] ?? null
|
|
|
|
return {
|
|
id: deck.id,
|
|
name: deck.name,
|
|
notebookId: deck.notebookId,
|
|
notebookName: deck.notebook?.name ?? null,
|
|
totalCards,
|
|
dueCount,
|
|
masteredCount,
|
|
lastReviewedAt: lastReview ? lastReview.toISOString() : null,
|
|
nextReviewAt: nextReviewAt ? nextReviewAt.toISOString() : null,
|
|
createdAt: deck.createdAt.toISOString(),
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function getDeckDetail(userId: string, deckId: string) {
|
|
const deck = await prisma.flashcardDeck.findFirst({
|
|
where: { id: deckId, userId },
|
|
include: {
|
|
flashcards: {
|
|
orderBy: { nextReviewAt: 'asc' },
|
|
include: {
|
|
note: { select: { id: true, title: true } },
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if (!deck) return null
|
|
|
|
const now = new Date()
|
|
return {
|
|
id: deck.id,
|
|
name: deck.name,
|
|
notebookId: deck.notebookId,
|
|
cards: deck.flashcards.map((c) => ({
|
|
id: c.id,
|
|
front: c.front,
|
|
back: c.back,
|
|
type: c.type,
|
|
interval: c.interval,
|
|
easinessFactor: c.easinessFactor,
|
|
nextReviewAt: c.nextReviewAt.toISOString(),
|
|
noteId: c.noteId,
|
|
noteTitle: c.note?.title ?? null,
|
|
due: c.nextReviewAt <= now,
|
|
mastered: isCardMastered(c.interval),
|
|
})),
|
|
}
|
|
}
|