diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 4d4aeca..eb1ea68 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,3 +1,64 @@ -from flask import Blueprint +from flask import Blueprint, render_template +from datetime import date, timedelta +import sqlalchemy as sa +from app import db +from app.models import WorkEntry +from app.business.time_calc import minutes_to_str, work_minutes_reference +from app.business.travel_calc import compute_km_for_entry, compute_co2_grams +from app.business.leave_calc import compute_leave_used, get_or_create_balance +from app.config_loader import get_vehicles, get_journeys bp = Blueprint("dashboard", __name__) + + +@bp.route("/") +def index(): + today = date.today() + year = today.year + + monday = today - timedelta(days=today.weekday()) + sunday = monday + timedelta(days=6) + week_entries = db.session.scalars( + sa.select(WorkEntry).where(WorkEntry.date.between(monday, sunday)) + ).all() + + week_actual = sum(e.total_minutes() for e in week_entries) + week_ref = sum(work_minutes_reference(e.day_type) for e in week_entries) + week_balance = week_actual - week_ref + + month_start = today.replace(day=1) + month_entries = db.session.scalars( + sa.select(WorkEntry).where(WorkEntry.date.between(month_start, today)) + ).all() + + vehicles = get_vehicles() + journeys = get_journeys() + + month_km = {} + month_co2 = 0.0 + for entry in month_entries: + km = compute_km_for_entry(entry.journey_profile_id, journeys) + for v, d in km.items(): + month_km[v] = month_km.get(v, 0) + d + month_co2 += compute_co2_grams(km, vehicles) + + balance = get_or_create_balance(year) + used = compute_leave_used(year) + + today_entry = db.session.scalar( + sa.select(WorkEntry).where(WorkEntry.date == today) + ) + + return render_template( + "dashboard.html", + today=today, + today_entry=today_entry, + journeys=journeys, + week_actual_str=minutes_to_str(week_actual), + week_balance=week_balance, + week_balance_str=minutes_to_str(abs(week_balance)), + month_km=month_km, + month_co2_kg=round(month_co2 / 1000, 2), + balance=balance, + used=used, + ) diff --git a/app/routes/entries.py b/app/routes/entries.py index 43e234f..4634041 100644 --- a/app/routes/entries.py +++ b/app/routes/entries.py @@ -1,3 +1,99 @@ -from flask import Blueprint +from flask import Blueprint, render_template, request, redirect, url_for, flash +from datetime import date, time +import sqlalchemy as sa +from app import db +from app.models import WorkEntry, TimeSlot +from app.config_loader import get_journeys, day_types_without_journey -bp = Blueprint("entries", __name__) +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(): + 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("//edit", methods=["GET", "POST"]) +def entry_form(entry_id=None): + 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 + 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 + + 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, + day_types_without_journey=day_types_without_journey(), + today=date.today().isoformat(), + ) + + +@bp.route("//delete", methods=["POST"]) +def delete_entry(entry_id): + 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")) diff --git a/app/routes/reports.py b/app/routes/reports.py index 3d64de4..dcbc0ce 100644 --- a/app/routes/reports.py +++ b/app/routes/reports.py @@ -1,3 +1,48 @@ -from flask import Blueprint +from flask import Blueprint, render_template, request +from datetime import date +import sqlalchemy as sa +from app import db +from app.models import WorkEntry +from app.business.travel_calc import compute_km_for_entry, compute_co2_grams, compute_frais_reels +from app.config_loader import get_vehicles, get_journeys, get_bareme -bp = Blueprint("reports", __name__) +bp = Blueprint("reports", __name__, url_prefix="/reports") + + +@bp.route("/") +def index(): + year = request.args.get("year", date.today().year, type=int) + start = date(year, 1, 1) + end = date(year, 12, 31) + + entries = db.session.scalars( + sa.select(WorkEntry).where(WorkEntry.date.between(start, end)) + ).all() + + vehicles = get_vehicles() + journeys = get_journeys() + + total_km = {} + total_co2 = 0.0 + for entry in entries: + km = compute_km_for_entry(entry.journey_profile_id, journeys) + for v, d in km.items(): + total_km[v] = total_km.get(v, 0) + d + total_co2 += compute_co2_grams(km, vehicles) + + frais_reels = {} + for vehicle_id, km in total_km.items(): + vehicle = vehicles.get(vehicle_id, {}) + cv = vehicle.get("cv") + if cv: + tranches = get_bareme(year, cv) + frais_reels[vehicle_id] = round(compute_frais_reels(km, tranches), 2) + + return render_template( + "reports.html", + year=year, + total_km=total_km, + total_co2_kg=round(total_co2 / 1000, 2), + frais_reels=frais_reels, + vehicles=vehicles, + ) diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..19037e5 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,30 @@ + + + + + + {% block title %}Tableau de bord{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endwith %} + {% block content %}{% endblock %} +
+ + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..63a0bfa --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} +{% block title %}Tableau de bord{% endblock %} +{% block content %} + +
+

