feat: GarageManager v2.1 - véhicules, interventions, pièces

- Application FastAPI avec SQLite et Jinja2
- Gestion des véhicules (VIN, immatriculation, infos libres)
- Gestion des interventions avec statut (en_attente/en_cours/termine)
- Gestion des pièces détachées avec photos
- Tableau de bord avec statistiques
- Déploiement Docker + Traefik (HTTPS)
- README complet en français
This commit is contained in:
2026-03-15 12:40:50 +01:00
commit 97ae64caf0
18 changed files with 1304 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GarageManager{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head>
<body class="bg-gray-100 min-h-screen flex">
<div class="w-64 bg-slate-800 min-h-screen flex flex-col fixed left-0 top-0">
<div class="p-6 border-b border-slate-700">
<h1 class="text-white text-xl font-bold flex items-center gap-2">
<i class="fas fa-wrench text-orange-400"></i> GarageManager
</h1>
</div>
<nav class="flex-1 p-4 space-y-1">
<a href="/" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'dashboard' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<i class="fas fa-chart-line w-5"></i> Dashboard
</a>
<a href="/vehicules" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'vehicules' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<i class="fas fa-car w-5"></i> Véhicules
</a>
<a href="/interventions" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'interventions' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<i class="fas fa-tools w-5"></i> Interventions
</a>
</nav>
<div class="p-4 border-t border-slate-700 text-xs text-slate-500 text-center">GarageManager v2.1</div>
</div>
<div class="ml-64 flex-1 p-8">
{% block content %}{% endblock %}
</div>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block title %}{{ client.nom }} {{ client.prenom }} - GarageManager{% endblock %}
{% block content %}
<div class="mb-6">
<a href="/clients" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
<div class="flex justify-between items-start">
<div>
<h2 class="text-2xl font-bold text-slate-800">{{ client.nom }} {{ client.prenom }}</h2>
<div class="flex flex-wrap gap-4 mt-2 text-sm text-slate-500">
{% if client.telephone %}<span><i class="fas fa-phone mr-1"></i>{{ client.telephone }}</span>{% endif %}
{% if client.email %}<span><i class="fas fa-envelope mr-1"></i>{{ client.email }}</span>{% endif %}
{% if client.adresse %}<span><i class="fas fa-map-marker-alt mr-1"></i>{{ client.adresse }}</span>{% endif %}
</div>
</div>
<div class="flex gap-2">
<a href="/clients/{{ client.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
<form method="post" action="/clients/{{ client.id }}/delete" onsubmit="return confirm('Supprimer ce client et tous ses véhicules ?')">
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
</form>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow">
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-800">Véhicules ({{ vehicules|length }})</h3>
<a href="/vehicules/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Ajouter un véhicule</a>
</div>
{% if vehicules %}
<div class="divide-y divide-gray-100">
{% for v in vehicules %}
<div class="px-6 py-4 flex justify-between items-center hover:bg-gray-50">
<div>
<span class="font-medium text-slate-800">{{ v.immatriculation }}</span>
<span class="text-slate-500 ml-2">{{ v.marque }} {{ v.modele }}</span>
{% if v.annee %}<span class="text-slate-400 text-sm ml-2">({{ v.annee }})</span>{% endif %}
{% if v.kilometrage %}<span class="text-slate-400 text-sm ml-2">— {{ v.kilometrage }} km</span>{% endif %}
</div>
<div class="flex items-center gap-3">
<span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">{{ v.nb_interventions }} intervention(s)</span>
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline text-sm">Voir →</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-8 text-center text-slate-400 text-sm">Aucun véhicule enregistré pour ce client.</div>
{% endif %}
</div>
{% endblock %}
+38
View File
@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}{% if client %}Modifier{% else %}Nouveau{% endif %} client - GarageManager{% endblock %}
{% block content %}
<div class="mb-8">
<a href="/clients" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
<h2 class="text-2xl font-bold text-slate-800">{% if client %}Modifier le client{% else %}Nouveau client{% endif %}</h2>
</div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Nom *</label>
<input name="nom" value="{{ client.nom if client else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Prénom *</label>
<input name="prenom" value="{{ client.prenom if client else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Téléphone</label>
<input name="telephone" value="{{ client.telephone if client else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Email</label>
<input name="email" type="email" value="{{ client.email if client else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Adresse</label>
<textarea name="adresse" rows="2" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ client.adresse if client else '' }}</textarea>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
<a href="/clients" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
</div>
</form>
</div>
{% endblock %}
+54
View File
@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}Clients - GarageManager{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-8">
<div><h2 class="text-2xl font-bold text-slate-800">Clients</h2><p class="text-slate-500 mt-1">{{ clients|length }} client(s)</p></div>
<a href="/clients/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition flex items-center gap-2"><i class="fas fa-plus"></i> Nouveau client</a>
</div>
<div class="bg-white rounded-xl shadow">
<div class="p-4 border-b border-gray-100">
<form method="get" class="flex gap-2">
<input name="q" value="{{ q }}" placeholder="Rechercher par nom, téléphone..." class="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
<button type="submit" class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm hover:bg-slate-800">Rechercher</button>
{% if q %}<a href="/clients" class="text-slate-500 px-3 py-2 text-sm hover:text-slate-700 flex items-center"></a>{% endif %}
</form>
</div>
<div class="overflow-x-auto">
{% if clients %}
<table class="w-full text-sm">
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
<tr>
<th class="px-6 py-3 text-left">Nom</th>
<th class="px-6 py-3 text-left">Téléphone</th>
<th class="px-6 py-3 text-left">Email</th>
<th class="px-6 py-3 text-left">Véhicules</th>
<th class="px-6 py-3 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for c in clients %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4"><a href="/clients/{{ c.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ c.nom }} {{ c.prenom }}</a></td>
<td class="px-6 py-4 text-slate-600">{{ c.telephone or '-' }}</td>
<td class="px-6 py-4 text-slate-600">{{ c.email or '-' }}</td>
<td class="px-6 py-4"><span class="bg-purple-100 text-purple-700 px-2 py-1 rounded-full text-xs">{{ c.nb_vehicules }}</span></td>
<td class="px-6 py-4 flex gap-3">
<a href="/clients/{{ c.id }}" class="text-blue-500 hover:underline">Voir</a>
<a href="/clients/{{ c.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
<form method="post" action="/clients/{{ c.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce client ?')">
<button type="submit" class="text-red-500 hover:underline">Supprimer</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-8 text-center text-slate-400">
<i class="fas fa-users text-3xl mb-3"></i><p>Aucun client trouvé</p>
<a href="/clients/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}Dashboard - GarageManager{% endblock %}
{% block content %}
<div class="mb-8">
<h2 class="text-2xl font-bold text-slate-800">Dashboard</h2>
<p class="text-slate-500 mt-1">Vue d'ensemble de votre garage</p>
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">Véhicules</p><p class="text-3xl font-bold text-slate-800 mt-1">{{ stats.nb_vehicules }}</p></div>
<div class="bg-purple-100 p-3 rounded-full"><i class="fas fa-car text-purple-500 text-xl"></i></div>
</div>
<a href="/vehicules" class="text-xs text-purple-500 hover:underline mt-3 inline-block">Voir tous →</a>
</div>
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">En attente</p><p class="text-3xl font-bold text-yellow-500 mt-1">{{ stats.nb_en_attente }}</p></div>
<div class="bg-yellow-100 p-3 rounded-full"><i class="fas fa-clock text-yellow-500 text-xl"></i></div>
</div>
<a href="/interventions?statut=en_attente" class="text-xs text-yellow-500 hover:underline mt-3 inline-block">Voir →</a>
</div>
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">En cours</p><p class="text-3xl font-bold text-blue-500 mt-1">{{ stats.nb_en_cours }}</p></div>
<div class="bg-blue-100 p-3 rounded-full"><i class="fas fa-spinner text-blue-500 text-xl"></i></div>
</div>
<a href="/interventions?statut=en_cours" class="text-xs text-blue-500 hover:underline mt-3 inline-block">Voir →</a>
</div>
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">Terminées</p><p class="text-3xl font-bold text-green-500 mt-1">{{ stats.nb_termine }}</p></div>
<div class="bg-green-100 p-3 rounded-full"><i class="fas fa-check-circle text-green-500 text-xl"></i></div>
</div>
<a href="/interventions?statut=termine" class="text-xs text-green-500 hover:underline mt-3 inline-block">Voir →</a>
</div>
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">Coût pièces</p><p class="text-3xl font-bold text-slate-800 mt-1">{{ "%.0f"|format(stats.cout_pieces) }} €</p></div>
<div class="bg-red-100 p-3 rounded-full"><i class="fas fa-cog text-red-400 text-xl"></i></div>
</div>
</div>
<div class="bg-white rounded-xl shadow p-6">
<div class="flex items-center justify-between">
<div><p class="text-sm text-slate-500">Total dépensé</p><p class="text-3xl font-bold text-orange-500 mt-1">{{ "%.0f"|format(stats.cout_total) }} €</p></div>
<div class="bg-orange-100 p-3 rounded-full"><i class="fas fa-euro-sign text-orange-500 text-xl"></i></div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow">
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-800">Interventions récentes</h3>
<a href="/interventions/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition">+ Nouvelle</a>
</div>
{% if recent %}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
<tr>
<th class="px-6 py-3 text-left">Véhicule</th>
<th class="px-6 py-3 text-left">Description</th>
<th class="px-6 py-3 text-left">Statut</th>
<th class="px-6 py-3 text-left">Coût total</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for i in recent %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-medium text-slate-800"><a href="/vehicules/{{ i.vehicule_id }}" class="hover:text-orange-500">{{ i.immatriculation }}<br><span class="text-xs font-normal text-slate-400">{{ i.marque }} {{ i.modele }}</span></a></td>
<td class="px-6 py-4 text-slate-600"><a href="/interventions/{{ i.id }}" class="hover:text-orange-500">{{ i.description[:60] }}{% if i.description|length > 60 %}...{% endif %}</a></td>
<td class="px-6 py-4">
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
</td>
<td class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="p-8 text-center text-slate-400">
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention pour l'instant</p>
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a>
</div>
{% endif %}
</div>
{% endblock %}
+142
View File
@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Intervention #{{ intervention.id }} - GarageManager{% endblock %}
{% block content %}
<div class="mb-6">
<a href="/vehicules/{{ intervention.vid }}" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour au véhicule</a>
<div class="flex justify-between items-start">
<div>
<h2 class="text-2xl font-bold text-slate-800">{{ intervention.description }}</h2>
<p class="text-slate-500 mt-1">{{ intervention.immatriculation }} — {{ intervention.marque }} {{ intervention.modele }}</p>
<p class="text-xs text-slate-400 mt-1">{{ intervention.created_at[:10] }}{% if intervention.technicien %} • <i class="fas fa-user-cog mr-1"></i>{{ intervention.technicien }}{% endif %}</p>
</div>
<div class="flex gap-2 items-center">
{% if intervention.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm">⏳ En attente</span>
{% elif intervention.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm">🔧 En cours</span>
{% else %}<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm">✅ Terminé</span>{% endif %}
<a href="/interventions/{{ intervention.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
<form method="post" action="/interventions/{{ intervention.id }}/delete" onsubmit="return confirm('Supprimer cette intervention ?')">
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
</form>
</div>
</div>
</div>
<!-- Résumé coûts -->
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl shadow p-5">
<p class="text-xs text-slate-500">Main d'œuvre</p>
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre) }} €</p>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-xs text-slate-500">Pièces</p>
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_pieces) }} €</p>
</div>
<div class="bg-orange-50 rounded-xl shadow p-5 border border-orange-200">
<p class="text-xs text-orange-600">Total</p>
<p class="text-2xl font-bold text-orange-600 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre + intervention.cout_pieces) }} €</p>
</div>
</div>
{% if intervention.notes %}
<div class="bg-white rounded-xl shadow p-5 mb-6">
<h3 class="font-semibold text-slate-800 mb-2">📋 Notes</h3>
<p class="text-sm text-slate-600 whitespace-pre-wrap">{{ intervention.notes }}</p>
</div>
{% endif %}
<!-- Pièces -->
<div class="bg-white rounded-xl shadow">
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-800">🔩 Pièces utilisées ({{ pieces|length }})</h3>
<button onclick="document.getElementById('add-piece-form').classList.toggle('hidden')" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Ajouter une pièce</button>
</div>
<!-- Formulaire ajout pièce -->
<div id="add-piece-form" class="hidden p-5 border-b border-gray-100 bg-gray-50">
<form method="post" action="/interventions/{{ intervention.id }}/pieces/add" enctype="multipart/form-data">
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Nom de la pièce *</label>
<input name="nom" required placeholder="Ex: Filtre à huile, Courroie..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Référence</label>
<input name="reference" placeholder="Ex: OC90, 06A115561B..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div class="grid grid-cols-3 gap-3 mb-3">
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Marque</label>
<input name="marque" placeholder="Ex: Bosch, NGK, Valeo..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Prix unitaire (€)</label>
<input name="prix" type="number" step="0.01" min="0" value="0" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Quantité</label>
<input name="quantite" type="number" min="1" value="1" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div class="mb-3">
<label class="block text-xs font-medium text-slate-600 mb-1">Photo (optionnel)</label>
<input type="file" name="photo" accept="image/*" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
</div>
<button type="submit" class="bg-orange-500 text-white px-5 py-2 rounded-lg text-sm hover:bg-orange-600">Ajouter la pièce</button>
</form>
</div>
<!-- Liste des pièces -->
{% if pieces %}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
<tr>
<th class="px-5 py-3 text-left">Photo</th>
<th class="px-5 py-3 text-left">Pièce</th>
<th class="px-5 py-3 text-left">Référence</th>
<th class="px-5 py-3 text-left">Marque</th>
<th class="px-5 py-3 text-left">Qté</th>
<th class="px-5 py-3 text-left">Prix unit.</th>
<th class="px-5 py-3 text-left">Total</th>
<th class="px-5 py-3 text-left"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for p in pieces %}
<tr class="hover:bg-gray-50">
<td class="px-5 py-3">
{% if p.photo %}
<img src="{{ p.photo }}" alt="{{ p.nom }}" class="w-12 h-12 object-cover rounded-lg cursor-pointer border" onclick="window.open('{{ p.photo }}','_blank')">
{% else %}
<div class="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center text-slate-300"><i class="fas fa-cog"></i></div>
{% endif %}
</td>
<td class="px-5 py-3 font-medium text-slate-800">{{ p.nom }}</td>
<td class="px-5 py-3 text-slate-500 font-mono text-xs">{{ p.reference or '-' }}</td>
<td class="px-5 py-3 text-slate-600">{{ p.marque or '-' }}</td>
<td class="px-5 py-3 text-slate-600">{{ p.quantite }}</td>
<td class="px-5 py-3 text-slate-600">{{ "%.2f"|format(p.prix) }} €</td>
<td class="px-5 py-3 font-semibold text-slate-800">{{ "%.2f"|format(p.prix * p.quantite) }} €</td>
<td class="px-5 py-3">
<form method="post" action="/interventions/{{ intervention.id }}/pieces/{{ p.id }}/delete" onsubmit="return confirm('Supprimer cette pièce ?')">
<button class="text-red-400 hover:text-red-600 text-xs hover:underline">Supprimer</button>
</form>
</td>
</tr>
{% endfor %}
<tr class="bg-orange-50">
<td colspan="6" class="px-5 py-3 text-right font-semibold text-slate-700">Total pièces</td>
<td colspan="2" class="px-5 py-3 font-bold text-orange-600">{{ "%.2f"|format(intervention.cout_pieces) }} €</td>
</tr>
</tbody>
</table>
</div>
{% else %}
<div class="p-8 text-center text-slate-400 text-sm">
Aucune pièce enregistrée.<br>
<button onclick="document.getElementById('add-piece-form').classList.remove('hidden')" class="text-orange-500 hover:underline mt-1 inline-block">Ajouter la première →</button>
</div>
{% endif %}
</div>
{% endblock %}
+51
View File
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}{% if intervention %}Modifier{% else %}Nouvelle{% endif %} intervention - GarageManager{% endblock %}
{% block content %}
<div class="mb-8">
<a href="/interventions" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
<h2 class="text-2xl font-bold text-slate-800">{% if intervention %}Modifier l'intervention{% else %}Nouvelle intervention{% endif %}</h2>
</div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Véhicule *</label>
<select name="vehicule_id" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
<option value="">-- Sélectionner un véhicule --</option>
{% for v in vehicules %}
<option value="{{ v.id }}" {% if (intervention and intervention.vehicule_id == v.id) or preselect_vehicule == v.id %}selected{% endif %}>{{ v.label }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Description *</label>
<textarea name="description" rows="2" required placeholder="Ex: Vidange + filtre à huile, Changement courroie de distribution..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ intervention.description if intervention else '' }}</textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Statut</label>
<select name="statut" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
<option value="en_attente" {% if not intervention or intervention.statut == 'en_attente' %}selected{% endif %}>⏳ En attente</option>
<option value="en_cours" {% if intervention and intervention.statut == 'en_cours' %}selected{% endif %}>🔧 En cours</option>
<option value="termine" {% if intervention and intervention.statut == 'termine' %}selected{% endif %}>✅ Terminé</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Main d'œuvre (€)</label>
<input name="cout_main_oeuvre" type="number" step="0.01" min="0" value="{{ intervention.cout_main_oeuvre if intervention else '0' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Technicien</label>
<input name="technicien" value="{{ intervention.technicien if intervention else '' }}" placeholder="Nom du technicien / garage" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Notes</label>
<textarea name="notes" rows="3" placeholder="Observations, prochaine révision prévue, problèmes restants..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ intervention.notes if intervention else '' }}</textarea>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
<a href="/interventions" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
</div>
</form>
</div>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}Interventions - GarageManager{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-8">
<div><h2 class="text-2xl font-bold text-slate-800">Interventions</h2><p class="text-slate-500 mt-1">{{ interventions|length }} intervention(s)</p></div>
<a href="/interventions/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 flex items-center gap-2"><i class="fas fa-plus"></i> Nouvelle intervention</a>
</div>
<div class="flex gap-2 mb-6 flex-wrap">
<a href="/interventions" class="px-4 py-2 rounded-full text-sm {% if not statut %}bg-slate-800 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">Toutes</a>
<a href="/interventions?statut=en_attente" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_attente' %}bg-yellow-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">⏳ En attente</a>
<a href="/interventions?statut=en_cours" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_cours' %}bg-blue-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">🔧 En cours</a>
<a href="/interventions?statut=termine" class="px-4 py-2 rounded-full text-sm {% if statut == 'termine' %}bg-green-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">✅ Terminées</a>
</div>
<div class="bg-white rounded-xl shadow overflow-x-auto">
{% if interventions %}
<table class="w-full text-sm">
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
<tr>
<th class="px-6 py-3 text-left">Véhicule</th>
<th class="px-6 py-3 text-left">Description</th>
<th class="px-6 py-3 text-left">Statut</th>
<th class="px-6 py-3 text-left">Pièces</th>
<th class="px-6 py-3 text-left">Total</th>
<th class="px-6 py-3 text-left">Date</th>
<th class="px-6 py-3 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for i in interventions %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-medium text-slate-800">
<a href="/vehicules/{{ i.vehicule_id }}" class="hover:text-orange-500">{{ i.immatriculation }}<br><span class="text-xs font-normal text-slate-400">{{ i.marque }} {{ i.modele }}</span></a>
</td>
<td class="px-6 py-4 text-slate-600 max-w-xs">
<a href="/interventions/{{ i.id }}" class="hover:text-orange-500">{{ i.description[:60] }}{% if i.description|length > 60 %}...{% endif %}</a>
</td>
<td class="px-6 py-4">
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
</td>
<td class="px-6 py-4 text-slate-500 text-xs">{{ "%.2f"|format(i.cout_pieces) }} €</td>
<td class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td>
<td class="px-6 py-4 text-slate-400 text-xs">{{ i.created_at[:10] }}</td>
<td class="px-6 py-4 flex gap-3">
<a href="/interventions/{{ i.id }}" class="text-blue-500 hover:underline">Voir</a>
<a href="/interventions/{{ i.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
<form method="post" action="/interventions/{{ i.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ?')">
<button class="text-red-500 hover:underline">Supprimer</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-8 text-center text-slate-400">
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention trouvée</p>
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a>
</div>
{% endif %}
</div>
{% endblock %}
+128
View File
@@ -0,0 +1,128 @@
{% extends "base.html" %}
{% block title %}{{ vehicule.immatriculation }} - GarageManager{% endblock %}
{% block content %}
<div class="mb-6">
<a href="/vehicules" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
<div class="flex justify-between items-start">
<div>
<h2 class="text-2xl font-bold text-slate-800">{{ vehicule.immatriculation }}</h2>
<p class="text-slate-500 mt-1">{{ vehicule.marque }} {{ vehicule.modele }}{% if vehicule.annee %} ({{ vehicule.annee }}){% endif %}{% if vehicule.kilometrage %} • {{ vehicule.kilometrage }} km{% endif %}</p>
{% if vehicule.vin %}<p class="text-xs font-mono text-slate-400 mt-1">VIN : {{ vehicule.vin }}</p>{% endif %}
</div>
<div class="flex gap-2">
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Intervention</a>
<a href="/vehicules/{{ vehicule.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
<form method="post" action="/vehicules/{{ vehicule.id }}/delete" onsubmit="return confirm('Supprimer ce véhicule ?')">
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
</form>
</div>
</div>
</div>
<!-- Infos / Notes -->
<div class="bg-white rounded-xl shadow mb-6">
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-800">📎 Infos & Notes</h3>
<button onclick="document.getElementById('add-info-form').classList.toggle('hidden')" class="text-orange-500 text-sm hover:underline">+ Ajouter</button>
</div>
<div id="add-info-form" class="hidden p-5 border-b border-gray-100 bg-gray-50">
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/add" enctype="multipart/form-data" class="space-y-3">
<div class="grid grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Type</label>
<select name="type" id="info-type" onchange="toggleInfoType()" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
<option value="texte">📝 Texte</option>
<option value="url">🔗 URL / Lien</option>
<option value="image">🖼️ Image</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-slate-600 mb-1">Titre (optionnel)</label>
<input name="titre" placeholder="Ex: Manuel d'entretien, Lien forum..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
</div>
</div>
<div id="input-texte">
<label class="block text-xs font-medium text-slate-600 mb-1">Contenu *</label>
<textarea name="contenu" rows="3" placeholder="Notes libres, infos techniques..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"></textarea>
</div>
<div id="input-url" class="hidden">
<label class="block text-xs font-medium text-slate-600 mb-1">URL *</label>
<input name="contenu" placeholder="https://..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
</div>
<div id="input-image" class="hidden">
<label class="block text-xs font-medium text-slate-600 mb-1">Photo *</label>
<input type="file" name="photo" accept="image/*" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
<input type="hidden" name="contenu" value="photo">
</div>
<button type="submit" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">Enregistrer</button>
</form>
</div>
{% if infos %}
<div class="divide-y divide-gray-100">
{% for info in infos %}
<div class="px-5 py-4 flex justify-between items-start hover:bg-gray-50">
<div class="flex-1">
{% if info.titre %}<p class="text-xs font-semibold text-slate-500 uppercase mb-1">{{ info.titre }}</p>{% endif %}
{% if info.type == 'texte' %}
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ info.contenu }}</p>
{% elif info.type == 'url' %}
<a href="{{ info.contenu }}" target="_blank" class="text-sm text-blue-500 hover:underline break-all">🔗 {{ info.contenu }}</a>
{% elif info.type == 'image' %}
<img src="{{ info.contenu }}" alt="{{ info.titre or 'image' }}" class="max-h-48 rounded-lg mt-1 cursor-pointer" onclick="window.open('{{ info.contenu }}','_blank')">
{% endif %}
<p class="text-xs text-slate-400 mt-1">{{ info.created_at[:10] }}</p>
</div>
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/{{ info.id }}/delete" class="ml-4" onsubmit="return confirm('Supprimer ?')">
<button class="text-red-400 hover:text-red-600 text-xs"></button>
</form>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-6 text-center text-slate-400 text-sm">Aucune info pour l'instant.</div>
{% endif %}
</div>
<!-- Interventions -->
<div class="bg-white rounded-xl shadow">
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-800">🔧 Interventions ({{ interventions|length }})</h3>
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 text-sm hover:underline">+ Nouvelle intervention</a>
</div>
{% if interventions %}
<div class="divide-y divide-gray-100">
{% for i in interventions %}
<div class="px-5 py-4 hover:bg-gray-50">
<div class="flex justify-between items-start">
<div class="flex-1">
<a href="/interventions/{{ i.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ i.description }}</a>
{% if i.technicien %}<p class="text-xs text-slate-400 mt-1"><i class="fas fa-user-cog mr-1"></i>{{ i.technicien }}</p>{% endif %}
<p class="text-xs text-slate-400 mt-1">{{ i.created_at[:10] }} • {{ i.nb_pieces }} pièce(s)</p>
</div>
<div class="flex items-center gap-3 ml-4 flex-shrink-0">
<span class="text-sm font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</span>
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-6 text-center text-slate-400 text-sm">
Aucune intervention.<br>
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 hover:underline mt-1 inline-block">Créer la première →</a>
</div>
{% endif %}
</div>
<script>
function toggleInfoType() {
const type = document.getElementById('info-type').value;
document.getElementById('input-texte').classList.toggle('hidden', type !== 'texte');
document.getElementById('input-url').classList.toggle('hidden', type !== 'url');
document.getElementById('input-image').classList.toggle('hidden', type !== 'image');
}
</script>
{% endblock %}
+44
View File
@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}{% if vehicule %}Modifier{% else %}Nouveau{% endif %} véhicule - GarageManager{% endblock %}
{% block content %}
<div class="mb-8">
<a href="/vehicules" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
<h2 class="text-2xl font-bold text-slate-800">{% if vehicule %}Modifier le véhicule{% else %}Nouveau véhicule{% endif %}</h2>
</div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Immatriculation *</label>
<input name="immatriculation" value="{{ vehicule.immatriculation if vehicule else '' }}" required placeholder="AB-123-CD" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300 uppercase">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Marque *</label>
<input name="marque" value="{{ vehicule.marque if vehicule else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Modèle *</label>
<input name="modele" value="{{ vehicule.modele if vehicule else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Année</label>
<input name="annee" type="number" value="{{ vehicule.annee if vehicule else '' }}" min="1900" max="2030" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Kilométrage</label>
<input name="kilometrage" type="number" value="{{ vehicule.kilometrage if vehicule else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">VIN (numéro de châssis)</label>
<input name="vin" value="{{ vehicule.vin if vehicule else '' }}" placeholder="17 caractères" maxlength="17" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-orange-300 uppercase">
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
<a href="/vehicules" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
</div>
</form>
</div>
{% endblock %}
+58
View File
@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}Véhicules - GarageManager{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-8">
<div><h2 class="text-2xl font-bold text-slate-800">Véhicules</h2><p class="text-slate-500 mt-1">{{ vehicules|length }} véhicule(s)</p></div>
<a href="/vehicules/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition flex items-center gap-2"><i class="fas fa-plus"></i> Nouveau véhicule</a>
</div>
<div class="bg-white rounded-xl shadow">
<div class="p-4 border-b border-gray-100">
<form method="get" class="flex gap-2">
<input name="q" value="{{ q }}" placeholder="Immatriculation, marque, modèle, VIN..." class="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
<button type="submit" class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Rechercher</button>
{% if q %}<a href="/vehicules" class="text-slate-500 px-3 py-2 text-sm flex items-center"></a>{% endif %}
</form>
</div>
<div class="overflow-x-auto">
{% if vehicules %}
<table class="w-full text-sm">
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
<tr>
<th class="px-6 py-3 text-left">Immatriculation</th>
<th class="px-6 py-3 text-left">Marque / Modèle</th>
<th class="px-6 py-3 text-left">Année</th>
<th class="px-6 py-3 text-left">VIN</th>
<th class="px-6 py-3 text-left">Kilométrage</th>
<th class="px-6 py-3 text-left">Interventions</th>
<th class="px-6 py-3 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for v in vehicules %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-bold text-slate-800">{{ v.immatriculation }}</td>
<td class="px-6 py-4 font-medium text-slate-700">{{ v.marque }} {{ v.modele }}</td>
<td class="px-6 py-4 text-slate-500">{{ v.annee or '-' }}</td>
<td class="px-6 py-4 text-slate-400 text-xs font-mono">{{ v.vin or '-' }}</td>
<td class="px-6 py-4 text-slate-600">{{ (v.kilometrage|string + ' km') if v.kilometrage else '-' }}</td>
<td class="px-6 py-4"><span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">{{ v.nb_interventions }}</span></td>
<td class="px-6 py-4 flex gap-3">
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline">Voir</a>
<a href="/vehicules/{{ v.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
<form method="post" action="/vehicules/{{ v.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce véhicule ?')">
<button class="text-red-500 hover:underline">Supprimer</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-8 text-center text-slate-400">
<i class="fas fa-car text-3xl mb-3"></i><p>Aucun véhicule trouvé</p>
<a href="/vehicules/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}