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:
@@ -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 %}
|
||||
Reference in New Issue
Block a user