feat: Bloquer la création de devoirs non conformes en mode hard
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Les établissements utilisant le mode "Hard" des règles de devoirs
empêchent désormais les enseignants de créer des devoirs hors règles.
Contrairement au mode "Soft" (avertissement avec possibilité de passer
outre), le mode "Hard" est un blocage strict : même acknowledgeWarning
ne permet pas de contourner.

L'API retourne 422 (au lieu de 409 pour le soft) avec des dates
conformes suggérées calculées via le calendrier scolaire (weekends,
fériés, vacances exclus). Le frontend affiche un modal de blocage
avec les raisons, des dates cliquables, et une validation client
inline qui empêche la soumission de dates non conformes.
This commit is contained in:
2026-03-19 00:35:20 +01:00
parent c46d053db7
commit 40b646a5de
15 changed files with 1496 additions and 8 deletions

View File

@@ -0,0 +1,218 @@
<script lang="ts">
interface RuleWarning {
ruleType: string;
message: string;
params: Record<string, unknown>;
}
let {
warnings,
suggestedDates = [],
onSelectDate,
onClose,
}: {
warnings: RuleWarning[];
suggestedDates: string[];
onSelectDate: (date: string) => void;
onClose: () => void;
} = $props();
let modalElement = $state<HTMLDivElement | null>(null);
$effect(() => {
if (modalElement) {
modalElement.focus();
}
});
function formatDate(dateStr: string): string {
const date = new Date(dateStr + 'T00:00:00');
return date.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
});
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-overlay" role="presentation">
<div
bind:this={modalElement}
class="modal modal-confirm"
role="alertdialog"
aria-modal="true"
aria-labelledby="rule-blocked-title"
aria-describedby="rule-blocked-description"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => {
if (e.key === 'Escape') onClose();
}}
>
<header class="modal-header modal-header-blocked">
<h2 id="rule-blocked-title">Impossible de créer ce devoir</h2>
</header>
<div class="modal-body">
<p id="rule-blocked-description">
Les règles de votre établissement interdisent la création de ce devoir :
</p>
<ul class="rule-blocked-list">
{#each warnings as warning}
<li class="rule-blocked-item">
<span class="rule-blocked-icon">&#128683;</span>
<span>{warning.message}</span>
</li>
{/each}
</ul>
{#if suggestedDates.length > 0}
<div class="suggested-dates" role="group" aria-label="Dates conformes suggérées">
<p class="suggested-dates-label">Dates conformes suggérées :</p>
<div class="suggested-dates-list">
{#each suggestedDates as date}
<button
type="button"
class="suggested-date-btn"
aria-label="Sélectionner {formatDate(date)}"
onclick={() => onSelectDate(date)}
>
{formatDate(date)}
</button>
{/each}
</div>
</div>
{/if}
<p class="rule-blocked-exception">
<span class="exception-link-placeholder">
Besoin d'une exception ? Contactez votre administration.
</span>
</p>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick={onClose}>
Modifier la date
</button>
</div>
</div>
</div>
<style>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: white;
border-radius: 0.75rem;
max-width: 500px;
width: 95%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header-blocked {
background: #dc2626;
color: white;
padding: 1rem 1.5rem;
border-radius: 0.75rem 0.75rem 0 0;
}
.modal-header-blocked h2 {
margin: 0;
font-size: 1.1rem;
}
.modal-body {
padding: 1.5rem;
}
.rule-blocked-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.rule-blocked-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem;
background: #fef2f2;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #fecaca;
}
.rule-blocked-icon {
flex-shrink: 0;
}
.suggested-dates {
margin: 1rem 0;
padding: 1rem;
background: #f0fdf4;
border-radius: 0.5rem;
border: 1px solid #bbf7d0;
}
.suggested-dates-label {
font-weight: 600;
margin: 0 0 0.5rem;
color: #166534;
}
.suggested-dates-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.suggested-date-btn {
display: block;
width: 100%;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #86efac;
border-radius: 0.375rem;
cursor: pointer;
text-align: left;
font-size: 0.9rem;
transition:
background-color 0.15s,
border-color 0.15s;
}
.suggested-date-btn:hover {
background: #dcfce7;
border-color: #22c55e;
}
.rule-blocked-exception {
margin: 1rem 0 0;
text-align: center;
}
.exception-link-placeholder {
color: #6b7280;
font-size: 0.875rem;
font-style: italic;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
}
</style>