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
+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 %}