feat: Flask routes and Jinja2/HTMX/Tailwind templates
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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("/<int:entry_id>/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("/<int:entry_id>/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"))
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
30
app/templates/base.html
Normal file
30
app/templates/base.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Tableau de bord{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
<nav class="bg-blue-700 text-white px-4 py-3 flex items-center justify-between">
|
||||
<a href="/" class="font-bold text-lg">⏱ Temps de travail</a>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<a href="/entries/new" class="hover:underline">+ Saisir</a>
|
||||
<a href="/entries/" class="hover:underline">Historique</a>
|
||||
<a href="/reports/" class="hover:underline">Rapports</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="max-w-2xl mx-auto px-4 py-6">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-3 rounded {% if category == 'error' %}bg-red-100 text-red-800{% else %}bg-green-100 text-green-800{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
73
app/templates/dashboard.html
Normal file
73
app/templates/dashboard.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Tableau de bord{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-2">
|
||||
Aujourd'hui — {{ today.strftime('%A %d %B %Y') }}
|
||||
</h2>
|
||||
{% if today_entry %}
|
||||
<p class="text-green-700 font-medium">{{ today_entry.day_type }} — {{ today_entry.total_hours_str() }}</p>
|
||||
<a href="/entries/{{ today_entry.id }}/edit"
|
||||
class="mt-2 inline-block text-sm text-blue-600 hover:underline">Modifier</a>
|
||||
{% else %}
|
||||
<a href="/entries/new"
|
||||
class="block w-full text-center bg-blue-600 text-white py-3 rounded-lg font-semibold text-lg hover:bg-blue-700 active:bg-blue-800 transition">
|
||||
+ Saisir ma journée
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-3">Semaine courante</h2>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Total</span>
|
||||
<span class="font-bold text-xl">{{ week_actual_str }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span class="text-gray-600">Écart vs 38h45</span>
|
||||
<span class="font-medium {% if week_balance >= 0 %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{{ '+' if week_balance >= 0 else '-' }}{{ week_balance_str }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-3">Ce mois</h2>
|
||||
{% for vehicle_id, km in month_km.items() %}
|
||||
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
||||
<span>{{ vehicle_id | capitalize }}</span>
|
||||
<span>{{ km }} km</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="flex justify-between text-sm text-gray-600 mt-2">
|
||||
<span>CO₂</span>
|
||||
<span>{{ month_co2_kg }} kg</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-3">Congés / RTT</h2>
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>Congés</span>
|
||||
<span>{{ used.conges }} / {{ balance.conges_total }} j</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-blue-500 h-2 rounded-full"
|
||||
style="width: {{ [[used.conges / balance.conges_total * 100, 100] | min, 0] | max }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>RTT</span>
|
||||
<span>{{ used.rtt }} / {{ balance.rtt_total }} j</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-purple-500 h-2 rounded-full"
|
||||
style="width: {{ [[used.rtt / balance.rtt_total * 100, 100] | min, 0] | max }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
122
app/templates/entry_form.html
Normal file
122
app/templates/entry_form.html
Normal file
@@ -0,0 +1,122 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if entry %}Modifier{% else %}Nouvelle entrée{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h1 class="text-xl font-bold mb-4">
|
||||
{% if entry %}Modifier le {{ entry.date }}{% else %}Nouvelle journée{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="POST" class="space-y-5">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
|
||||
<input type="date" name="date"
|
||||
value="{{ entry.date.isoformat() if entry else today }}"
|
||||
class="w-full border rounded-lg px-3 py-2 text-sm" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Type de journée</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{% for value, label in day_types %}
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="day_type" value="{{ value }}"
|
||||
{% if entry and entry.day_type == value %}checked{% elif not entry and value == 'WORK' %}checked{% endif %}
|
||||
class="sr-only peer"
|
||||
onchange="updateJourneyVisibility(this.value)">
|
||||
<div class="text-center text-sm py-2 px-1 rounded-lg border-2 border-gray-200
|
||||
peer-checked:border-blue-500 peer-checked:bg-blue-50 peer-checked:text-blue-700
|
||||
hover:border-gray-300 transition">
|
||||
{{ label }}
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="journey-section"
|
||||
class="{% if entry and entry.day_type in day_types_without_journey %}hidden{% endif %}">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Trajet domicile-travail</label>
|
||||
<select name="journey_profile_id"
|
||||
class="w-full border rounded-lg px-3 py-2 text-sm">
|
||||
<option value="">— Pas de déplacement —</option>
|
||||
{% for jid, jdata in journeys.items() %}
|
||||
<option value="{{ jid }}"
|
||||
{% if entry and entry.journey_profile_id == jid %}selected{% endif %}>
|
||||
{{ jdata.name }}
|
||||
({% for v, d in jdata.distances.items() %}{{ d }} km {{ v }}{% if not loop.last %} + {% endif %}{% endfor %})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Plages horaires</label>
|
||||
<div id="time-slots" class="space-y-2">
|
||||
{% if entry and entry.time_slots %}
|
||||
{% for slot in entry.time_slots %}
|
||||
<div class="flex gap-2 items-center time-slot-row">
|
||||
<input type="time" name="start_time" value="{{ slot.start_time.strftime('%H:%M') }}"
|
||||
class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<span class="text-gray-400">→</span>
|
||||
<input type="time" name="end_time" value="{{ slot.end_time.strftime('%H:%M') }}"
|
||||
class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="flex gap-2 items-center time-slot-row">
|
||||
<input type="time" name="start_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<span class="text-gray-400">→</span>
|
||||
<input type="time" name="end_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button" onclick="addTimeSlot()"
|
||||
class="mt-2 text-sm text-blue-600 hover:underline">+ Ajouter une plage</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire</label>
|
||||
<textarea name="comment" rows="2"
|
||||
class="w-full border rounded-lg px-3 py-2 text-sm"
|
||||
placeholder="Formation, déplacement exceptionnel...">{{ entry.comment if entry and entry.comment else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit"
|
||||
class="flex-1 bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition">
|
||||
Enregistrer
|
||||
</button>
|
||||
<a href="/" class="flex-1 text-center bg-gray-100 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-200 transition">
|
||||
Annuler
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const NO_JOURNEY_TYPES = {{ day_types_without_journey | list | tojson }};
|
||||
|
||||
function updateJourneyVisibility(dayType) {
|
||||
const section = document.getElementById('journey-section');
|
||||
section.classList.toggle('hidden', NO_JOURNEY_TYPES.includes(dayType));
|
||||
}
|
||||
|
||||
function addTimeSlot() {
|
||||
const container = document.getElementById('time-slots');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex gap-2 items-center time-slot-row';
|
||||
row.innerHTML = `
|
||||
<input type="time" name="start_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<span class="text-gray-400">→</span>
|
||||
<input type="time" name="end_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
36
app/templates/entry_list.html
Normal file
36
app/templates/entry_list.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Historique{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h1 class="text-xl font-bold mb-4">Historique</h1>
|
||||
|
||||
{% if not entries %}
|
||||
<p class="text-gray-500 text-center py-8">Aucune entrée pour le moment.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="space-y-2">
|
||||
{% for entry in entries %}
|
||||
<div class="bg-white rounded-xl shadow p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium text-sm">{{ entry.date.strftime('%a %d %b %Y') }}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ entry.day_type }}
|
||||
{% if entry.time_slots %} — {{ entry.total_hours_str() }}{% endif %}
|
||||
{% if entry.journey_profile_id %} — {{ entry.journey_profile_id }}{% endif %}
|
||||
</p>
|
||||
{% if entry.comment %}
|
||||
<p class="text-xs text-gray-400 mt-0.5 italic">{{ entry.comment }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/entries/{{ entry.id }}/edit" class="text-blue-500 text-sm hover:underline">Éditer</a>
|
||||
<form method="POST" action="/entries/{{ entry.id }}/delete"
|
||||
onsubmit="return confirm('Supprimer cette entrée ?')">
|
||||
<button class="text-red-400 text-sm hover:underline">Suppr.</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
45
app/templates/reports.html
Normal file
45
app/templates/reports.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Rapports{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-bold">Rapports {{ year }}</h1>
|
||||
<form method="GET">
|
||||
<select name="year" onchange="this.form.submit()"
|
||||
class="border rounded-lg px-2 py-1 text-sm">
|
||||
{% for y in range(2024, year + 2) %}
|
||||
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-3">Kilométrage annuel</h2>
|
||||
{% for vehicle_id, km in total_km.items() %}
|
||||
<div class="flex justify-between py-1 border-b border-gray-100 last:border-0">
|
||||
<span class="text-sm text-gray-600">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
||||
<span class="font-medium text-sm">{{ km }} km</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="flex justify-between pt-2 mt-1">
|
||||
<span class="text-sm text-gray-500">CO₂ total</span>
|
||||
<span class="font-medium text-sm">{{ total_co2_kg }} kg</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
||||
<h2 class="font-semibold text-gray-700 mb-1">Frais réels (barème {{ year }})</h2>
|
||||
<p class="text-xs text-gray-400 mb-3">Déduction fiscale estimée — véhicules motorisés uniquement</p>
|
||||
{% for vehicle_id, montant in frais_reels.items() %}
|
||||
<div class="flex justify-between py-1 border-b border-gray-100 last:border-0">
|
||||
<span class="text-sm text-gray-600">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
||||
<span class="font-bold text-green-700">{{ montant }} €</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not frais_reels %}
|
||||
<p class="text-sm text-gray-400">Aucune donnée pour {{ year }}.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user