feat: Permettre à l'enseignant de saisir les notes dans une grille inline
L'enseignant avait besoin d'un moyen rapide de saisir les notes après une évaluation. La grille inline permet de compléter 30 élèves en moins de 3 minutes grâce à la navigation clavier (Tab/Enter/Shift+Tab), la validation temps réel, l'auto-save debounced (500ms) et les raccourcis /abs et /disp pour marquer absents/dispensés. Les notes restent en brouillon jusqu'à publication explicite (avec confirmation modale). Une fois publiées, les élèves les voient immédiatement ; les parents après un délai de 24h (VisibiliteNotesPolicy). Le mode offline stocke les notes en IndexedDB et synchronise automatiquement au retour de la connexion. Chaque modification est auditée dans grade_events via un event subscriber qui écoute NoteSaisie/NoteModifiee sur le bus d'événements.
This commit is contained in:
83
frontend/src/lib/stores/gradeOfflineStore.ts
Normal file
83
frontend/src/lib/stores/gradeOfflineStore.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
const DB_NAME = 'classeo-grades';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'pending-grades';
|
||||
|
||||
interface PendingGrade {
|
||||
evaluationId: string;
|
||||
studentId: string;
|
||||
value: number | null;
|
||||
status: string;
|
||||
savedAt: number;
|
||||
}
|
||||
|
||||
function openDb(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
const store = db.createObjectStore(STORE_NAME, {
|
||||
keyPath: ['evaluationId', 'studentId'],
|
||||
});
|
||||
store.createIndex('byEvaluation', 'evaluationId', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function savePendingGrade(grade: PendingGrade): Promise<void> {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
tx.objectStore(STORE_NAME).put(grade);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function savePendingGrades(grades: PendingGrade[]): Promise<void> {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
for (const grade of grades) {
|
||||
store.put(grade);
|
||||
}
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPendingGrades(evaluationId: string): Promise<PendingGrade[]> {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||
const index = tx.objectStore(STORE_NAME).index('byEvaluation');
|
||||
const request = index.getAll(evaluationId);
|
||||
request.onsuccess = () => resolve(request.result as PendingGrade[]);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearPendingGrades(evaluationId: string): Promise<void> {
|
||||
const db = await openDb();
|
||||
const pending = await getPendingGrades(evaluationId);
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
for (const grade of pending) {
|
||||
store.delete([grade.evaluationId, grade.studentId]);
|
||||
}
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function hasPendingGrades(evaluationId: string): Promise<boolean> {
|
||||
const pending = await getPendingGrades(evaluationId);
|
||||
return pending.length > 0;
|
||||
}
|
||||
@@ -483,6 +483,9 @@
|
||||
|
||||
{#if ev.status === 'published'}
|
||||
<div class="evaluation-actions">
|
||||
<a class="btn-primary btn-sm" href="/dashboard/teacher/evaluations/{ev.id}/grades">
|
||||
Saisir les notes
|
||||
</a>
|
||||
<button class="btn-secondary btn-sm" onclick={() => openEditModal(ev)}>
|
||||
Modifier
|
||||
</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user