+ >
)}
)}
diff --git a/src/components/Tests/__tests__/TestAssignmentModal.test.tsx b/src/components/Tests/__tests__/TestAssignmentModal.test.tsx
index c6265c39..4a54c3dd 100644
--- a/src/components/Tests/__tests__/TestAssignmentModal.test.tsx
+++ b/src/components/Tests/__tests__/TestAssignmentModal.test.tsx
@@ -1,11 +1,13 @@
import React from 'react';
-import { screen } from '@testing-library/react';
-import { describe, it, expect, vi } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderWithRouter } from '../../../test-utils/renderWithProviders';
import { DEFAULT_FORMAT } from '../../../types';
import type { AppSettings, Class, Student, Test as RmTest } from '../../../types';
import { decodeTestAssignment } from '../../../utils/shareCode';
+const mockDbStatus = vi.hoisted(() => ({ isConnected: false }));
+
const mockSettings: AppSettings = {
defaultGradeScaleId: 'gs1',
theme: 'dark',
@@ -31,11 +33,14 @@ const mockTest: RmTest = {
createdAt: '2024-01-01T00:00:00Z',
};
+const mockSaveTestAssignment = vi.fn().mockResolvedValue({ success: true });
+
vi.mock('../../../context/AppContext', () => ({
useApp: () => ({
students: mockStudents,
classes: [mockClass],
settings: mockSettings,
+ saveTestAssignment: mockSaveTestAssignment,
}),
}));
@@ -50,18 +55,30 @@ vi.mock('react-i18next', () => ({
}));
vi.mock('../../../hooks/useDbStatus', () => ({
- useDbStatus: () => ({ isConnected: false, status: 'idle', lastSyncAt: null, userId: null, currentUser: null }),
+ useDbStatus: () => ({
+ isConnected: mockDbStatus.isConnected,
+ status: 'idle',
+ lastSyncAt: null,
+ userId: null,
+ currentUser: null,
+ }),
}));
vi.mock('../../../services/database', () => ({
- loadSupabaseConfig: vi.fn(() => null),
+ loadSupabaseConfig: vi.fn(() => ({ supabaseUrl: 'https://x.supabase.co', supabaseAnonKey: 'anon-key' })),
}));
describe('TestAssignmentModal', () => {
- it('generates per-student share links that decode back to a valid TestAssignmentPayload', async () => {
+ beforeEach(() => {
+ mockDbStatus.isConnected = false;
+ mockSaveTestAssignment.mockClear();
+ });
+
+ it('generates per-student share links that decode back to a valid TestAssignmentPayload with distinct teacherKeys', async () => {
const { default: TestAssignmentModal } = await import('../TestAssignmentModal');
renderWithRouter();
+ const teacherKeys = new Set();
for (const student of mockStudents) {
const input = screen.getByLabelText(
`tests.assignment_link_for:{"name":"${student.name}"}`
@@ -78,6 +95,23 @@ describe('TestAssignmentModal', () => {
expect(decoded?.teacherKey).toBeTruthy();
expect(decoded?.requireSEB).toBe(true);
expect(decoded?.durationMinutes).toBe(30);
+ teacherKeys.add(decoded!.teacherKey);
}
+ // Each student's row must have its own id — a shared key would let one DB row
+ // (test_assignments is 1:1 per teacherKey) silently overwrite another's assignment.
+ expect(teacherKeys.size).toBe(mockStudents.length);
+ });
+
+ it('saves one test_assignments row per student when DB embedding is enabled', async () => {
+ mockDbStatus.isConnected = true;
+ const { default: TestAssignmentModal } = await import('../TestAssignmentModal');
+ renderWithRouter();
+
+ await waitFor(() => expect(mockSaveTestAssignment).toHaveBeenCalledTimes(mockStudents.length));
+
+ const savedStudentIds = mockSaveTestAssignment.mock.calls.map(([a]) => a.studentId).sort();
+ expect(savedStudentIds).toEqual(mockStudents.map((s) => s.id).sort());
+ const savedKeys = new Set(mockSaveTestAssignment.mock.calls.map(([a]) => a.teacherKey));
+ expect(savedKeys.size).toBe(mockStudents.length);
});
});
diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx
index 5229531c..ac3bff56 100644
--- a/src/context/AppContext.tsx
+++ b/src/context/AppContext.tsx
@@ -32,6 +32,7 @@ import type {
GradingTask,
Test,
StudentTest,
+ TestAssignment,
UserRole,
} from '../types';
import {
@@ -756,6 +757,10 @@ interface AppContextValue extends StoreData {
) => Promise>>;
deleteEssaySubmission: (submissionId: string, storagePath: string) => Promise;
getEssaySignedUrl: (storagePath: string) => Promise;
+ // Test assignments (teacher side)
+ saveTestAssignment: (a: TestAssignment) => Promise;
+ fetchMyTestAssignments: () => Promise>>;
+ fetchAssignedTestContent: (testId: string) => Promise;
// Backup / restore
importBackup: (json: string) => Promise;
// Landing / auth flow
@@ -1531,6 +1536,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
[]
);
const getEssaySignedUrl = useCallback((path: string) => storageSync.getEssaySignedUrl(path), []);
+ const saveTestAssignment = useCallback((a: TestAssignment) => storageSync.saveTestAssignment(a), []);
+ const fetchMyTestAssignments = useCallback(() => storageSync.fetchMyTestAssignments(), []);
+ const fetchAssignedTestContent = useCallback((testId: string) => storageSync.fetchAssignedTestContent(testId), []);
// ─── Landing / auth flow ──────────────────────────────────────────
const enterLocalMode = useCallback(() => {
@@ -1732,6 +1740,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
fetchEssayAssignmentByKey,
deleteEssaySubmission,
getEssaySignedUrl,
+ saveTestAssignment,
+ fetchMyTestAssignments,
+ fetchAssignedTestContent,
importBackup,
showLanding: landingState === 'show',
isCheckingSession: landingState === 'checking',
diff --git a/src/locales/de.json b/src/locales/de.json
index 71e28843..b91cdc98 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -466,6 +466,10 @@
"assignment_link_for": "Testlink für {{name}}",
"copy_all_links": "Alle Links kopieren ({{count}})",
"dev_open_as_student": "Als Schüler öffnen (nur Dev)",
+ "assignment_db_embed_help": "Speichert für jeden Schüler einen Zuweisungsdatensatz, sodass er in dessen Aufgabenliste im Portal erscheint. Der Link selbst funktioniert weiterhin eigenständig, ohne Portalkonto.",
+ "assignment_save_all_to_db": "Alle in Datenbank speichern",
+ "assignment_saved_count": "{{saved}}/{{total}} gespeichert",
+ "assignment_save_partial_error": "{{count}} Zuweisung(en) konnten nicht gespeichert werden — erneut versuchen",
"results": {
"title": "Testergebnisse",
"not_found": "Diese Abgabe wurde nicht gefunden.",
@@ -1259,9 +1263,6 @@
"self_assess_save": "Speichern",
"self_assess_saved": "Gespeichert!",
"self_assessed_on": "Selbst eingeschätzt am {{date}}",
- "essays_pending": "Aufsätze zu vervollständigen",
- "essays_completed": "Eingereichte Aufsätze",
- "essays_empty": "Noch keine Aufsatzaufgaben.",
"essay_open": "Aufsatz öffnen",
"essay_due": "Fällig am {{date}}",
"essay_due_soon": "Bald fällig",
@@ -1273,8 +1274,25 @@
"essay_time": "{{n}} Min.",
"essay_seb_required": "Safe Exam Browser erforderlich",
"essay_submitted_words": "Eingereicht am {{date}} · {{wordCount}} Wörter",
- "essays_load_error": "Ihre Aufsatzaufgaben konnten nicht geladen werden. Bitte aktualisieren Sie die Seite.",
- "peer_reviews_received": "Erhaltene Peer-Reviews"
+ "peer_reviews_received": "Erhaltene Peer-Reviews",
+ "my_work": "Meine Aufgaben",
+ "work_overdue": "Überfällig",
+ "work_planned": "Geplant",
+ "work_completed": "Abgeschlossen",
+ "work_load_error": "Ihre zugewiesenen Aufgaben konnten nicht geladen werden. Bitte aktualisieren Sie die Seite.",
+ "my_progress": "Mein Fortschritt",
+ "progress_view_label": "Ansicht",
+ "progress_view_combined": "Kombiniert",
+ "test_open": "Test öffnen",
+ "test_opening": "Wird geöffnet…",
+ "test_due": "Fällig am {{date}}",
+ "test_due_soon": "Bald fällig",
+ "test_expired": "Abgelaufen",
+ "test_in_progress": "In Bearbeitung",
+ "test_submitted": "Eingereicht",
+ "test_duration": "{{n}} Min.",
+ "test_seb_required": "Safe Exam Browser erforderlich",
+ "test_open_error": "Dieser Test konnte nicht geladen werden. Bitten Sie Ihre Lehrkraft, den Link erneut zu senden."
},
"selfAssess": {
"title": "GER-Selbstbewertung",
@@ -1958,7 +1976,7 @@
"route_speaking_session_label": "Sprechsitzung",
"route_speaking_session_desc": "Strukturierte Sprechbewertungen mit sechs GER-orientierten Dimensionen.",
"route_student_portal_label": "Schülerportal",
- "route_student_portal_desc": "Schüler sehen Feedback, reichen Aufsätze ein und führen Selbstbeurteilungen durch. Kein Login erforderlich.",
+ "route_student_portal_desc": "Schüler sehen ihren Noten-/Fortschrittsverlauf, eine Aufgabenliste zugewiesener Aufsätze und Tests, und führen Selbstbeurteilungen durch. Über einen Freigabelink erreichbar, kein Login erforderlich.",
"route_take_test_label": "Test ablegen",
"route_take_test_desc": "Schüler öffnen einen geteilten Link, um einen Test zu absolvieren — Fragen beantworten, optionaler Countdown-Timer und Einreichen. Kein Login erforderlich.",
"route_attachments_label": "Anhänge",
@@ -2199,6 +2217,14 @@
"gr_test_summary_body_suffix": "bei einem Test, um das Panel",
"gr_test_summary_export_panel": "Zusammenfassung exportieren",
"gr_test_summary_body2": "zu finden. Wähle einen einzelnen Schüler oder die ganze Klasse und exportiere dann eine PDF- oder Word-Zusammenfassung mit Genauigkeit pro Frage und einer Stark/Entwicklung/Schwach-Aufschlüsselung nach verknüpftem Standard oder GER-Deskriptor — nützlich, um zu erkennen, welche Themen ein Schüler oder die gesamte Klasse wiederholen muss.",
+ "gr_portal_title": "Schülerportal",
+ "gr_portal_intro_prefix": "Jeder Schüler erhält ein persönliches Dashboard mit Notenverlauf, GER-Fortschritt und Feedback. Teile es über die Schaltfläche",
+ "gr_portal_intro_button": "Portal anzeigen",
+ "gr_portal_intro_suffix": "auf der Schülerseite, oder lass den Schüler sich selbst anmelden (E-Mail-OTP oder ein von der Lehrkraft festgelegtes Passwort) — auch direkt erreichbar unter",
+ "gr_portal_work_title": "Meine Aufgaben",
+ "gr_portal_work_body": "Eine kombinierte Aufgabenliste aus jedem zugewiesenen Aufsatz und Test, gruppiert nach Überfällig, Geplant und Abgeschlossen. Jeder Eintrag zeigt seinen Status (nicht begonnen, in Bearbeitung, eingereicht) und die Frist, sofern die Lehrkraft eine festgelegt hat. Tests öffnen sich mit einem Klick aus der Liste — kein Suchen nach einem Link mit Testcode mehr nötig.",
+ "gr_portal_progress_title": "Mein Fortschritt",
+ "gr_portal_progress_body": "Ein Radardiagramm der eigenen Punktzahlen des Schülers pro Kriterium, entweder kombiniert über alle bewerteten Rubrics (Kriterien mit demselben Namen werden gemittelt) oder gefiltert auf eine einzelne Rubric — sichtbar, sobald mindestens 3 verschiedene bewertete Kriterien vorliegen.",
"ce_overview_title": "GER-Übersicht",
"ce_overview_intro": "Das GER-Modul (Gemeinsamer Europäischer Referenzrahmen) verfolgt Sprachkompetenz in Lesen, Schreiben, Sprechen und Hören — abgestimmt auf niederländische VO-Zielniveaus (VMBO-BB bis VWO).",
"ce_overview_item_whole_class": "Klassenweite Übersicht (/cefr-overview) — Heatmap aller Schüler über GER-Niveaus pro Fertigkeit.",
diff --git a/src/locales/en.json b/src/locales/en.json
index 20e174ff..29240bdf 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -466,6 +466,10 @@
"assignment_link_for": "Test link for {{name}}",
"copy_all_links": "Copy all links ({{count}})",
"dev_open_as_student": "Open as student (dev only)",
+ "assignment_db_embed_help": "Saves an assignment record for each student so it appears in their portal to-do list. The link itself still works standalone, without a portal account.",
+ "assignment_save_all_to_db": "Save all to database",
+ "assignment_saved_count": "Saved {{saved}}/{{total}}",
+ "assignment_save_partial_error": "{{count}} assignment(s) failed to save — try again",
"results": {
"title": "Test results",
"not_found": "This submission could not be found.",
@@ -1259,9 +1263,6 @@
"self_assess_save": "Save",
"self_assess_saved": "Saved!",
"self_assessed_on": "Self-assessed {{date}}",
- "essays_pending": "Essays to complete",
- "essays_completed": "Submitted essays",
- "essays_empty": "No essay assignments yet.",
"essay_open": "Open essay",
"essay_due": "Due {{date}}",
"essay_due_soon": "Due soon",
@@ -1273,8 +1274,25 @@
"essay_time": "{{n}} min",
"essay_seb_required": "Safe Exam Browser required",
"essay_submitted_words": "Submitted {{date}} · {{wordCount}} words",
- "essays_load_error": "Could not load your essay assignments. Please try refreshing the page.",
- "peer_reviews_received": "Peer Reviews Received"
+ "peer_reviews_received": "Peer Reviews Received",
+ "my_work": "My work",
+ "work_overdue": "Overdue",
+ "work_planned": "Planned",
+ "work_completed": "Completed",
+ "work_load_error": "Could not load your assigned work. Please try refreshing the page.",
+ "my_progress": "My progress",
+ "progress_view_label": "View",
+ "progress_view_combined": "Combined",
+ "test_open": "Open test",
+ "test_opening": "Opening…",
+ "test_due": "Due {{date}}",
+ "test_due_soon": "Due soon",
+ "test_expired": "Expired",
+ "test_in_progress": "In progress",
+ "test_submitted": "Submitted",
+ "test_duration": "{{n}} min",
+ "test_seb_required": "Safe Exam Browser required",
+ "test_open_error": "Couldn't load this test. Ask your teacher to resend the link."
},
"selfAssess": {
"title": "CEFR Self-Assessment",
@@ -1958,7 +1976,7 @@
"route_speaking_session_label": "Speaking Session",
"route_speaking_session_desc": "Structured speaking assessments with six CEFR-aligned dimensions.",
"route_student_portal_label": "Student Portal",
- "route_student_portal_desc": "Students view feedback, submit essays, and complete self-assessments. No login required.",
+ "route_student_portal_desc": "Students view grade/progress history, a to-do list of assigned essays and tests, and complete self-assessments. Reachable via a share link, no login required.",
"route_take_test_label": "Take a Test",
"route_take_test_desc": "Students open a shared link to take a test — answer questions, optional countdown timer, and submit. No login required.",
"route_attachments_label": "Attachments",
@@ -2197,6 +2215,14 @@
"gr_test_summary_body_suffix": "on a test to find the",
"gr_test_summary_export_panel": "Export summary",
"gr_test_summary_body2": "panel. Choose a single student or the whole class, then export a PDF or Word summary with per-question accuracy and a strong/developing/weak breakdown by linked standard or CEFR descriptor — useful for spotting which topics a student or the class as a whole needs to revisit.",
+ "gr_portal_title": "Student Portal",
+ "gr_portal_intro_prefix": "Each student gets a personal dashboard with their grade history, CEFR progress, and feedback. Share it via the",
+ "gr_portal_intro_button": "View portal",
+ "gr_portal_intro_suffix": "button on the Students page, or let the student log in themselves (email OTP or a teacher-set password) — it's also reachable directly at",
+ "gr_portal_work_title": "My Work",
+ "gr_portal_work_body": "A combined to-do list of every assigned essay and test, grouped into Overdue, Planned, and Completed. Each item shows its status (not started, in progress, submitted) and due date where the teacher set one. Tests open in one click from the list — no need to hunt down a share-code link.",
+ "gr_portal_progress_title": "My Progress",
+ "gr_portal_progress_body": "A radar chart of the student's own per-criterion scores, either combined across every rubric they've been graded on (criteria with the same name are averaged together) or filtered to a single rubric — shown once they have at least 3 distinct graded criteria.",
"ce_overview_title": "CEFR Overview",
"ce_overview_intro": "The CEFR (Common European Framework of Reference) module tracks language proficiency across Reading, Writing, Speaking, and Listening — aligned to Dutch VO targets (VMBO-BB through VWO).",
"ce_overview_item_whole_class": "Whole-class overview (/cefr-overview) — heatmap of all students across CEFR levels per skill.",
diff --git a/src/locales/es.json b/src/locales/es.json
index 1a639c43..1f25818c 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -466,6 +466,10 @@
"assignment_link_for": "Enlace de la prueba para {{name}}",
"copy_all_links": "Copiar todos los enlaces ({{count}})",
"dev_open_as_student": "Abrir como estudiante (solo dev)",
+ "assignment_db_embed_help": "Guarda un registro de asignación para cada estudiante para que aparezca en su lista de tareas del portal. El enlace sigue funcionando de forma independiente, sin cuenta de portal.",
+ "assignment_save_all_to_db": "Guardar todo en la base de datos",
+ "assignment_saved_count": "{{saved}}/{{total}} guardado(s)",
+ "assignment_save_partial_error": "{{count}} asignación(es) no se pudieron guardar — inténtalo de nuevo",
"results": {
"title": "Resultados de la prueba",
"not_found": "No se ha podido encontrar esta entrega.",
@@ -1259,9 +1263,6 @@
"self_assess_save": "Guardar",
"self_assess_saved": "¡Guardado!",
"self_assessed_on": "Autoevaluado el {{date}}",
- "essays_pending": "Ensayos por completar",
- "essays_completed": "Ensayos enviados",
- "essays_empty": "Aún no hay tareas de ensayo.",
"essay_open": "Abrir ensayo",
"essay_due": "Fecha límite {{date}}",
"essay_due_soon": "Fecha límite próxima",
@@ -1273,8 +1274,25 @@
"essay_time": "{{n}} min",
"essay_seb_required": "Se requiere Safe Exam Browser",
"essay_submitted_words": "Enviado el {{date}} · {{wordCount}} palabras",
- "essays_load_error": "No se pudieron cargar tus tareas de ensayo. Por favor, intenta actualizar la página.",
- "peer_reviews_received": "Revisiones de pares recibidas"
+ "peer_reviews_received": "Revisiones de pares recibidas",
+ "my_work": "Mi trabajo",
+ "work_overdue": "Atrasado",
+ "work_planned": "Planificado",
+ "work_completed": "Completado",
+ "work_load_error": "No se pudo cargar tu trabajo asignado. Por favor, intenta actualizar la página.",
+ "my_progress": "Mi progreso",
+ "progress_view_label": "Vista",
+ "progress_view_combined": "Combinada",
+ "test_open": "Abrir examen",
+ "test_opening": "Abriendo…",
+ "test_due": "Fecha límite {{date}}",
+ "test_due_soon": "Fecha límite próxima",
+ "test_expired": "Expirado",
+ "test_in_progress": "En curso",
+ "test_submitted": "Enviado",
+ "test_duration": "{{n}} min",
+ "test_seb_required": "Se requiere Safe Exam Browser",
+ "test_open_error": "No se pudo cargar este examen. Pide a tu profesor que reenvíe el enlace."
},
"selfAssess": {
"title": "Autoevaluación MCER",
@@ -1958,7 +1976,7 @@
"route_speaking_session_label": "Sesión oral",
"route_speaking_session_desc": "Evaluaciones orales estructuradas con seis dimensiones alineadas con el MCER.",
"route_student_portal_label": "Portal del alumno",
- "route_student_portal_desc": "Los alumnos ven comentarios, entregan redacciones y completan autoevaluaciones. No se requiere inicio de sesión.",
+ "route_student_portal_desc": "Los alumnos ven su historial de calificaciones/progreso, una lista de tareas de ensayos y exámenes asignados, y completan autoevaluaciones. Accesible mediante un enlace para compartir, no se requiere inicio de sesión.",
"route_take_test_label": "Hacer una prueba",
"route_take_test_desc": "Los alumnos abren un enlace compartido para hacer una prueba — responder preguntas, temporizador opcional y enviar. No se requiere inicio de sesión.",
"route_attachments_label": "Adjuntos",
@@ -2199,6 +2217,14 @@
"gr_test_summary_body_suffix": "en una prueba para encontrar el panel",
"gr_test_summary_export_panel": "Exportar resumen",
"gr_test_summary_body2": "Elige un alumno individual o toda la clase, luego exporta un resumen en PDF o Word con la precisión por pregunta y un desglose fuerte/en desarrollo/débil por estándar o descriptor MCER vinculado — útil para detectar qué temas necesita repasar un alumno o toda la clase.",
+ "gr_portal_title": "Portal del estudiante",
+ "gr_portal_intro_prefix": "Cada estudiante tiene un panel personal con su historial de calificaciones, progreso MCER y comentarios. Compártelo con el botón",
+ "gr_portal_intro_button": "Ver portal",
+ "gr_portal_intro_suffix": "de la página de Estudiantes, o deja que el propio estudiante inicie sesión (OTP por correo o una contraseña definida por el profesor) — también accesible directamente en",
+ "gr_portal_work_title": "Mi trabajo",
+ "gr_portal_work_body": "Una lista combinada de tareas de cada ensayo y examen asignado, agrupados en Atrasado, Planificado y Completado. Cada elemento muestra su estado (no iniciado, en curso, enviado) y la fecha límite cuando el profesor la haya definido. Los exámenes se abren con un clic desde la lista — sin necesidad de buscar un enlace con código.",
+ "gr_portal_progress_title": "Mi progreso",
+ "gr_portal_progress_body": "Un gráfico de radar con las propias puntuaciones del estudiante por criterio, combinado en todas las rúbricas calificadas (los criterios con el mismo nombre se promedian) o filtrado a una sola rúbrica — se muestra en cuanto haya al menos 3 criterios calificados distintos.",
"ce_overview_title": "Resumen del MCER",
"ce_overview_intro": "El módulo MCER (Marco Común Europeo de Referencia) sigue la competencia lingüística en Lectura, Escritura, Expresión oral y Comprensión oral — alineado con los objetivos neerlandeses VO (de VMBO-BB a VWO).",
"ce_overview_item_whole_class": "Resumen de toda la clase (/cefr-overview) — mapa de calor de todos los alumnos según los niveles MCER por destreza.",
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 9662c68a..11045b4e 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -466,6 +466,10 @@
"assignment_link_for": "Lien du test pour {{name}}",
"copy_all_links": "Copier tous les liens ({{count}})",
"dev_open_as_student": "Ouvrir en tant qu'élève (dev uniquement)",
+ "assignment_db_embed_help": "Enregistre une attribution pour chaque élève afin qu'elle apparaisse dans sa liste de travaux du portail. Le lien fonctionne toujours seul, sans compte portail.",
+ "assignment_save_all_to_db": "Tout enregistrer dans la base de données",
+ "assignment_saved_count": "{{saved}}/{{total}} enregistré(s)",
+ "assignment_save_partial_error": "{{count}} attribution(s) n'ont pas pu être enregistrées — réessayez",
"results": {
"title": "Résultats du test",
"not_found": "Cette soumission n'a pas pu être trouvée.",
@@ -1259,9 +1263,6 @@
"self_assess_save": "Enregistrer",
"self_assess_saved": "Enregistré !",
"self_assessed_on": "Auto-évalué le {{date}}",
- "essays_pending": "Essais à rédiger",
- "essays_completed": "Essais soumis",
- "essays_empty": "Aucun devoir d'essai pour l'instant.",
"essay_open": "Ouvrir l'essai",
"essay_due": "Échéance {{date}}",
"essay_due_soon": "Bientôt dû",
@@ -1273,8 +1274,25 @@
"essay_time": "{{n}} min",
"essay_seb_required": "Safe Exam Browser requis",
"essay_submitted_words": "Soumis le {{date}} · {{wordCount}} mots",
- "essays_load_error": "Impossible de charger vos devoirs d'essai. Veuillez actualiser la page.",
- "peer_reviews_received": "Évaluations par les pairs reçues"
+ "peer_reviews_received": "Évaluations par les pairs reçues",
+ "my_work": "Mon travail",
+ "work_overdue": "En retard",
+ "work_planned": "Prévu",
+ "work_completed": "Terminé",
+ "work_load_error": "Impossible de charger votre travail assigné. Veuillez actualiser la page.",
+ "my_progress": "Ma progression",
+ "progress_view_label": "Vue",
+ "progress_view_combined": "Combinée",
+ "test_open": "Ouvrir le test",
+ "test_opening": "Ouverture…",
+ "test_due": "Échéance {{date}}",
+ "test_due_soon": "Bientôt dû",
+ "test_expired": "Expiré",
+ "test_in_progress": "En cours",
+ "test_submitted": "Soumis",
+ "test_duration": "{{n}} min",
+ "test_seb_required": "Safe Exam Browser requis",
+ "test_open_error": "Impossible de charger ce test. Demandez à votre enseignant de renvoyer le lien."
},
"selfAssess": {
"title": "Auto-évaluation CECR",
@@ -1958,7 +1976,7 @@
"route_speaking_session_label": "Session orale",
"route_speaking_session_desc": "Évaluations orales structurées avec six dimensions alignées sur le CECRL.",
"route_student_portal_label": "Portail élève",
- "route_student_portal_desc": "Les élèves consultent les retours, soumettent des rédactions et effectuent des autoévaluations. Aucune connexion requise.",
+ "route_student_portal_desc": "Les élèves consultent leur historique de notes/progression, une liste de tâches des essais et tests attribués, et effectuent des autoévaluations. Accessible via un lien de partage, aucune connexion requise.",
"route_take_test_label": "Passer un test",
"route_take_test_desc": "Les élèves ouvrent un lien partagé pour passer un test — répondre aux questions, minuteur optionnel et soumission. Aucune connexion requise.",
"route_attachments_label": "Pièces jointes",
@@ -2199,6 +2217,14 @@
"gr_test_summary_body_suffix": "sur un test pour trouver le panneau",
"gr_test_summary_export_panel": "Exporter le résumé",
"gr_test_summary_body2": "Choisissez un élève unique ou toute la classe, puis exportez un résumé PDF ou Word avec la précision par question et une répartition fort/en développement/faible par référentiel ou descripteur CECRL lié — utile pour repérer quels sujets un élève ou la classe entière doit revoir.",
+ "gr_portal_title": "Portail élève",
+ "gr_portal_intro_prefix": "Chaque élève dispose d'un tableau de bord personnel avec son historique de notes, sa progression CECRL et ses retours. Partagez-le via le bouton",
+ "gr_portal_intro_button": "Voir le portail",
+ "gr_portal_intro_suffix": "de la page Élèves, ou laissez l'élève se connecter lui-même (OTP par e-mail ou mot de passe défini par l'enseignant) — également accessible directement à",
+ "gr_portal_work_title": "Mon travail",
+ "gr_portal_work_body": "Une liste de tâches combinant chaque essai et test attribués, regroupés en En retard, Prévu et Terminé. Chaque élément affiche son statut (non commencé, en cours, soumis) et sa date limite lorsque l'enseignant en a défini une. Les tests s'ouvrent en un clic depuis la liste — plus besoin de retrouver un lien de code d'accès.",
+ "gr_portal_progress_title": "Ma progression",
+ "gr_portal_progress_body": "Un graphique radar des propres scores de l'élève par critère, combiné sur tous les référentiels évalués (les critères de même nom sont moyennés) ou filtré sur un seul référentiel — affiché dès qu'au moins 3 critères distincts ont été évalués.",
"ce_overview_title": "Vue d'ensemble CECRL",
"ce_overview_intro": "Le module CECRL (Cadre européen commun de référence pour les langues) suit la compétence linguistique en Lecture, Écriture, Expression orale et Compréhension orale — aligné sur les objectifs néerlandais VO (de VMBO-BB à VWO).",
"ce_overview_item_whole_class": "Vue d'ensemble de la classe (/cefr-overview) — carte de chaleur de tous les élèves selon les niveaux CECRL par compétence.",
diff --git a/src/locales/nl.json b/src/locales/nl.json
index 61c6d450..1ffc1d69 100644
--- a/src/locales/nl.json
+++ b/src/locales/nl.json
@@ -466,6 +466,10 @@
"assignment_link_for": "Toetslink voor {{name}}",
"copy_all_links": "Alle links kopiëren ({{count}})",
"dev_open_as_student": "Openen als leerling (alleen dev)",
+ "assignment_db_embed_help": "Slaat een toewijzingsrecord op voor elke leerling zodat deze in hun portaal-takenlijst verschijnt. De link zelf werkt nog steeds op zichzelf, zonder portaalaccount.",
+ "assignment_save_all_to_db": "Alles opslaan in database",
+ "assignment_saved_count": "{{saved}}/{{total}} opgeslagen",
+ "assignment_save_partial_error": "{{count}} toewijzing(en) konden niet worden opgeslagen — probeer opnieuw",
"results": {
"title": "Toetsresultaten",
"not_found": "Deze inzending kon niet worden gevonden.",
@@ -1259,9 +1263,6 @@
"self_assess_save": "Opslaan",
"self_assess_saved": "Opgeslagen!",
"self_assessed_on": "Zelfbeoordeeld op {{date}}",
- "essays_pending": "Nog te maken essays",
- "essays_completed": "Ingediende essays",
- "essays_empty": "Nog geen essayopdrachten.",
"essay_open": "Essay openen",
"essay_due": "Inleveren voor {{date}}",
"essay_due_soon": "Bijna verlopen",
@@ -1273,8 +1274,25 @@
"essay_time": "{{n}} min",
"essay_seb_required": "Safe Exam Browser vereist",
"essay_submitted_words": "Ingediend op {{date}} · {{wordCount}} woorden",
- "essays_load_error": "Essayopdrachten konden niet worden geladen. Probeer de pagina te vernieuwen.",
- "peer_reviews_received": "Ontvangen peerreviews"
+ "peer_reviews_received": "Ontvangen peerreviews",
+ "my_work": "Mijn werk",
+ "work_overdue": "Te laat",
+ "work_planned": "Gepland",
+ "work_completed": "Voltooid",
+ "work_load_error": "Je toegewezen werk kon niet worden geladen. Probeer de pagina te vernieuwen.",
+ "my_progress": "Mijn voortgang",
+ "progress_view_label": "Weergave",
+ "progress_view_combined": "Gecombineerd",
+ "test_open": "Toets openen",
+ "test_opening": "Openen…",
+ "test_due": "Inleveren voor {{date}}",
+ "test_due_soon": "Bijna verlopen",
+ "test_expired": "Verlopen",
+ "test_in_progress": "Bezig",
+ "test_submitted": "Ingediend",
+ "test_duration": "{{n}} min",
+ "test_seb_required": "Safe Exam Browser vereist",
+ "test_open_error": "Deze toets kon niet worden geladen. Vraag je docent de link opnieuw te sturen."
},
"selfAssess": {
"title": "ERK Zelfbeoordeling",
@@ -1958,7 +1976,7 @@
"route_speaking_session_label": "Spreeksessie",
"route_speaking_session_desc": "Gestructureerde spreekbeoordelingen met zes ERK-gerelateerde dimensies.",
"route_student_portal_label": "Leerlingenportaal",
- "route_student_portal_desc": "Leerlingen bekijken feedback, dienen essays in en voltooien zelfbeoordelingen. Geen login vereist.",
+ "route_student_portal_desc": "Leerlingen bekijken hun cijfer-/voortgangsgeschiedenis, een takenlijst met toegewezen essays en toetsen, en voltooien zelfbeoordelingen. Bereikbaar via een deellink, geen login vereist.",
"route_take_test_label": "Toets maken",
"route_take_test_desc": "Leerlingen openen een gedeelde link om een toets te maken — vragen beantwoorden, optionele afteltimer en inzenden. Geen login vereist.",
"route_attachments_label": "Bijlagen",
@@ -2199,6 +2217,14 @@
"gr_test_summary_body_suffix": "bij een toets om het paneel",
"gr_test_summary_export_panel": "Overzicht exporteren",
"gr_test_summary_body2": "te vinden. Kies een individuele leerling of de hele klas, en exporteer dan een PDF- of Word-overzicht met nauwkeurigheid per vraag en een sterk/ontwikkelend/zwak-overzicht per gekoppelde standaard of ERK-descriptor — handig om te zien welke onderwerpen een leerling of de klas als geheel moet herhalen.",
+ "gr_portal_title": "Leerlingenportaal",
+ "gr_portal_intro_prefix": "Elke leerling krijgt een persoonlijk dashboard met zijn cijfergeschiedenis, ERK-voortgang en feedback. Deel het via de knop",
+ "gr_portal_intro_button": "Portaal bekijken",
+ "gr_portal_intro_suffix": "op de leerlingenpagina, of laat de leerling zelf inloggen (e-mail-OTP of een door de docent ingesteld wachtwoord) — ook rechtstreeks bereikbaar via",
+ "gr_portal_work_title": "Mijn werk",
+ "gr_portal_work_body": "Een gecombineerde takenlijst van elk toegewezen essay en elke toets, gegroepeerd in Te laat, Gepland en Voltooid. Elk item toont de status (nog niet begonnen, bezig, ingediend) en de deadline indien de docent die heeft ingesteld. Toetsen openen met één klik vanuit de lijst — geen link met toetscode meer nodig.",
+ "gr_portal_progress_title": "Mijn voortgang",
+ "gr_portal_progress_body": "Een radargrafiek van de eigen scores per criterium van de leerling, gecombineerd over alle beoordeelde rubrics (criteria met dezelfde naam worden gemiddeld) of gefilterd op één rubric — zichtbaar zodra er minstens 3 verschillende beoordeelde criteria zijn.",
"ce_overview_title": "ERK-overzicht",
"ce_overview_intro": "De ERK-module (Europees Referentiekader) volgt taalvaardigheid op het gebied van Lezen, Schrijven, Spreken en Luisteren — afgestemd op Nederlandse VO-doelen (VMBO-BB tot en met VWO).",
"ce_overview_item_whole_class": "Klasbreed overzicht (/cefr-overview) — heatmap van alle leerlingen over ERK-niveaus per vaardigheid.",
diff --git a/src/pages/DocsPage.tsx b/src/pages/DocsPage.tsx
index 16d8196a..5df6a7fd 100644
--- a/src/pages/DocsPage.tsx
+++ b/src/pages/DocsPage.tsx
@@ -948,6 +948,25 @@ function GradingTab() {
{t('docs.gr_test_summary_body2')}