+ Aujourd'hui — {{ today.strftime('%A %d %B %Y') }} +

+ {% if today_entry %} +

{{ today_entry.day_type }} — {{ today_entry.total_hours_str() }}

+ Modifier + {% else %} + + + Saisir ma journée + + {% endif %} +
+ +
+

Semaine courante

+
+ Total + {{ week_actual_str }} +
+
+ Écart vs 38h45 + + {{ '+' if week_balance >= 0 else '-' }}{{ week_balance_str }} + +
+
+ +
+

Ce mois

+ {% for vehicle_id, km in month_km.items() %} +
+ {{ vehicle_id | capitalize }} + {{ km }} km +
+ {% endfor %} +
+ CO₂ + {{ month_co2_kg }} kg +
+
+ +
+

Congés / RTT

+
+
+ Congés + {{ used.conges }} / {{ balance.conges_total }} j +
+
+
+
+
+
+
+ RTT + {{ used.rtt }} / {{ balance.rtt_total }} j +
+
+
+
+
+
+ +{% endblock %} diff --git a/app/templates/entry_form.html b/app/templates/entry_form.html new file mode 100644 index 0000000..c158a12 --- /dev/null +++ b/app/templates/entry_form.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} +{% block title %}{% if entry %}Modifier{% else %}Nouvelle entrée{% endif %}{% endblock %} +{% block content %} + +

+ {% if entry %}Modifier le {{ entry.date }}{% else %}Nouvelle journée{% endif %} +

+ +
+ +
+ + +
+ +
+ +
+ {% for value, label in day_types %} + + {% endfor %} +
+
+ +
+ + +
+ +
+ +
+ {% if entry and entry.time_slots %} + {% for slot in entry.time_slots %} +
+ + + + +
+ {% endfor %} + {% else %} +
+ + + + +
+ {% endif %} +
+ +
+ +
+ + +
+ +
+ + + Annuler + +
+
+ + +{% endblock %} diff --git a/app/templates/entry_list.html b/app/templates/entry_list.html new file mode 100644 index 0000000..86bd019 --- /dev/null +++ b/app/templates/entry_list.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}Historique{% endblock %} +{% block content %} + +

Historique

+ +{% if not entries %} +

Aucune entrée pour le moment.

+{% endif %} + +
+{% for entry in entries %} +
+
+

{{ entry.date.strftime('%a %d %b %Y') }}

+

+ {{ entry.day_type }} + {% if entry.time_slots %} — {{ entry.total_hours_str() }}{% endif %} + {% if entry.journey_profile_id %} — {{ entry.journey_profile_id }}{% endif %} +

+ {% if entry.comment %} +

{{ entry.comment }}

+ {% endif %} +
+
+ Éditer +
+ +
+
+
+{% endfor %} +
+ +{% endblock %} diff --git a/app/templates/reports.html b/app/templates/reports.html new file mode 100644 index 0000000..ab35973 --- /dev/null +++ b/app/templates/reports.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Rapports{% endblock %} +{% block content %} + +
+

Rapports {{ year }}

+
+ +
+
+ +
+

Kilométrage annuel

+ {% for vehicle_id, km in total_km.items() %} +
+ {{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }} + {{ km }} km +
+ {% endfor %} +
+ CO₂ total + {{ total_co2_kg }} kg +
+
+ +
+

Frais réels (barème {{ year }})

+

Déduction fiscale estimée — véhicules motorisés uniquement

+ {% for vehicle_id, montant in frais_reels.items() %} +
+ {{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }} + {{ montant }} € +
+ {% endfor %} + {% if not frais_reels %} +

Aucune donnée pour {{ year }}.

+ {% endif %} +
+ +{% endblock %}