design: refonte UI — journal de bord (Playfair + JetBrains Mono, palette encre/crème/ambre)
This commit is contained in:
@@ -3,23 +3,242 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Tableau de bord{% endblock %}</title>
|
<title>{% block title %}Journal de bord{% endblock %}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=JetBrains+Mono:wght@400;500;600&family=Lato:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink: #1A2332;
|
||||||
|
--cream: #F7F4EE;
|
||||||
|
--parchment: #EDE9DF;
|
||||||
|
--amber: #C8842A;
|
||||||
|
--amber-lt: #F0A94A;
|
||||||
|
--amber-pale:#FDF3E3;
|
||||||
|
--sage: #4A7A54;
|
||||||
|
--sage-pale: #EBF4ED;
|
||||||
|
--rust: #B85035;
|
||||||
|
--rust-pale: #FCEEE9;
|
||||||
|
--mist: #D5D0C8;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
background-color: var(--cream);
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: 100vh;
|
||||||
|
/* subtle dot grid */
|
||||||
|
background-image: radial-gradient(circle, rgba(180,170,155,0.35) 1px, transparent 1px);
|
||||||
|
background-size: 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-display { font-family: 'Playfair Display', Georgia, serif; }
|
||||||
|
.font-data { font-family: 'JetBrains Mono', monospace; }
|
||||||
|
|
||||||
|
/* ── Cards ─────────────────────────────── */
|
||||||
|
.card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1.1rem 1rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(26,35,50,0.07);
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
.card-amber { border-left-color: var(--amber); }
|
||||||
|
.card-sage { border-left-color: var(--sage); }
|
||||||
|
.card-ink { border-left-color: var(--ink); }
|
||||||
|
.card-rust { border-left-color: var(--rust); }
|
||||||
|
.card-mist { border-left-color: var(--mist); }
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8A8278;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Big stat number ───────────────────── */
|
||||||
|
.stat-number {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.stat-sub {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #8A8278;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Balance badge ─────────────────────── */
|
||||||
|
.balance-pos { color: var(--sage); background: var(--sage-pale); }
|
||||||
|
.balance-neg { color: var(--rust); background: var(--rust-pale); }
|
||||||
|
.balance-badge {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress bar ──────────────────────── */
|
||||||
|
.progress-track {
|
||||||
|
height: 4px;
|
||||||
|
background: var(--parchment);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Nav ───────────────────────────────── */
|
||||||
|
nav { background-color: var(--ink); }
|
||||||
|
.nav-link {
|
||||||
|
color: rgba(247,244,238,0.65);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-bottom: 1.5px solid transparent;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #F7F4EE;
|
||||||
|
border-bottom-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Inputs ────────────────────────────── */
|
||||||
|
.field-input {
|
||||||
|
width: 100%;
|
||||||
|
background: #FAFAF8;
|
||||||
|
border: 1px solid var(--mist);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.field-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--amber);
|
||||||
|
box-shadow: 0 0 0 3px rgba(200,132,42,0.12);
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8A8278;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons ───────────────────────────── */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--ink);
|
||||||
|
color: var(--cream);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s, transform 0.1s;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background-color: #243147; }
|
||||||
|
.btn-primary:active { transform: scale(0.99); }
|
||||||
|
|
||||||
|
.btn-amber {
|
||||||
|
background-color: var(--amber);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-amber:hover { background-color: var(--amber-lt); }
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #8A8278;
|
||||||
|
border: 1px solid var(--mist);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-ghost:hover { background-color: var(--parchment); }
|
||||||
|
|
||||||
|
/* ── Pill radio (type journée) ─────────── */
|
||||||
|
.pill-option { display: block; cursor: pointer; }
|
||||||
|
.pill-option input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||||
|
.pill-inner {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.45rem 0.25rem;
|
||||||
|
border: 1.5px solid var(--mist);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #8A8278;
|
||||||
|
background: #FAFAF8;
|
||||||
|
transition: all 0.12s;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.pill-option input:checked + .pill-inner {
|
||||||
|
border-color: var(--ink);
|
||||||
|
background-color: var(--ink);
|
||||||
|
color: var(--cream);
|
||||||
|
}
|
||||||
|
.pill-inner:hover { border-color: #AAA49C; color: var(--ink); }
|
||||||
|
|
||||||
|
/* ── Divider line ──────────────────────── */
|
||||||
|
.ruled { border-color: var(--parchment); }
|
||||||
|
|
||||||
|
/* ── Day type dot (entry list) ─────────── */
|
||||||
|
.dt-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 min-h-screen">
|
<body>
|
||||||
<nav class="bg-blue-700 text-white px-4 py-3 flex items-center justify-between">
|
<nav class="px-4 py-3 flex items-center justify-between sticky top-0 z-10">
|
||||||
<a href="/" class="font-bold text-lg">⏱ Temps de travail</a>
|
<a href="/" class="font-display text-white text-base font-semibold" style="letter-spacing:0.015em;">
|
||||||
<div class="flex gap-4 text-sm">
|
Journal de bord
|
||||||
<a href="/entries/new" class="hover:underline">+ Saisir</a>
|
</a>
|
||||||
<a href="/entries/" class="hover:underline">Historique</a>
|
<div class="flex gap-5">
|
||||||
<a href="/reports/" class="hover:underline">Rapports</a>
|
<a href="/entries/new" class="nav-link">Saisir</a>
|
||||||
|
<a href="/entries/" class="nav-link">Historique</a>
|
||||||
|
<a href="/reports/" class="nav-link">Rapports</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="max-w-2xl mx-auto px-4 py-6">
|
|
||||||
|
<main class="max-w-xl mx-auto px-4 py-5">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% for category, message in messages %}
|
{% 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 %}">
|
<div class="mb-4 px-4 py-2.5 text-sm rounded-sm"
|
||||||
|
style="{% if category == 'error' %}border-left:3px solid var(--rust);background:var(--rust-pale);color:var(--rust){% else %}border-left:3px solid var(--sage);background:var(--sage-pale);color:var(--sage){% endif %}">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,71 +1,97 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Tableau de bord{% endblock %}
|
{% block title %}Journal de bord{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
<!-- En-tête date -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-2">
|
<div class="mb-5">
|
||||||
Aujourd'hui — {{ today | date_fr }}
|
<p class="font-display text-2xl font-semibold" style="color:var(--ink); letter-spacing:-0.01em;">
|
||||||
</h2>
|
{{ today | date_fr }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte aujourd'hui -->
|
||||||
|
<div class="card card-amber">
|
||||||
|
<p class="card-label">Aujourd'hui</p>
|
||||||
{% if today_entry %}
|
{% if today_entry %}
|
||||||
<p class="text-green-700 font-medium">{{ today_entry.day_type }} — {{ today_entry.total_hours_str() }}</p>
|
<div class="flex items-baseline gap-3">
|
||||||
<a href="/entries/{{ today_entry.id }}/edit"
|
<span class="stat-number">{{ today_entry.total_hours_str() }}</span>
|
||||||
class="mt-2 inline-block text-sm text-blue-600 hover:underline">Modifier</a>
|
<span class="stat-sub">travaillées</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between mt-3">
|
||||||
|
<span class="text-xs font-semibold px-2 py-0.5 rounded" style="background:var(--parchment); color:#6A6258; letter-spacing:0.06em;">
|
||||||
|
{{ today_entry.day_type }}{% if today_entry.motor_vehicle_id %} · {{ today_entry.motor_vehicle_id }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<a href="/entries/{{ today_entry.id }}/edit"
|
||||||
|
class="text-xs font-semibold" style="color:var(--amber);">Modifier →</a>
|
||||||
|
</div>
|
||||||
|
{% if today_entry.comment %}
|
||||||
|
<p class="mt-2 text-xs italic" style="color:#9A9288;">{{ today_entry.comment }}</p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/entries/new"
|
<a href="/entries/new" class="btn-primary btn-amber mt-1" style="letter-spacing:0.06em;">
|
||||||
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
|
+ Saisir ma journée
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
<!-- Carte semaine -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-3">Semaine courante</h2>
|
<div class="card card-ink">
|
||||||
<div class="flex justify-between items-center">
|
<p class="card-label">Semaine en cours</p>
|
||||||
<span class="text-gray-600">Total</span>
|
<div class="flex items-end justify-between">
|
||||||
<span class="font-bold text-xl">{{ week_actual_str }}</span>
|
<div>
|
||||||
</div>
|
<span class="stat-number">{{ week_actual_str }}</span>
|
||||||
<div class="flex justify-between items-center mt-1">
|
<p class="stat-sub mt-1">objectif 38h45</p>
|
||||||
<span class="text-gray-600">Écart vs 38h45</span>
|
</div>
|
||||||
<span class="font-medium {% if week_balance >= 0 %}text-green-600{% else %}text-red-600{% endif %}">
|
<span class="balance-badge {% if week_balance >= 0 %}balance-pos{% else %}balance-neg{% endif %}">
|
||||||
{{ '+' if week_balance >= 0 else '-' }}{{ week_balance_str }}
|
{{ '+' if week_balance >= 0 else '−' }}{{ week_balance_str }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
<!-- Carte mois -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-3">Ce mois</h2>
|
{% if month_km %}
|
||||||
{% for vehicle_id, km in month_km.items() %}
|
<div class="card card-mist">
|
||||||
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
<p class="card-label">Ce mois — déplacements</p>
|
||||||
<span>{{ vehicle_id | capitalize }}</span>
|
<div class="space-y-2 mt-1">
|
||||||
<span>{{ km }} km</span>
|
{% for vehicle_id, km in month_km.items() %}
|
||||||
</div>
|
<div class="flex items-baseline justify-between">
|
||||||
{% endfor %}
|
<span class="text-xs" style="color:#8A8278;">{{ vehicle_id | capitalize }}</span>
|
||||||
<div class="flex justify-between text-sm text-gray-600 mt-2">
|
<span class="font-data font-semibold text-sm" style="color:var(--ink);">{{ km }} <span class="font-normal text-xs" style="color:#9A9288;">km</span></span>
|
||||||
<span>CO₂</span>
|
</div>
|
||||||
<span>{{ month_co2_kg }} kg</span>
|
{% endfor %}
|
||||||
|
<div class="flex items-baseline justify-between pt-1" style="border-top:1px solid var(--parchment);">
|
||||||
|
<span class="text-xs" style="color:#8A8278;">CO₂</span>
|
||||||
|
<span class="font-data font-semibold text-sm" style="color:var(--ink);">{{ month_co2_kg }} <span class="font-normal text-xs" style="color:#9A9288;">kg</span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4">
|
<!-- Carte congés / RTT -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-3">Congés / RTT</h2>
|
<div class="card card-sage">
|
||||||
<div class="mb-3">
|
<p class="card-label">Solde congés & RTT</p>
|
||||||
<div class="flex justify-between text-sm mb-1">
|
<div class="space-y-3 mt-1">
|
||||||
<span>Congés</span>
|
<div>
|
||||||
<span>{{ used.conges }} / {{ balance.conges_total }} j</span>
|
<div class="flex justify-between items-baseline">
|
||||||
|
<span class="text-xs font-semibold" style="color:#8A8278; letter-spacing:0.06em;">CONGÉS</span>
|
||||||
|
<span class="font-data text-sm font-semibold" style="color:var(--ink);">
|
||||||
|
{{ used.conges }}<span class="font-normal text-xs" style="color:#9A9288;"> / {{ balance.conges_total }} j</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-fill" style="width:{{ [[used.conges / balance.conges_total * 100, 100] | min, 0] | max }}%; background:var(--sage);"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
<div>
|
||||||
<div class="bg-blue-500 h-2 rounded-full"
|
<div class="flex justify-between items-baseline">
|
||||||
style="width: {{ [[used.conges / balance.conges_total * 100, 100] | min, 0] | max }}%"></div>
|
<span class="text-xs font-semibold" style="color:#8A8278; letter-spacing:0.06em;">RTT</span>
|
||||||
</div>
|
<span class="font-data text-sm font-semibold" style="color:var(--ink);">
|
||||||
</div>
|
{{ used.rtt }}<span class="font-normal text-xs" style="color:#9A9288;"> / {{ balance.rtt_total }} j</span>
|
||||||
<div>
|
</span>
|
||||||
<div class="flex justify-between text-sm mb-1">
|
</div>
|
||||||
<span>RTT</span>
|
<div class="progress-track">
|
||||||
<span>{{ used.rtt }} / {{ balance.rtt_total }} j</span>
|
<div class="progress-fill" style="width:{{ [[used.rtt / balance.rtt_total * 100, 100] | min, 0] | max }}%; background:var(--amber);"></div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{% if entry %}Modifier{% else %}Nouvelle entrée{% endif %}{% endblock %}
|
{% block title %}{% if entry %}Modifier l'entrée{% else %}Nouvelle journée{% endif %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1 class="text-xl font-bold mb-4">
|
<div class="mb-5">
|
||||||
{% if entry %}Modifier le {{ entry.date }}{% else %}Nouvelle journée{% endif %}
|
<p class="font-display text-2xl font-semibold" style="color:var(--ink);">
|
||||||
</h1>
|
{% if entry %}Modifier{% else %}Nouvelle journée{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="POST" class="space-y-5">
|
<form method="POST" class="space-y-5">
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
|
<label class="field-label">Date</label>
|
||||||
<input type="date" name="date"
|
<input type="date" name="date"
|
||||||
value="{{ entry.date.isoformat() if entry else today }}"
|
value="{{ entry.date.isoformat() if entry else today }}"
|
||||||
class="w-full border rounded-lg px-3 py-2 text-sm" required>
|
class="field-input" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Type de journée -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Type de journée</label>
|
<label class="field-label">Type de journée</label>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
{% for value, label in day_types %}
|
{% for value, label in day_types %}
|
||||||
<label class="cursor-pointer">
|
<label class="pill-option">
|
||||||
<input type="radio" name="day_type" value="{{ value }}"
|
<input type="radio" name="day_type" value="{{ value }}"
|
||||||
{% if entry and entry.day_type == value %}checked{% elif not entry and value == 'WORK' %}checked{% endif %}
|
{% if entry and entry.day_type == value %}checked{% elif not entry and value == 'WORK' %}checked{% endif %}
|
||||||
class="sr-only peer"
|
|
||||||
onchange="updateJourneyVisibility(this.value)">
|
onchange="updateJourneyVisibility(this.value)">
|
||||||
<div class="text-center text-sm py-2 px-1 rounded-lg border-2 border-gray-200
|
<span class="pill-inner">{{ label }}</span>
|
||||||
peer-checked:border-blue-500 peer-checked:bg-blue-50 peer-checked:text-blue-700
|
|
||||||
hover:border-gray-300 transition">
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Trajet domicile-travail -->
|
||||||
<div id="journey-section"
|
<div id="journey-section"
|
||||||
class="{% if entry and entry.day_type in day_types_without_journey %}hidden{% endif %}">
|
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>
|
<label class="field-label">Trajet domicile-travail</label>
|
||||||
<select name="journey_profile_id" id="journey_profile_id"
|
<select name="journey_profile_id" id="journey_profile_id"
|
||||||
onchange="updateMotorVehicleVisibility(this.value)"
|
onchange="updateMotorVehicleVisibility(this.value)"
|
||||||
class="w-full border rounded-lg px-3 py-2 text-sm">
|
class="field-input">
|
||||||
<option value="">— Pas de déplacement —</option>
|
<option value="">— Pas de déplacement —</option>
|
||||||
{% for jid, jdata in journeys.items() %}
|
{% for jid, jdata in journeys.items() %}
|
||||||
<option value="{{ jid }}"
|
<option value="{{ jid }}"
|
||||||
@@ -52,68 +52,67 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Véhicule à moteur -->
|
||||||
<div id="motor-vehicle-section" class="{% if not entry or not entry.motor_vehicle_id %}hidden{% endif %}">
|
<div id="motor-vehicle-section" class="{% if not entry or not entry.motor_vehicle_id %}hidden{% endif %}">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Véhicule à moteur utilisé</label>
|
<label class="field-label">Véhicule à moteur utilisé</label>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
{% for vid, vdata in motor_vehicles.items() %}
|
{% for vid, vdata in motor_vehicles.items() %}
|
||||||
<label class="cursor-pointer">
|
<label class="pill-option">
|
||||||
<input type="radio" name="motor_vehicle_id" value="{{ vid }}"
|
<input type="radio" name="motor_vehicle_id" value="{{ vid }}"
|
||||||
{% if entry and entry.motor_vehicle_id == vid %}checked{% endif %}
|
{% if entry and entry.motor_vehicle_id == vid %}checked{% endif %}>
|
||||||
class="sr-only peer">
|
<span class="pill-inner" style="font-size:0.72rem;">{{ vdata.name }}</span>
|
||||||
<div class="text-center text-sm py-2 px-1 rounded-lg border-2 border-gray-200
|
|
||||||
peer-checked:border-orange-500 peer-checked:bg-orange-50 peer-checked:text-orange-700
|
|
||||||
hover:border-gray-300 transition">
|
|
||||||
{{ vdata.name }}
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Plages horaires -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Plages horaires</label>
|
<label class="field-label">Plages horaires</label>
|
||||||
<div id="time-slots" class="space-y-2">
|
<div id="time-slots" class="space-y-2">
|
||||||
{% if entry and entry.time_slots %}
|
{% if entry and entry.time_slots %}
|
||||||
{% for slot in entry.time_slots %}
|
{% for slot in entry.time_slots %}
|
||||||
<div class="flex gap-2 items-center time-slot-row">
|
<div class="flex gap-2 items-center time-slot-row">
|
||||||
<input type="time" name="start_time" value="{{ slot.start_time.strftime('%H:%M') }}"
|
<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">
|
class="field-input flex-1" style="padding:0.5rem 0.6rem; font-family:'JetBrains Mono',monospace; font-size:0.9rem;">
|
||||||
<span class="text-gray-400">→</span>
|
<span style="color:var(--mist); font-size:1rem;">→</span>
|
||||||
<input type="time" name="end_time" value="{{ slot.end_time.strftime('%H:%M') }}"
|
<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">
|
class="field-input flex-1" style="padding:0.5rem 0.6rem; font-family:'JetBrains Mono',monospace; font-size:0.9rem;">
|
||||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
class="text-lg leading-none transition-colors" style="color:var(--mist);"
|
||||||
|
onmouseover="this.style.color='var(--rust)'" onmouseout="this.style.color='var(--mist)'">×</button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex gap-2 items-center time-slot-row">
|
<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">
|
<input type="time" name="start_time"
|
||||||
<span class="text-gray-400">→</span>
|
class="field-input flex-1" style="padding:0.5rem 0.6rem; font-family:'JetBrains Mono',monospace; font-size:0.9rem;">
|
||||||
<input type="time" name="end_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
<span style="color:var(--mist); font-size:1rem;">→</span>
|
||||||
|
<input type="time" name="end_time"
|
||||||
|
class="field-input flex-1" style="padding:0.5rem 0.6rem; font-family:'JetBrains Mono',monospace; font-size:0.9rem;">
|
||||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
class="text-lg leading-none" style="color:var(--mist);"
|
||||||
|
onmouseover="this.style.color='var(--rust)'" onmouseout="this.style.color='var(--mist)'">×</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onclick="addTimeSlot()"
|
<button type="button" onclick="addTimeSlot()"
|
||||||
class="mt-2 text-sm text-blue-600 hover:underline">+ Ajouter une plage</button>
|
class="mt-2 text-xs font-semibold" style="color:var(--amber); letter-spacing:0.05em;">
|
||||||
</div>
|
+ Ajouter une plage
|
||||||
|
|
||||||
<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>
|
</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">
|
</div>
|
||||||
Annuler
|
|
||||||
</a>
|
<!-- Commentaire -->
|
||||||
|
<div>
|
||||||
|
<label class="field-label">Commentaire</label>
|
||||||
|
<textarea name="comment" rows="2" class="field-input"
|
||||||
|
placeholder="Formation, déplacement exceptionnel…">{{ entry.comment if entry and entry.comment else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex gap-3 pt-1">
|
||||||
|
<button type="submit" class="btn-primary flex-1">Enregistrer</button>
|
||||||
|
<a href="/" class="btn-ghost flex-1">Annuler</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -145,11 +144,14 @@ function addTimeSlot() {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'flex gap-2 items-center time-slot-row';
|
row.className = 'flex gap-2 items-center time-slot-row';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<input type="time" name="start_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
<input type="time" name="start_time"
|
||||||
<span class="text-gray-400">→</span>
|
class="field-input flex-1" style="padding:0.5rem 0.6rem;font-family:'JetBrains Mono',monospace;font-size:0.9rem;">
|
||||||
<input type="time" name="end_time" class="flex-1 border rounded-lg px-3 py-2 text-sm">
|
<span style="color:var(--mist);font-size:1rem;">→</span>
|
||||||
|
<input type="time" name="end_time"
|
||||||
|
class="field-input flex-1" style="padding:0.5rem 0.6rem;font-family:'JetBrains Mono',monospace;font-size:0.9rem;">
|
||||||
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
<button type="button" onclick="this.closest('.time-slot-row').remove()"
|
||||||
class="text-red-400 hover:text-red-600 text-xl leading-none">×</button>
|
class="text-lg leading-none" style="color:var(--mist);"
|
||||||
|
onmouseover="this.style.color='var(--rust)'" onmouseout="this.style.color='var(--mist)'">×</button>
|
||||||
`;
|
`;
|
||||||
container.appendChild(row);
|
container.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,57 @@
|
|||||||
{% block title %}Historique{% endblock %}
|
{% block title %}Historique{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1 class="text-xl font-bold mb-4">Historique</h1>
|
<div class="flex items-baseline justify-between mb-5">
|
||||||
|
<p class="font-display text-2xl font-semibold" style="color:var(--ink);">Historique</p>
|
||||||
|
<a href="/entries/new" class="text-xs font-semibold" style="color:var(--amber); letter-spacing:0.05em;">+ Saisir</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if not entries %}
|
{% if not entries %}
|
||||||
<p class="text-gray-500 text-center py-8">Aucune entrée pour le moment.</p>
|
<div class="card card-mist text-center py-8">
|
||||||
|
<p style="color:#9A9288; font-size:0.875rem;">Aucune entrée pour le moment.</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div class="bg-white rounded-xl shadow p-3 flex items-center justify-between">
|
|
||||||
<div>
|
{% set dot_color =
|
||||||
<p class="font-medium text-sm">{{ entry.date | date_fr }}</p>
|
'#4A7A54' if entry.day_type == 'WORK' else
|
||||||
<p class="text-xs text-gray-500">
|
'#3A6AAA' if entry.day_type == 'TT' else
|
||||||
{{ entry.day_type }}
|
'#C8842A' if entry.day_type in ('GARDE', 'ASTREINTE') else
|
||||||
{% if entry.time_slots %} — {{ entry.total_hours_str() }}{% endif %}
|
'#7A4AAA' if entry.day_type in ('RTT', 'FORMATION') else
|
||||||
{% if entry.journey_profile_id %} — {{ entry.journey_profile_id }}{% endif %}
|
'#B85035' if entry.day_type == 'MALADE' else
|
||||||
</p>
|
'#8A8278'
|
||||||
{% if entry.comment %}
|
%}
|
||||||
<p class="text-xs text-gray-400 mt-0.5 italic">{{ entry.comment }}</p>
|
|
||||||
{% endif %}
|
<div class="card" style="border-left-color: {{ dot_color }}; padding: 0.75rem 1rem;">
|
||||||
</div>
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex-1 min-w-0">
|
||||||
<a href="/entries/{{ entry.id }}/edit" class="text-blue-500 text-sm hover:underline">Éditer</a>
|
<p class="font-semibold text-sm" style="color:var(--ink);">{{ entry.date | date_fr }}</p>
|
||||||
<form method="POST" action="/entries/{{ entry.id }}/delete"
|
<div class="flex items-center gap-2 mt-0.5">
|
||||||
onsubmit="return confirm('Supprimer cette entrée ?')">
|
<span class="text-xs font-semibold" style="color:{{ dot_color }}; letter-spacing:0.06em;">{{ entry.day_type }}</span>
|
||||||
<button class="text-red-400 text-sm hover:underline">Suppr.</button>
|
{% if entry.time_slots %}
|
||||||
</form>
|
<span class="font-data text-xs" style="color:#8A8278;">{{ entry.total_hours_str() }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.motor_vehicle_id %}
|
||||||
|
<span class="text-xs" style="color:#9A9288;">· {{ entry.motor_vehicle_id }}</span>
|
||||||
|
{% elif entry.journey_profile_id %}
|
||||||
|
<span class="text-xs" style="color:#9A9288;">· {{ entry.journey_profile_id }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if entry.comment %}
|
||||||
|
<p class="text-xs italic mt-1 truncate" style="color:#9A9288;">{{ entry.comment }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 flex-shrink-0">
|
||||||
|
<a href="/entries/{{ entry.id }}/edit"
|
||||||
|
class="text-xs font-semibold" style="color:var(--amber);">Éditer</a>
|
||||||
|
<form method="POST" action="/entries/{{ entry.id }}/delete"
|
||||||
|
onsubmit="return confirm('Supprimer cette entrée ?')">
|
||||||
|
<button class="text-xs" style="color:var(--mist);"
|
||||||
|
onmouseover="this.style.color='var(--rust)'" onmouseout="this.style.color='var(--mist)'">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Rapports{% endblock %}
|
{% block title %}Rapports {{ year }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- En-tête -->
|
||||||
<h1 class="text-xl font-bold">Rapports {{ year }}</h1>
|
<div class="flex items-baseline justify-between mb-5">
|
||||||
|
<p class="font-display text-2xl font-semibold" style="color:var(--ink);">Rapports</p>
|
||||||
<form method="GET">
|
<form method="GET">
|
||||||
<select name="year" onchange="this.form.submit()"
|
<select name="year" onchange="this.form.submit()" class="field-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;">
|
||||||
class="border rounded-lg px-2 py-1 text-sm">
|
|
||||||
{% for y in range(2024, year + 2) %}
|
{% for y in range(2024, year + 2) %}
|
||||||
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
|
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -14,31 +14,48 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
<!-- Kilométrage -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-3">Kilométrage annuel</h2>
|
<div class="card card-ink">
|
||||||
{% for vehicle_id, km in total_km.items() %}
|
<p class="card-label">Kilométrage {{ year }}</p>
|
||||||
<div class="flex justify-between py-1 border-b border-gray-100 last:border-0">
|
{% if total_km %}
|
||||||
<span class="text-sm text-gray-600">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
<div class="space-y-3 mt-1">
|
||||||
<span class="font-medium text-sm">{{ km }} km</span>
|
{% for vehicle_id, km in total_km.items() %}
|
||||||
</div>
|
<div class="flex items-baseline justify-between">
|
||||||
{% endfor %}
|
<span class="text-xs" style="color:#8A8278;">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
||||||
<div class="flex justify-between pt-2 mt-1">
|
<span class="font-data font-semibold" style="font-size:1.35rem; color:var(--ink);">
|
||||||
<span class="text-sm text-gray-500">CO₂ total</span>
|
{{ km }}<span class="font-normal text-xs ml-1" style="color:#9A9288;">km</span>
|
||||||
<span class="font-medium text-sm">{{ total_co2_kg }} kg</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="flex items-baseline justify-between pt-2" style="border-top:1px solid var(--parchment);">
|
||||||
|
<span class="text-xs font-semibold" style="color:#8A8278; letter-spacing:0.06em;">CO₂ TOTAL</span>
|
||||||
|
<span class="font-data font-semibold" style="font-size:1.35rem; color:var(--ink);">
|
||||||
|
{{ total_co2_kg }}<span class="font-normal text-xs ml-1" style="color:#9A9288;">kg</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="mt-1 text-sm" style="color:#9A9288;">Aucun déplacement enregistré pour {{ year }}.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow p-4 mb-4">
|
<!-- Frais réels -->
|
||||||
<h2 class="font-semibold text-gray-700 mb-1">Frais réels (barème {{ year }})</h2>
|
<div class="card card-sage">
|
||||||
<p class="text-xs text-gray-400 mb-3">Déduction fiscale estimée — véhicules motorisés uniquement</p>
|
<p class="card-label">Frais réels — barème {{ year }}</p>
|
||||||
{% for vehicle_id, montant in frais_reels.items() %}
|
<p class="text-xs mb-3" style="color:#9A9288;">Déduction fiscale estimée · véhicules motorisés uniquement</p>
|
||||||
<div class="flex justify-between py-1 border-b border-gray-100 last:border-0">
|
{% if frais_reels %}
|
||||||
<span class="text-sm text-gray-600">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
<div class="space-y-3">
|
||||||
<span class="font-bold text-green-700">{{ montant }} €</span>
|
{% for vehicle_id, montant in frais_reels.items() %}
|
||||||
</div>
|
<div class="flex items-baseline justify-between">
|
||||||
{% endfor %}
|
<span class="text-xs" style="color:#8A8278;">{{ vehicles.get(vehicle_id, {}).get('name', vehicle_id) }}</span>
|
||||||
{% if not frais_reels %}
|
<span class="font-data font-bold" style="font-size:1.6rem; color:var(--sage);">
|
||||||
<p class="text-sm text-gray-400">Aucune donnée pour {{ year }}.</p>
|
{{ montant }}<span class="font-normal text-sm ml-1" style="color:#9A9288;">€</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm" style="color:#9A9288;">Aucune donnée pour {{ year }}.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sqlalchemy as sa
|
|||||||
def test_dashboard_empty(client):
|
def test_dashboard_empty(client):
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Tableau de bord" in response.text
|
assert "Journal de bord" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_entry_form_get(client):
|
def test_entry_form_get(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user