import type { Exam, WeatherKind } from '@meteo-exam/shared' import { WEATHER, WEATHER_KINDS, WEIGHT_PRESETS, kindFromWeight } from '@meteo-exam/shared' import { h } from '../dom' import { isoDayInput } from '../dates' import { saveExam, setError } from '../store' import { showToast } from '../toast' interface Draft { subject: string teacher: string room: string chapter: string day: string time: string durationMin: number weight: number kind: WeatherKind note: string } const inputClass = 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 transition-colors' const labelClass = 'block text-[10px] font-black uppercase tracking-wider text-slate-500 mb-1.5' function field(label: string, control: HTMLElement, hint?: string): HTMLElement { return h( 'label', { class: 'block' }, h('span', { class: labelClass }, label), control, hint ? h('span', { class: 'block text-[10px] text-slate-600 font-semibold mt-1' }, hint) : null, ) } function draftFrom(exam: Exam | undefined): Draft { if (exam) { const date = new Date(exam.startsAt) return { subject: exam.subject, teacher: exam.teacher ?? '', room: exam.room ?? '', chapter: exam.chapter ?? '', day: isoDayInput(date), time: `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`, durationMin: exam.durationMin, weight: exam.weight, kind: exam.kind, note: exam.note ?? '', } } const now = new Date() now.setMinutes(0, 0, 0) now.setHours(Math.max(8, now.getHours() + 1)) return { subject: '', teacher: '', room: '', chapter: '', day: isoDayInput(now), time: `${String(now.getHours()).padStart(2, '0')}:00`, durationMin: 60, weight: 2, kind: 'orage', note: '', } } function closeOverlay(overlay: HTMLElement): void { overlay.remove() } /** Modale d'ajout / modification. `exam` absent = création. */ export function openExamForm(exam: Exam | undefined, onSaved: () => void): void { const draft = draftFrom(exam) const editing = exam !== undefined const overlay = h('div', { class: 'fixed inset-0 z-[95] bg-slate-950/80 backdrop-blur-sm flex items-end sm:items-center justify-center', }) const errorBox = h('p', { class: 'hidden text-xs font-bold text-rose-300 bg-rose-500/10 border border-rose-500/25 rounded-xl px-3 py-2' }) const showError = (message: string): void => { errorBox.textContent = message errorBox.classList.remove('hidden') } // --- Champs --------------------------------------------------------------- const subject = h('input', { class: inputClass, value: draft.subject, placeholder: 'Mathématiques' }) const teacher = h('input', { class: inputClass, value: draft.teacher, placeholder: 'M. Roussel' }) const room = h('input', { class: inputClass, value: draft.room, placeholder: 'B204' }) const chapter = h('input', { class: inputClass, value: draft.chapter, placeholder: 'Chapitre ou thème' }) const day = h('input', { type: 'date', class: inputClass, value: draft.day }) const time = h('input', { type: 'time', class: inputClass, value: draft.time }) const note = h('textarea', { class: `${inputClass} min-h-[70px] resize-none`, placeholder: 'Circonscription, calculatrice autorisée…' }) note.value = draft.note const duration = h('input', { type: 'number', class: inputClass, value: String(draft.durationMin), min: '5', max: '480', step: '5' }) const weight = h('input', { type: 'number', class: inputClass, value: String(draft.weight), min: '0', max: '20', step: '0.5' }) // Le statut découle du coefficient tant qu'on ne le surcharge pas à la main. let kindOverride = false const kindPreview = h('div', { class: 'flex flex-wrap gap-1.5' }) const renderKinds = (): void => { kindPreview.replaceChildren( ...WEATHER_KINDS.map((kind) => { const active = draft.kind === kind return h( 'button', { type: 'button', class: `px-2.5 py-1.5 rounded-xl text-[11px] font-extrabold border transition-colors ${ active ? WEATHER[kind].badge : 'bg-slate-800/60 border-slate-700 text-slate-400' }`, onclick: () => { draft.kind = kind kindOverride = true renderKinds() }, }, `${WEATHER[kind].icon} ${WEATHER[kind].label}`, ) }), ) } weight.addEventListener('input', () => { const value = Number(weight.value) if (!Number.isFinite(value)) return draft.weight = value if (!kindOverride) { draft.kind = kindFromWeight(value) renderKinds() } }) renderKinds() // --- Actions -------------------------------------------------------------- const submit = async (): Promise => { if (!subject.value.trim()) { showError('La matière est obligatoire.') return } const startsAt = new Date(`${day.value}T${time.value || '08:00'}:00`) if (Number.isNaN(startsAt.getTime())) { showError('Date ou heure invalide.') return } const durationMin = Number(duration.value) if (!Number.isFinite(durationMin) || durationMin < 5) { showError('Durée minimum : 5 minutes.') return } submitBtn.disabled = true submitBtn.classList.add('opacity-60') try { await saveExam( { subject: subject.value.trim(), teacher: teacher.value.trim() || null, room: room.value.trim() || null, chapter: chapter.value.trim() || null, startsAt: startsAt.toISOString(), durationMin, weight: Number(weight.value) || 0, kind: draft.kind, note: note.value.trim() || null, }, exam?.id, ) showToast(editing ? 'Évaluation mise à jour' : 'Évaluation ajoutée', 'success') closeOverlay(overlay) onSaved() } catch (error) { setError(error instanceof Error ? error.message : 'Enregistrement impossible') showError(error instanceof Error ? error.message : 'Enregistrement impossible') submitBtn.disabled = false submitBtn.classList.remove('opacity-60') } } const submitBtn = h( 'button', { class: 'flex-1 bg-sky-500 text-slate-950 text-sm font-extrabold py-3 rounded-xl active:scale-95 transition-transform', onclick: submit }, editing ? 'Enregistrer' : 'Ajouter au bulletin', ) const panel = h( 'div', { class: 'w-full sm:max-w-md bg-slate-900 border border-slate-700 rounded-t-[2rem] sm:rounded-[2rem] p-5 max-h-[90vh] overflow-y-auto no-scrollbar shadow-2xl' }, h( 'div', { class: 'flex items-start justify-between gap-3 mb-4' }, h( 'div', {}, h('h2', { class: 'text-lg font-black text-slate-100' }, editing ? 'Modifier l’évaluation' : 'Nouvelle évaluation'), h('p', { class: 'text-xs text-slate-400 font-semibold mt-0.5' }, 'Le statut météo découle du coefficient.'), ), h('button', { class: 'text-slate-400 hover:text-slate-200 text-lg', onclick: () => closeOverlay(overlay) }, h('i', { class: 'fa-solid fa-xmark' })), ), h( 'div', { class: 'space-y-3.5' }, field('Matière', subject), h('div', { class: 'grid grid-cols-2 gap-3' }, field('Enseignant', teacher), field('Salle', room)), field('Chapitre', chapter), h('div', { class: 'grid grid-cols-2 gap-3' }, field('Date', day), field('Heure', time)), h('div', { class: 'grid grid-cols-2 gap-3' }, field('Durée (min)', duration), field('Coefficient', weight)), field( 'Statut météo', kindPreview, kindOverride ? 'Statut forcé manuellement.' : WEIGHT_PRESETS.find((p) => p.value === draft.weight)?.label, ), field('Note', note), errorBox, h('div', { class: 'flex gap-2 pt-1' }, h('button', { class: 'bg-slate-800 border border-slate-700 text-slate-300 text-sm font-extrabold px-4 py-3 rounded-xl active:scale-95 transition-transform', onclick: () => closeOverlay(overlay) }, 'Annuler'), submitBtn, ), ), ) overlay.append(panel) overlay.addEventListener('click', (event) => { if (event.target === overlay) closeOverlay(overlay) }) document.body.append(overlay) subject.focus() }