Files
Classeo/frontend/src/lib/components/molecules/ExceptionRequestModal/ExceptionRequestModal.svelte
Mathias STRASSER 14c7849179
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
feat: Permettre aux enseignants de contourner les règles de devoirs avec justification
Akeneo permet de configurer des règles de devoirs en mode Hard qui bloquent
totalement la création. Or certains cas légitimes (sorties scolaires, événements
exceptionnels) nécessitent de passer outre ces règles. Sans mécanisme d'exception,
l'enseignant est bloqué et doit contacter manuellement la direction.

Cette implémentation ajoute un flux complet d'exception : l'enseignant justifie
sa demande (min 20 caractères), le devoir est créé immédiatement, et la direction
est notifiée par email. Le handler vérifie côté serveur que les règles sont
réellement bloquantes avant d'accepter l'exception, empêchant toute fabrication
de fausses exceptions via l'API. La direction dispose d'un rapport filtrable
par période, enseignant et type de règle.
2026-03-20 18:35:02 +01:00

293 lines
5.7 KiB
Svelte

<script lang="ts">
interface RuleWarning {
ruleType: string;
message: string;
params: Record<string, unknown>;
}
let {
warnings,
onSubmit,
onClose,
isSubmitting = false,
}: {
warnings: RuleWarning[];
onSubmit: (justification: string, ruleTypes: string[]) => void;
onClose: () => void;
isSubmitting?: boolean;
} = $props();
let justification = $state('');
let charCount = $derived(justification.length);
let isValid = $derived(charCount >= 20);
let modalElement = $state<HTMLDivElement | null>(null);
$effect(() => {
if (modalElement) {
modalElement.focus();
}
});
function handleSubmit() {
if (!isValid || isSubmitting) return;
const ruleTypes = warnings.map((w) => w.ruleType);
onSubmit(justification, ruleTypes);
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-overlay" onclick={onClose} role="presentation">
<div
bind:this={modalElement}
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="exception-request-title"
aria-describedby="exception-request-description"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => {
if (e.key === 'Escape') onClose();
}}
>
<header class="modal-header modal-header-exception">
<h2 id="exception-request-title">Demander une exception</h2>
<button class="modal-close" onclick={onClose} aria-label="Fermer">&times;</button>
</header>
<form
class="modal-body"
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<p id="exception-request-description">
Ce devoir enfreint les règles suivantes. Veuillez justifier votre demande d'exception :
</p>
<ul class="rule-list">
{#each warnings as warning}
<li class="rule-item">
<span class="rule-icon">&#128683;</span>
<span>{warning.message}</span>
</li>
{/each}
</ul>
<div class="form-group">
<label for="exception-justification">
Justification <span class="required">*</span>
</label>
<textarea
id="exception-justification"
bind:value={justification}
placeholder="Expliquez pourquoi ce devoir nécessite une exception aux règles..."
rows="4"
minlength="20"
required
></textarea>
<div class="char-counter" class:char-counter-valid={isValid} class:char-counter-invalid={charCount > 0 && !isValid}>
{charCount}/20 caractères minimum
</div>
</div>
<p class="exception-notice">
Le devoir sera créé immédiatement. La direction sera notifiée de cette exception.
</p>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick={onClose} disabled={isSubmitting}>
Annuler
</button>
<button type="submit" class="btn-primary" disabled={!isValid || isSubmitting}>
{#if isSubmitting}
Création...
{:else}
Créer avec exception
{/if}
</button>
</div>
</form>
</div>
</div>
<style>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
}
.modal {
background: white;
border-radius: 0.75rem;
width: 100%;
max-width: 32rem;
max-height: 90vh;
overflow: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.modal-header-exception {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: #f59e0b;
color: white;
border-radius: 0.75rem 0.75rem 0 0;
}
.modal-header-exception h2 {
margin: 0;
font-size: 1.1rem;
}
.modal-close {
padding: 0.25rem 0.5rem;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
color: white;
cursor: pointer;
opacity: 0.8;
}
.modal-close:hover {
opacity: 1;
}
.modal-body {
padding: 1.5rem;
}
.rule-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.rule-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-icon {
flex-shrink: 0;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #374151;
}
.required {
color: #dc2626;
}
.form-group textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
resize: vertical;
min-height: 80px;
transition: border-color 0.2s;
}
.form-group textarea:focus {
outline: none;
border-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
}
.char-counter {
margin-top: 0.25rem;
font-size: 0.75rem;
color: #6b7280;
}
.char-counter-valid {
color: #16a34a;
}
.char-counter-invalid {
color: #dc2626;
}
.exception-notice {
font-size: 0.875rem;
color: #6b7280;
font-style: italic;
margin: 1rem 0 0;
padding: 0.75rem;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 0.375rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
margin-top: 1rem;
}
.btn-primary {
padding: 0.625rem 1.25rem;
background: #f59e0b;
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover:not(:disabled) {
background: #d97706;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
padding: 0.625rem 1.25rem;
background: white;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
}
.btn-secondary:hover:not(:disabled) {
background: #f3f4f6;
}
</style>