/** * Types et métadonnées partagés entre le serveur et le front. * Aucun import runtime ici : ce module est consommé tel quel par les deux côtés. */ /** Statut météorologique d'une évaluation. */ export type WeatherKind = 'soleil' | 'brouillard' | 'vent' | 'orage' | 'tempete' export const WEATHER_KINDS: WeatherKind[] = ['soleil', 'brouillard', 'vent', 'orage', 'tempete'] /** Du plus rassurant au plus menaçant. Sert aux tris et au calcul du statut d'une journée. */ export const SEVERITY: Record = { soleil: 0, brouillard: 1, vent: 2, orage: 3, tempete: 4, } export interface WeatherMeta { /** Nom affiché, ex. « Alerte Rouge ». */ label: string /** Titre long pour les modales et la légende. */ title: string icon: string /** Description pédagogique affichée dans la légende. */ hint: string /** Couleur d'accent en dur, pour les SVG et jauges. */ hex: string /** Classes Tailwind de la pastille (fond / bordure / texte). */ chip: string /** Classe de couleur du texte seule, pour les libellés sur fond neutre. */ accent: string /** Classes Tailwind du badge plein. */ badge: string } export const WEATHER: Record = { soleil: { label: 'Grand Soleil', title: 'Grand Soleil', icon: '☀️', hint: 'Journée calme. Aucun examen répertorié ni suspecté.', hex: '#0284c7', chip: 'bg-sky-500/10 border-sky-500/20 text-sky-400', accent: 'text-sky-400', badge: 'bg-sky-500/15 text-sky-300 border-sky-400/30', }, brouillard: { label: 'Brouillard', title: 'Brouillard', icon: '🌫️', hint: 'Zone d’incertitude. Rumeur ou sous-entendu d’un professeur.', hex: '#7c3aed', chip: 'bg-purple-500/10 border-purple-500/20 text-purple-400', accent: 'text-purple-400', badge: 'bg-purple-500/15 text-purple-300 border-purple-400/30', }, vent: { label: 'Coup de vent', title: 'Coup de vent', icon: '💨', hint: 'Interrogation surprise potentielle ou petit QCM, coefficient faible.', hex: '#d97706', chip: 'bg-amber-500/10 border-amber-500/20 text-amber-400', accent: 'text-amber-400', badge: 'bg-amber-500/15 text-amber-300 border-amber-400/30', }, orage: { label: 'Orage', title: 'Orage / Pluie battante', icon: '🌩️', hint: 'Devoir surveillé (DS) confirmé ou évaluation de chapitre.', hex: '#e11d48', chip: 'bg-rose-500/10 border-rose-500/20 text-rose-400', accent: 'text-rose-400', badge: 'bg-rose-500/15 text-rose-300 border-rose-400/30', }, tempete: { label: 'Alerte Rouge', title: 'Alerte Rouge / Tempête', icon: '🚨', hint: 'Examen blanc, épreuve commune ou gros devoir de fin de trimestre.', hex: '#dc2626', chip: 'bg-red-500/10 border-red-500/20 text-red-500', accent: 'text-red-500', badge: 'bg-red-500/15 text-red-300 border-red-400/30', }, } export function isWeatherKind(value: unknown): value is WeatherKind { return typeof value === 'string' && (WEATHER_KINDS as string[]).includes(value) } /** * Statut par défaut déduit du coefficient, pour pré-remplir le formulaire d'ajout. * 0 = rien de prévu, 1 = interro surprise, 2-3 = DS, 4+ = alerte rouge. */ export function kindFromWeight(weight: number): WeatherKind { if (weight <= 0) return 'soleil' if (weight < 2) return 'vent' if (weight < 4) return 'orage' return 'tempete' } /** Les coefficients proposés dans le formulaire, avec leur statut déduit. */ export const WEIGHT_PRESETS: { value: number; label: string }[] = [ { value: 0, label: '0 — coursework' }, { value: 1, label: '1 — interro' }, { value: 2, label: '2 — contrôle' }, { value: 3, label: '3 — DS' }, { value: 4, label: '4 — examen blanc' }, { value: 5, label: '5 — épreuve commune' }, ] /** Statut le plus sévère d'une journée ; `soleil` si la journée est vide. */ export function dominantKind(exams: { kind: WeatherKind }[]): WeatherKind { let best: WeatherKind = 'soleil' for (const exam of exams) { if (SEVERITY[exam.kind] > SEVERITY[best]) best = exam.kind } return best } export type Role = 'admin' | 'student' export interface Exam { id: number classId: number subject: string teacher: string | null room: string | null chapter: string | null /** ISO 8601 UTC. */ startsAt: string durationMin: number /** Coefficient : alimente la future « pression atmosphérique ». */ weight: number kind: WeatherKind note: string | null createdAt: string updatedAt: string } export interface ClassInfo { id: number name: string level: string | null /** Message d'annonce du jour (flash info), lu par tous les membres. */ flashMessage: string | null } export interface Member { id: number classId: number nickname: string role: Role /** true = créateur de la classe (ne peut pas être rétrogradé). */ isCreator: boolean } /** Charge utile de `GET /api/me`, renvoyée aussi à la création et à l'adhésion. */ export interface Session { member: Member klass: ClassInfo } export interface ApiError { error: string } /** Override météo posé par l'admin pour un jour donné (clé locale AAAA-MM-JJ). */ export interface DayWeather { classId: number dayKey: string kind: WeatherKind updatedAt: string } /** Flash info : message quotidien rédigé par l'admin. */ export interface FlashInfo { classId: number /** null = pas de message publié. */ message: string | null updatedAt: string } /** Payloads émis par le flux SSE. */ export type StreamEvent = | { type: 'exam.created'; exam: Exam } | { type: 'exam.updated'; exam: Exam } | { type: 'exam.deleted'; examId: number } | { type: 'day_weather.updated'; dayWeather: DayWeather } | { type: 'day_weather.deleted'; dayKey: string } | { type: 'flash.updated'; flash: FlashInfo | null } | { type: 'class.updated'; klass: ClassInfo } | { type: 'ping' } export const CLASS_CODE_LENGTH = 6 /** Version de l'application. Incrémentée à chaque release APK. */ export const APP_VERSION = '0.3.0'