meteo-exam/shared/src/index.ts
Mathis eb1f626d17 fix : le premier membre qui rejoint une classe devient admin
- joinClass : si aucun admin 'réel' (is_creator=0) n'existe, le nouveau membre est admin
- Seed : ne crée plus de membre admin — la première personne qui rejoint gère la classe
- Régression : 'Mathis' rejoignant TERMS2 devient admin au lieu d'élève
- v0.4.1
2026-10-04 21:38:47 +02:00

202 lines
6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<WeatherKind, number> = {
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<WeatherKind, WeatherMeta> = {
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.4.1'