feat: Avertir l'enseignant quand un devoir ne respecte pas les règles (mode soft)
Quand un établissement configure des règles de devoirs en mode "soft", l'enseignant est maintenant averti avant la création si la date d'échéance ne respecte pas les contraintes (délai minimum, pas de lundi après un certain créneau). Il peut alors choisir de continuer (avec traçabilité) ou de modifier la date vers une date conforme. Le mode "hard" (blocage) reste protégé : acknowledgeWarning ne permet pas de contourner les règles bloquantes, préparant la story 5.5.
This commit is contained in:
@@ -18,10 +18,17 @@
|
||||
status: string;
|
||||
className: string | null;
|
||||
subjectName: string | null;
|
||||
hasRuleOverride: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface RuleWarning {
|
||||
ruleType: string;
|
||||
message: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface TeacherAssignment {
|
||||
id: string;
|
||||
classId: string;
|
||||
@@ -87,6 +94,11 @@
|
||||
let duplicateValidationResults = $state<Array<{ classId: string; valid: boolean; error: string | null }>>([]);
|
||||
let duplicateWarnings = $state<Array<{ classId: string; warning: string }>>([]);
|
||||
|
||||
// Rule warning modal
|
||||
let showRuleWarningModal = $state(false);
|
||||
let ruleWarnings = $state<RuleWarning[]>([]);
|
||||
let ruleConformMinDate = $state('');
|
||||
|
||||
// Class filter
|
||||
let filterClassId = $state(page.url.searchParams.get('classId') ?? '');
|
||||
|
||||
@@ -287,13 +299,14 @@
|
||||
newTitle = '';
|
||||
newDescription = '';
|
||||
newDueDate = '';
|
||||
ruleConformMinDate = '';
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
showCreateModal = false;
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
async function handleCreate(acknowledgeWarning = false) {
|
||||
if (!newClassId || !newSubjectId || !newTitle.trim() || !newDueDate) return;
|
||||
|
||||
try {
|
||||
@@ -309,9 +322,20 @@
|
||||
title: newTitle.trim(),
|
||||
description: newDescription.trim() || null,
|
||||
dueDate: newDueDate,
|
||||
acknowledgeWarning,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 409) {
|
||||
const data = await response.json().catch(() => null);
|
||||
if (data?.type === 'homework_rules_warning' && Array.isArray(data.warnings)) {
|
||||
ruleWarnings = data.warnings;
|
||||
showCreateModal = false;
|
||||
showRuleWarningModal = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
const msg =
|
||||
@@ -323,6 +347,8 @@
|
||||
}
|
||||
|
||||
closeCreateModal();
|
||||
showRuleWarningModal = false;
|
||||
ruleWarnings = [];
|
||||
await loadHomeworks();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Erreur lors de la création';
|
||||
@@ -331,6 +357,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleContinueDespiteWarning() {
|
||||
showRuleWarningModal = false;
|
||||
handleCreate(true);
|
||||
}
|
||||
|
||||
function computeConformMinDate(warnings: RuleWarning[]): string {
|
||||
let minDate = new Date();
|
||||
minDate.setDate(minDate.getDate() + 1); // au moins demain
|
||||
|
||||
for (const w of warnings) {
|
||||
if (w.ruleType === 'minimum_delay' && typeof w.params['days'] === 'number') {
|
||||
const ruleMin = new Date();
|
||||
ruleMin.setDate(ruleMin.getDate() + (w.params['days'] as number));
|
||||
if (ruleMin > minDate) minDate = ruleMin;
|
||||
}
|
||||
if (w.ruleType === 'no_monday_after') {
|
||||
// Si le lundi est interdit (deadline dépassée), proposer mardi
|
||||
// car le problème ne concerne que les devoirs pour lundi
|
||||
const nextTuesday = new Date();
|
||||
nextTuesday.setDate(nextTuesday.getDate() + ((9 - nextTuesday.getDay()) % 7 || 7));
|
||||
if (nextTuesday > minDate) minDate = nextTuesday;
|
||||
}
|
||||
}
|
||||
|
||||
// Sauter les weekends
|
||||
const day = minDate.getDay();
|
||||
if (day === 0) minDate.setDate(minDate.getDate() + 1);
|
||||
if (day === 6) minDate.setDate(minDate.getDate() + 2);
|
||||
|
||||
const y = minDate.getFullYear();
|
||||
const m = String(minDate.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(minDate.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function handleModifyDate() {
|
||||
ruleConformMinDate = computeConformMinDate(ruleWarnings);
|
||||
showRuleWarningModal = false;
|
||||
showCreateModal = true;
|
||||
newDueDate = ruleConformMinDate;
|
||||
}
|
||||
|
||||
// --- Edit ---
|
||||
function openEditModal(hw: Homework) {
|
||||
editHomework = hw;
|
||||
@@ -584,9 +652,14 @@
|
||||
<div class="homework-card" class:overdue={isOverdue(hw.dueDate)}>
|
||||
<div class="homework-header">
|
||||
<h3 class="homework-title">{hw.title}</h3>
|
||||
<span class="homework-status" class:status-published={hw.status === 'published'} class:status-deleted={hw.status === 'deleted'}>
|
||||
{hw.status === 'published' ? 'Publié' : 'Supprimé'}
|
||||
</span>
|
||||
<div class="homework-badges">
|
||||
{#if hw.hasRuleOverride}
|
||||
<span class="badge-rule-override" title="Créé malgré un avertissement de règle">⚠</span>
|
||||
{/if}
|
||||
<span class="homework-status" class:status-published={hw.status === 'published'} class:status-deleted={hw.status === 'deleted'}>
|
||||
{hw.status === 'published' ? 'Publié' : 'Supprimé'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="homework-meta">
|
||||
@@ -706,8 +779,14 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hw-due-date">Date d'échéance *</label>
|
||||
<input type="date" id="hw-due-date" bind:value={newDueDate} required min={minDueDate} />
|
||||
<small class="form-hint">La date doit être au minimum demain, hors jours fériés et vacances</small>
|
||||
<input type="date" id="hw-due-date" bind:value={newDueDate} required min={ruleConformMinDate || minDueDate} />
|
||||
{#if ruleConformMinDate}
|
||||
<small class="form-hint form-hint-rule">
|
||||
Date minimale conforme aux règles : {new Date(ruleConformMinDate + 'T00:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</small>
|
||||
{:else}
|
||||
<small class="form-hint">La date doit être au minimum demain, hors jours fériés et vacances</small>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
@@ -950,6 +1029,55 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Rule Warning Modal -->
|
||||
{#if showRuleWarningModal && ruleWarnings.length > 0}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="modal-overlay" role="presentation">
|
||||
<div
|
||||
class="modal modal-confirm"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="rule-warning-title"
|
||||
aria-describedby="rule-warning-description"
|
||||
tabindex="-1"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') handleModifyDate(); }}
|
||||
>
|
||||
<header class="modal-header modal-header-warning">
|
||||
<h2 id="rule-warning-title">Avertissement</h2>
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
<p id="rule-warning-description">
|
||||
Ce devoir ne respecte pas les règles configurées par votre établissement :
|
||||
</p>
|
||||
<ul class="rule-warning-list">
|
||||
{#each ruleWarnings as warning}
|
||||
<li class="rule-warning-item">
|
||||
<span class="rule-warning-icon">⚠</span>
|
||||
<span>{warning.message}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="rule-warning-notice">Votre choix sera enregistré.</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" onclick={handleModifyDate} disabled={isSubmitting}>
|
||||
Modifier la date
|
||||
</button>
|
||||
<button type="button" class="btn-primary" onclick={handleContinueDespiteWarning} disabled={isSubmitting}>
|
||||
{#if isSubmitting}
|
||||
Création...
|
||||
{:else}
|
||||
Continuer malgré tout
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.homework-page {
|
||||
padding: 1.5rem;
|
||||
@@ -1476,4 +1604,63 @@
|
||||
color: #6b7280;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
/* Rule conforming date hint */
|
||||
.form-hint-rule {
|
||||
color: #d97706;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Rule override badge */
|
||||
.homework-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge-rule-override {
|
||||
font-size: 0.75rem;
|
||||
color: #d97706;
|
||||
opacity: 0.7;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Rule Warning Modal */
|
||||
.modal-header-warning {
|
||||
border-bottom: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.modal-header-warning h2 {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.rule-warning-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.rule-warning-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.rule-warning-icon {
|
||||
color: #d97706;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rule-warning-notice {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user