Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me> Co-authored-by: Google/Gemini-3.5-Flash <vibecoder@antoineve.me> Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
185 lines
7.3 KiB
Python
185 lines
7.3 KiB
Python
"""Blueprint des routes de gestion des entrées de temps (WorkEntry).
|
|
|
|
Ce module gère le cycle de vie complet des entrées journalières de travail :
|
|
- Affichage de la liste historique des entrées.
|
|
- Création d'une nouvelle entrée (formulaire et traitement POST).
|
|
- Modification d'une entrée existante (formulaire pré-rempli et traitement POST).
|
|
- Suppression d'une entrée.
|
|
|
|
Règles de validation et de cohérence des données :
|
|
1. Unicité de la date : Une seule entrée (`WorkEntry`) est autorisée par jour.
|
|
2. Types de jours sans trajet : Si le type de jour est dans `day_types_without_journey()` (TT, MALADE, CONGE, RTT, FERIE),
|
|
le profil de trajet (`journey_profile_id`) est forcé à `None`.
|
|
3. Véhicule à moteur : Si le profil de trajet sélectionné n'inclut pas de véhicule à moteur (vérifié via `journey_has_motor`),
|
|
le véhicule à moteur (`motor_vehicle_id`) est forcé à `None`.
|
|
4. Plages horaires : Les plages horaires (`TimeSlot`) existantes d'une entrée sont supprimées et recréées à chaque soumission
|
|
pour simplifier la mise à jour des plages multiples.
|
|
"""
|
|
|
|
from datetime import date, time
|
|
|
|
import sqlalchemy as sa
|
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|
|
|
from app import db
|
|
from app.config_loader import (
|
|
day_types_without_journey,
|
|
get_journeys,
|
|
get_motor_vehicles,
|
|
journey_has_motor,
|
|
)
|
|
from app.models import TimeSlot, WorkEntry
|
|
|
|
bp = Blueprint("entries", __name__, url_prefix="/entries")
|
|
|
|
DAY_TYPES = [
|
|
("WORK", "Travail"),
|
|
("TT", "Télétravail"),
|
|
("GARDE", "Garde"),
|
|
("ASTREINTE", "Astreinte"),
|
|
("FORMATION", "Formation"),
|
|
("RTT", "RTT"),
|
|
("CONGE", "Congé"),
|
|
("MALADE", "Maladie"),
|
|
("FERIE", "Férié"),
|
|
]
|
|
|
|
|
|
@bp.route("/")
|
|
def list_entries():
|
|
"""Affiche la liste historique de toutes les entrées de temps enregistrées.
|
|
|
|
Cette route récupère l'ensemble des entrées (`WorkEntry`) triées par date décroissante
|
|
et les transmet au template pour affichage sous forme de tableau ou de liste.
|
|
|
|
Méthode HTTP :
|
|
GET
|
|
|
|
Retourne:
|
|
str: Le rendu HTML de la liste des entrées (`entry_list.html`).
|
|
"""
|
|
entries = db.session.scalars(sa.select(WorkEntry).order_by(WorkEntry.date.desc())).all()
|
|
return render_template("entry_list.html", entries=entries)
|
|
|
|
|
|
@bp.route("/new", methods=["GET", "POST"])
|
|
@bp.route("/<int:entry_id>/edit", methods=["GET", "POST"])
|
|
def entry_form(entry_id=None):
|
|
"""Gère l'affichage du formulaire et l'enregistrement (création ou modification) d'une entrée.
|
|
|
|
Cette route est doublement mappée pour la création (`/new`) et l'édition (`/<entry_id>/edit`).
|
|
|
|
Comportement en GET :
|
|
- Si `entry_id` est fourni, récupère l'entrée correspondante en base de données. Si elle n'existe pas,
|
|
affiche un message d'erreur et redirige vers la liste des entrées.
|
|
- Prépare le contexte nécessaire au formulaire : liste des types de jours, profils de trajets,
|
|
véhicules à moteur disponibles, types de jours sans trajet, et la date du jour par défaut.
|
|
- Rend le template `entry_form.html`.
|
|
|
|
Comportement en POST :
|
|
- Extrait et valide les données du formulaire : date, type de jour, trajet, véhicule à moteur, commentaire.
|
|
- Applique les règles de cohérence (mise à `None` du trajet ou du véhicule si les conditions ne sont pas remplies).
|
|
- En création : vérifie qu'aucune entrée n'existe déjà à cette date. Si c'est le cas, affiche une erreur.
|
|
- Enregistre ou met à jour l'objet `WorkEntry` en base de données.
|
|
- Supprime toutes les plages horaires (`TimeSlot`) existantes associées à cette entrée.
|
|
- Parcourt les listes d'heures de début (`start_time`) et de fin (`end_time`) soumises, et recrée les objets
|
|
`TimeSlot` valides associés à l'entrée.
|
|
- Valide la transaction en base de données (`db.session.commit()`), affiche un message de succès
|
|
et redirige vers le tableau de bord.
|
|
|
|
Paramètres:
|
|
entry_id (int | None): L'identifiant de l'entrée à modifier, ou None pour une nouvelle entrée.
|
|
|
|
Méthodes HTTP :
|
|
GET, POST
|
|
|
|
Retourne:
|
|
str | Response: Le rendu HTML du formulaire (GET) ou une redirection HTTP (POST / erreur).
|
|
"""
|
|
entry = None
|
|
if entry_id:
|
|
entry = db.session.get(WorkEntry, entry_id)
|
|
if not entry:
|
|
flash("Entrée introuvable.", "error")
|
|
return redirect(url_for("entries.list_entries"))
|
|
|
|
if request.method == "POST":
|
|
entry_date = date.fromisoformat(request.form["date"])
|
|
day_type = request.form["day_type"]
|
|
journey_profile_id = request.form.get("journey_profile_id") or None
|
|
motor_vehicle_id = request.form.get("motor_vehicle_id") or None
|
|
if not journey_has_motor(journey_profile_id):
|
|
motor_vehicle_id = None
|
|
comment = request.form.get("comment") or None
|
|
|
|
if day_type in day_types_without_journey():
|
|
journey_profile_id = None
|
|
|
|
if entry is None:
|
|
existing = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == entry_date))
|
|
if existing:
|
|
flash(f"Une entrée existe déjà pour le {entry_date}.", "error")
|
|
return redirect(url_for("entries.entry_form"))
|
|
entry = WorkEntry(date=entry_date)
|
|
db.session.add(entry)
|
|
|
|
entry.day_type = day_type
|
|
entry.journey_profile_id = journey_profile_id
|
|
entry.comment = comment
|
|
entry.motor_vehicle_id = motor_vehicle_id
|
|
|
|
for slot in list(entry.time_slots):
|
|
db.session.delete(slot)
|
|
|
|
starts = request.form.getlist("start_time")
|
|
ends = request.form.getlist("end_time")
|
|
for s, e in zip(starts, ends):
|
|
if s and e:
|
|
db.session.add(
|
|
TimeSlot(
|
|
entry=entry,
|
|
start_time=time.fromisoformat(s),
|
|
end_time=time.fromisoformat(e),
|
|
)
|
|
)
|
|
|
|
db.session.commit()
|
|
flash("Entrée enregistrée.", "success")
|
|
return redirect(url_for("dashboard.index"))
|
|
|
|
journeys = get_journeys()
|
|
return render_template(
|
|
"entry_form.html",
|
|
entry=entry,
|
|
day_types=DAY_TYPES,
|
|
journeys=journeys,
|
|
motor_vehicles=get_motor_vehicles(),
|
|
day_types_without_journey=day_types_without_journey(),
|
|
today=date.today().isoformat(),
|
|
)
|
|
|
|
|
|
@bp.route("/<int:entry_id>/delete", methods=["POST"])
|
|
def delete_entry(entry_id):
|
|
"""Supprime une entrée de temps existante.
|
|
|
|
Cette route récupère l'entrée par son identifiant, la supprime de la base de données
|
|
(les plages horaires associées sont également supprimées en cascade si configuré, ou gérées par SQLAlchemy),
|
|
valide la transaction, affiche un message de succès et redirige vers la liste des entrées.
|
|
|
|
Paramètres:
|
|
entry_id (int): L'identifiant de l'entrée à supprimer.
|
|
|
|
Méthode HTTP :
|
|
POST (sécurisé contre les suppressions accidentelles via GET)
|
|
|
|
Retourne:
|
|
Response: Une redirection HTTP vers la liste des entrées (`entries.list_entries`).
|
|
"""
|
|
entry = db.session.get(WorkEntry, entry_id)
|
|
if entry:
|
|
db.session.delete(entry)
|
|
db.session.commit()
|
|
flash("Entrée supprimée.", "success")
|
|
return redirect(url_for("entries.list_entries"))
|