feat: redesign dark futuriste (Luca) — thème cyan/volt, sidebar dark, JetBrains Mono + README

This commit is contained in:
Luca
2026-03-15 21:24:59 +01:00
parent 97ae64caf0
commit c8b704216d
12 changed files with 1552 additions and 691 deletions
+130 -200
View File
@@ -1,224 +1,154 @@
# 🔧 GarageManager # 🔧 GarageManager
> Application web de gestion de garage automobile — véhicules, interventions, pièces détachées. > Application de gestion de garage automobile — suivi véhicules, interventions et pièces.
[![FastAPI](https://img.shields.io/badge/FastAPI-0.109-009688?style=flat&logo=fastapi)](https://fastapi.tiangolo.com) [![Python](https://img.shields.io/badge/Python-3.11-blue)](https://python.org)
[![Python](https://img.shields.io/badge/Python-3.12-3776AB?style=flat&logo=python)](https://python.org) [![FastAPI](https://img.shields.io/badge/FastAPI-latest-009688)](https://fastapi.tiangolo.com)
[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?style=flat&logo=docker)](https://docker.com) [![Docker](https://img.shields.io/badge/Docker-compose-2496ED)](https://docs.docker.com/compose/)
[![SQLite](https://img.shields.io/badge/SQLite-léger-003B57?style=flat&logo=sqlite)](https://sqlite.org)
🌐 **URL de production** : [https://garage.nas.percolouco.com](https://garage.nas.percolouco.com)
---
## 📋 Description
**GarageManager** est une application web légère permettant de gérer un garage automobile personnel ou professionnel. Elle permet de suivre les véhicules, leurs interventions mécaniques et les pièces utilisées, avec un tableau de bord synthétique.
Pensée pour être self-hostée, elle tourne dans un conteneur Docker derrière un reverse proxy Traefik.
---
## 🛠️ Stack technique
| Composant | Technologie | Rôle |
|-----------|-------------|------|
| **Backend** | [FastAPI](https://fastapi.tiangolo.com) 0.109 | API REST + rendu SSR |
| **Base de données** | [SQLite](https://sqlite.org) | Stockage persistant, fichier unique |
| **Templates** | [Jinja2](https://jinja.palletsprojects.com) 3.1 | Rendu HTML côté serveur |
| **CSS** | [Tailwind CSS](https://tailwindcss.com) (CDN) | Interface responsive moderne |
| **Serveur ASGI** | [Uvicorn](https://www.uvicorn.org) | Serveur HTTP haute performance |
| **Conteneur** | [Docker](https://docker.com) + Compose | Déploiement clé en main |
| **Reverse proxy** | [Traefik](https://traefik.io) | HTTPS automatique (Let's Encrypt) |
--- ---
## ✨ Fonctionnalités ## ✨ Fonctionnalités
### 🚗 Gestion des véhicules - **Véhicules** — Enregistrement complet (immatriculation, VIN, kilométrage, marque/modèle/année)
- **Interventions** — Historique par véhicule avec date, kilométrage, technicien, coûts main d'œuvre
- **Pièces** — Suivi par intervention avec référence, marque, prix, quantité et photo optionnelle
- **Clients** — Gestion des propriétaires de véhicules
- **Infos & Notes** — Ajout de textes, URLs et photos sur chaque véhicule
- **Dashboard** — Vue d'ensemble : stats globales, dernières interventions
- Ajout de véhicules avec les informations clés : ## 🎨 Design
- **Immatriculation** (clé unique, mise en majuscules automatiquement)
- **Marque**, **modèle**, **année**, **kilométrage**
- **Numéro VIN** (Vehicle Identification Number)
- **Infos libres** par véhicule : notes texte, liens URL, photos — système extensible
- Recherche par immatriculation, marque, modèle ou VIN
- Compteur d'interventions par véhicule
- Suppression en cascade (interventions + pièces supprimées automatiquement)
### 🔩 Gestion des interventions Thème sombre futuriste inspiré du projet `location` de Perco :
- Fond noir (`#0a0f1a`) avec dégradés subtils cyan/jaune
- Accents électriques : cyan `#00d4ff` et volt `#facc15`
- Police [Outfit](https://fonts.google.com/specimen/Outfit) + [JetBrains Mono](https://www.jetbrains.com/lp/mono/) pour les références/immatriculations
- Sidebar dark avec navigation intuitive
- Création d'interventions liées à un véhicule ## 🚀 Démarrage rapide
- Champs disponibles :
- **Description** de l'intervention
- **Statut** : `En attente` / `En cours` / `Terminé`
- **Coût main-d'œuvre**
- **Technicien** responsable
- **Notes** internes
- Suivi du coût total (main-d'œuvre + pièces)
- Filtrage par statut
### 🔧 Gestion des pièces détachées ```bash
# Lancer le service
cd /opt/container/garagemanager
docker compose up -d
- Association de pièces à chaque intervention : # Rebuild complet (après modif templates/code)
- **Nom** de la pièce docker compose build --no-cache && docker compose up -d
- **Référence** constructeur
- **Marque** de la pièce
- **Prix unitaire** et **quantité**
- **Photo** (upload d'image)
- Calcul automatique du coût total par intervention
### 📊 Tableau de bord # Logs
docker compose logs -f
```
- Vue synthétique en temps réel : ## 🏗️ Architecture
- Nombre de véhicules enregistrés
- Interventions par statut (en attente / en cours / terminées)
- Coût total des interventions terminées (pièces + main-d'œuvre)
- 5 dernières interventions avec accès rapide
---
## 📁 Structure du projet
``` ```
garagemanager/ garagemanager/
├── app/ ├── app/
│ ├── __init__.py │ ├── __init__.py
│ └── main.py # Application FastAPI (routes, DB, logique) │ └── main.py # FastAPI routes + SQLite logic
├── templates/ ├── templates/ # Jinja2 templates (thème dark)
│ ├── base.html # Template de base (nav, layout) │ ├── base.html # Layout sidebar + variables CSS
│ ├── dashboard.html # Tableau de bord │ ├── dashboard.html # Stats cards + tableau récent
│ ├── vehicules.html # Liste des véhicules │ ├── vehicules.html # Liste véhicules
│ ├── vehicule_form.html # Formulaire ajout/édition véhicule │ ├── vehicule_form.html
│ ├── vehicule_detail.html # Détail véhicule + interventions + infos │ ├── vehicule_detail.html # Interventions + pièces + infos
│ ├── interventions.html # Liste des interventions │ ├── interventions.html
│ ├── intervention_form.html # Formulaire ajout/édition intervention │ ├── intervention_form.html
── intervention_detail.html # Détail intervention + pièces ── intervention_detail.html # Détail + gestion pièces
├── data/ # Volume persistant (ignoré par git) │ ├── clients.html
── garage.db # Base SQLite ── client_form.html
├── Dockerfile # Image Docker Python 3.12-slim │ └── client_detail.html
├── docker-compose.yml # Déploiement avec Traefik ├── data/
├── requirements.txt # Dépendances Python │ └── garage.db # Base SQLite (volume Docker)
── README.md ── Dockerfile
├── docker-compose.yml
└── requirements.txt
``` ```
## 🔌 API REST
FastAPI génère automatiquement la documentation :
- **Swagger UI** : https://garage.nas.percolouco.com/docs
- **ReDoc** : https://garage.nas.percolouco.com/redoc
### Endpoints principaux
| Méthode | Route | Description |
|---------|-------|-------------|
| `GET` | `/` | Dashboard |
| `GET` | `/vehicules` | Liste des véhicules |
| `POST` | `/vehicules/new` | Créer un véhicule |
| `GET` | `/vehicules/{id}` | Détail véhicule |
| `GET` | `/interventions` | Liste des interventions |
| `POST` | `/interventions/new` | Créer une intervention |
| `POST` | `/interventions/{id}/pieces/add` | Ajouter une pièce |
## 🗃️ Modèle de données (SQLite)
```sql
-- Clients
CREATE TABLE clients (
id TEXT PRIMARY KEY,
nom TEXT NOT NULL,
prenom TEXT NOT NULL,
telephone TEXT,
email TEXT,
adresse TEXT,
created_at TEXT
);
-- Véhicules
CREATE TABLE vehicules (
id TEXT PRIMARY KEY,
immatriculation TEXT NOT NULL,
marque TEXT NOT NULL,
modele TEXT NOT NULL,
annee INTEGER,
kilometrage INTEGER,
vin TEXT,
client_id TEXT REFERENCES clients(id),
created_at TEXT
);
-- Interventions
CREATE TABLE interventions (
id TEXT PRIMARY KEY,
vehicule_id TEXT NOT NULL REFERENCES vehicules(id),
description TEXT NOT NULL,
date_intervention TEXT,
kilometrage_intervention INTEGER,
cout_main_oeuvre REAL DEFAULT 0,
technicien TEXT,
notes TEXT,
created_at TEXT
);
-- Pièces
CREATE TABLE pieces (
id TEXT PRIMARY KEY,
intervention_id TEXT NOT NULL REFERENCES interventions(id),
nom TEXT NOT NULL,
reference TEXT,
marque TEXT,
prix REAL DEFAULT 0,
quantite INTEGER DEFAULT 1,
photo TEXT,
created_at TEXT
);
```
## ⚙️ Configuration
| Variable d'env | Défaut | Description |
|----------------|--------|-------------|
| `DB_PATH` | `/data/garage.db` | Chemin vers la base SQLite |
## 🌐 Accès
- **Production** : https://garage.nas.percolouco.com
- **Réseau interne** : via Traefik sur le réseau Docker `proxy`
--- ---
## 🚀 Installation *Développé par l'équipe Perco — Design Luca, Backend intégré FastAPI*
### Prérequis
- Docker & Docker Compose installés
- (Optionnel) Traefik configuré pour HTTPS automatique
### 1. Cloner le dépôt
```bash
git clone http://192.168.1.29:3500/perco/garagemanager.git
cd garagemanager
```
### 2. Démarrer avec Docker Compose
**Avec Traefik (production)** :
```bash
docker compose up -d
```
L'application sera accessible via `https://garage.nas.percolouco.com` si Traefik est configuré avec le réseau `proxy`.
**Sans Traefik (local/dev)** :
Modifier `docker-compose.yml` pour exposer un port directement :
```yaml
ports:
- "8000:8000"
```
Puis :
```bash
docker compose up -d
```
Accessible sur : `http://localhost:8000`
### 3. Développement local (sans Docker)
```bash
# Créer un environnement virtuel
python3 -m venv venv
source venv/bin/activate
# Installer les dépendances
pip install -r requirements.txt
# Lancer le serveur avec rechargement automatique
DB_PATH=./data/garage.db uvicorn app.main:app --reload --port 8000
```
Accessible sur : `http://localhost:8000`
---
## 🗄️ Base de données
La base SQLite est initialisée automatiquement au démarrage. Le schéma comprend 4 tables :
| Table | Description |
|-------|-------------|
| `vehicules` | Véhicules (immat, marque, modèle, VIN, km) |
| `vehicule_infos` | Infos libres par véhicule (texte/url/image) |
| `interventions` | Interventions mécaniques avec statut et coûts |
| `pieces` | Pièces détachées liées aux interventions |
Les suppressions en cascade sont gérées par des contraintes SQLite (`ON DELETE CASCADE`).
---
## 🐳 Variables d'environnement
| Variable | Défaut | Description |
|----------|--------|-------------|
| `DB_PATH` | `/data/garage.db` | Chemin de la base SQLite |
| `UPLOAD_DIR` | `/data/uploads` | Dossier de stockage des photos |
---
## 🔄 Mise à jour
```bash
# Arrêter le conteneur
docker compose down
# Récupérer les dernières modifications
git pull
# Reconstruire l'image et redémarrer
docker compose up -d --build
```
---
## 📝 Notes
- Les données sont persistées dans le dossier `./data/` (volume Docker)
- Les photos uploadées sont stockées dans `./data/uploads/`
- Le dossier `data/` est exclu du dépôt git (`.gitignore`)
- L'application ne gère pas d'authentification — à protéger via Traefik middleware si nécessaire
---
## 🏷️ Versions
| Version | Notes |
|---------|-------|
| v2.1 | Infos libres véhicule (texte/url/image), upload photos pièces |
| v2.0 | Ajout gestion des pièces détachées |
| v1.0 | Gestion véhicules + interventions |
---
*Développé avec ❤️ pour la gestion de garage automobile*
+610 -18
View File
@@ -4,31 +4,623 @@
<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 %}GarageManager{% endblock %}</title> <title>{% block title %}GarageManager{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script> <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=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-primary: #0a0f1a;
--bg-secondary: #111827;
--bg-card: #1a2234;
--bg-input: #0d1321;
--accent-electric: #00d4ff;
--accent-volt: #facc15;
--accent-green: #22c55e;
--accent-red: #ef4444;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border: #2d3a52;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
--glow-electric: 0 0 20px rgba(0, 212, 255, 0.3);
}
body {
font-family: "Outfit", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
}
body::before {
content: "";
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(circle at 20% 20%, rgba(0,212,255,0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(250,204,21,0.06) 0%, transparent 50%),
linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
pointer-events: none;
z-index: -1;
}
/* ── Sidebar ── */
.app-sidebar {
width: 220px;
background: var(--bg-card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: fixed;
top: 0; left: 0; bottom: 0;
z-index: 100;
}
.sidebar-brand {
padding: 1.25rem 1.25rem 1rem;
font-size: 1.15rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-electric), var(--accent-volt));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
}
.sidebar-brand i {
background: linear-gradient(135deg, var(--accent-electric), var(--accent-volt));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sidebar-nav {
flex: 1;
padding: 0.75rem 0.5rem;
}
.sidebar-section {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
padding: 0.75rem 0.75rem 0.25rem;
font-weight: 600;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.75rem;
color: var(--text-secondary);
text-decoration: none;
border-radius: 8px;
margin: 0.1rem 0;
transition: all 0.2s;
font-size: 0.9rem;
font-weight: 500;
}
.sidebar-link i {
width: 16px;
text-align: center;
font-size: 0.85rem;
}
.sidebar-link:hover {
background: rgba(0, 212, 255, 0.08);
color: var(--accent-electric);
}
.sidebar-link.active {
background: rgba(0, 212, 255, 0.12);
color: var(--accent-electric);
border: 1px solid rgba(0, 212, 255, 0.2);
}
.sidebar-footer {
padding: 0.75rem 1rem;
border-top: 1px solid var(--border);
font-size: 0.7rem;
color: var(--text-muted);
text-align: center;
}
/* ── Main content ── */
.app-main {
margin-left: 220px;
flex: 1;
padding: 2rem;
min-height: 100vh;
}
/* ── Cards ── */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.card:hover {
border-color: rgba(0, 212, 255, 0.3);
box-shadow: var(--glow-electric);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.card-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--text-primary);
}
/* ── Tables ── */
table {
width: 100%;
border-collapse: collapse;
}
th {
background: var(--bg-secondary);
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.75rem 1rem;
text-align: left;
font-weight: 600;
}
td {
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
font-size: 0.88rem;
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background: rgba(0, 212, 255, 0.03);
}
tbody tr {
transition: background 0.15s;
}
/* ── Buttons ── */
.btn-primary {
background: linear-gradient(135deg, var(--accent-electric), #0099cc);
color: #0a0f1a;
font-weight: 600;
padding: 0.5rem 1.25rem;
border-radius: 8px;
border: none;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
font-family: "Outfit", sans-serif;
transition: box-shadow 0.2s, opacity 0.2s;
}
.btn-primary:hover {
box-shadow: var(--glow-electric);
opacity: 0.95;
}
.btn-secondary {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
padding: 0.5rem 1.25rem;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
font-family: "Outfit", sans-serif;
transition: border-color 0.2s, color 0.2s;
}
.btn-secondary:hover {
border-color: var(--accent-electric);
color: var(--accent-electric);
}
.btn-danger {
background: rgba(239, 68, 68, 0.1);
color: var(--accent-red);
border: 1px solid rgba(239, 68, 68, 0.3);
padding: 0.5rem 1.25rem;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
font-family: "Outfit", sans-serif;
transition: background 0.2s;
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.2);
}
.btn-sm {
padding: 0.35rem 0.875rem;
font-size: 0.8rem;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
justify-content: center;
border-radius: 6px;
}
/* ── Inputs ── */
input, select, textarea {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 8px;
padding: 0.5rem 0.75rem;
font-family: "Outfit", sans-serif;
font-size: 0.875rem;
width: 100%;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus, select:focus, textarea:focus {
border-color: var(--accent-electric);
outline: none;
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
input::placeholder, textarea::placeholder {
color: var(--text-muted);
}
select option {
background: var(--bg-secondary);
}
label {
display: block;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.4rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Badges ── */
.badge {
padding: 0.2rem 0.65rem;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.badge-success {
background: rgba(34, 197, 94, 0.12);
color: var(--accent-green);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.badge-warning {
background: rgba(250, 204, 21, 0.12);
color: var(--accent-volt);
border: 1px solid rgba(250, 204, 21, 0.2);
}
.badge-danger {
background: rgba(239, 68, 68, 0.12);
color: var(--accent-red);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.badge-info {
background: rgba(0, 212, 255, 0.1);
color: var(--accent-electric);
border: 1px solid rgba(0, 212, 255, 0.2);
}
.badge-neutral {
background: rgba(100, 116, 139, 0.12);
color: var(--text-secondary);
border: 1px solid var(--border);
}
/* ── Stats cards ── */
.stat-value {
font-size: 2.25rem;
font-weight: 700;
color: var(--accent-electric);
line-height: 1;
}
.stat-label {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Mono ── */
.mono {
font-family: "JetBrains Mono", monospace;
}
/* ── Page header ── */
.page-header {
margin-bottom: 2rem;
}
.page-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.page-subtitle {
font-size: 0.875rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
/* ── Back link ── */
.back-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--text-muted);
text-decoration: none;
font-size: 0.85rem;
margin-bottom: 1rem;
transition: color 0.2s;
}
.back-link:hover {
color: var(--accent-electric);
}
/* ── Grid ── */
.grid { display: grid; }
.grid-2 { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.grid-3 { grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.grid-4 { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
/* ── Flex utils ── */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.flex-1 { flex: 1; }
.flex-wrap { flex-wrap: wrap; }
/* ── Spacing ── */
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.ml-2 { margin-left: 0.5rem; }
/* ── Text utils ── */
.text-electric { color: var(--accent-electric); }
.text-volt { color: var(--accent-volt); }
.text-green { color: var(--accent-green); }
.text-red { color: var(--accent-red); }
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.font-medium { font-weight: 500; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uppercase { text-transform: uppercase; }
/* ── Empty state ── */
.empty-state {
padding: 3rem;
text-align: center;
color: var(--text-muted);
}
.empty-state i {
font-size: 2.5rem;
margin-bottom: 0.75rem;
display: block;
color: var(--border);
}
.empty-state p {
margin-bottom: 0.5rem;
}
/* ── Overflow ── */
.overflow-x-auto { overflow-x: auto; }
/* ── Link actions ── */
.link-action {
color: var(--accent-electric);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: opacity 0.2s;
}
.link-action:hover { opacity: 0.7; }
.link-danger {
color: var(--accent-red);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
background: none;
border: none;
cursor: pointer;
font-family: "Outfit", sans-serif;
padding: 0;
transition: opacity 0.2s;
}
.link-danger:hover { opacity: 0.7; }
.link-edit {
color: var(--accent-volt);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: opacity 0.2s;
}
.link-edit:hover { opacity: 0.7; }
/* ── Section dividers ── */
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 1.5rem 0;
}
/* ── Scrollable list ── */
.scroll-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem;
background: var(--bg-input);
}
.scroll-list-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.scroll-list-item:hover {
background: rgba(0, 212, 255, 0.06);
}
/* ── Toggle section ── */
.toggle-section {
display: none;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 1.25rem 1.5rem;
}
.toggle-section.open {
display: block;
}
/* ── Collapsible ── */
details summary {
list-style: none;
cursor: pointer;
}
details summary::-webkit-details-marker { display: none; }
/* ── Photo thumb ── */
.photo-thumb {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--border);
cursor: pointer;
}
.photo-placeholder {
width: 40px;
height: 40px;
background: var(--bg-secondary);
border-radius: 6px;
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
color: var(--border);
}
</style>
{% block extra_head %}{% endblock %}
</head> </head>
<body class="bg-gray-100 min-h-screen flex"> <body>
<div class="w-64 bg-slate-800 min-h-screen flex flex-col fixed left-0 top-0"> <!-- Sidebar -->
<div class="p-6 border-b border-slate-700"> <aside class="app-sidebar">
<h1 class="text-white text-xl font-bold flex items-center gap-2"> <div class="sidebar-brand">
<i class="fas fa-wrench text-orange-400"></i> GarageManager <i class="fas fa-wrench"></i> GarageManager
</h1>
</div> </div>
<nav class="flex-1 p-4 space-y-1"> <nav class="sidebar-nav">
<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 %}"> <div class="sidebar-section">Navigation</div>
<i class="fas fa-chart-line w-5"></i> Dashboard <a href="/" class="sidebar-link {% if page == 'dashboard' %}active{% endif %}">
<i class="fas fa-chart-line"></i> Dashboard
</a> </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 %}"> <a href="/vehicules" class="sidebar-link {% if page == 'vehicules' %}active{% endif %}">
<i class="fas fa-car w-5"></i> Véhicules <i class="fas fa-car"></i> Véhicules
</a> </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 %}"> <a href="/interventions" class="sidebar-link {% if page == 'interventions' %}active{% endif %}">
<i class="fas fa-tools w-5"></i> Interventions <i class="fas fa-tools"></i> Interventions
</a>
<a href="/clients" class="sidebar-link {% if page == 'clients' %}active{% endif %}">
<i class="fas fa-users"></i> Clients
</a> </a>
</nav> </nav>
<div class="p-4 border-t border-slate-700 text-xs text-slate-500 text-center">GarageManager v2.1</div> <div class="sidebar-footer">GarageManager v2.1</div>
</div> </aside>
<div class="ml-64 flex-1 p-8">
<!-- Main -->
<main class="app-main">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </main>
</body> </body>
</html> </html>
+51 -26
View File
@@ -1,49 +1,74 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ client.nom }} {{ client.prenom }} - GarageManager{% endblock %} {% block title %}{{ client.nom }} {{ client.prenom }} GarageManager{% endblock %}
{% block content %} {% 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> <a href="/clients" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
<div class="flex justify-between items-start">
<!-- Header -->
<div class="flex justify-between items-start mb-8">
<div> <div>
<h2 class="text-2xl font-bold text-slate-800">{{ client.nom }} {{ client.prenom }}</h2> <h1 class="page-title">{{ client.nom }} {{ client.prenom }}</h1>
<div class="flex flex-wrap gap-4 mt-2 text-sm text-slate-500"> <div class="flex gap-4 mt-2" style="flex-wrap:wrap">
{% if client.telephone %}<span><i class="fas fa-phone mr-1"></i>{{ client.telephone }}</span>{% endif %} {% if client.telephone %}
{% if client.email %}<span><i class="fas fa-envelope mr-1"></i>{{ client.email }}</span>{% endif %} <span class="text-sm text-secondary"><i class="fas fa-phone mr-1 text-electric"></i>{{ client.telephone }}</span>
{% if client.adresse %}<span><i class="fas fa-map-marker-alt mr-1"></i>{{ client.adresse }}</span>{% endif %} {% endif %}
{% if client.email %}
<span class="text-sm text-secondary"><i class="fas fa-envelope mr-1 text-electric"></i>{{ client.email }}</span>
{% endif %}
{% if client.adresse %}
<span class="text-sm text-secondary"><i class="fas fa-map-marker-alt mr-1 text-electric"></i>{{ client.adresse }}</span>
{% endif %}
</div> </div>
</div> </div>
<div class="flex gap-2"> <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> <a href="/clients/{{ client.id }}/edit" class="btn-secondary btn-sm">
<i class="fas fa-edit"></i> Modifier
</a>
<form method="post" action="/clients/{{ client.id }}/delete" onsubmit="return confirm('Supprimer ce client et tous ses véhicules ?')"> <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> <button class="btn-danger btn-sm">
<i class="fas fa-trash"></i> Supprimer
</button>
</form> </form>
</div> </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"> <!-- Véhicules du client -->
<h3 class="font-semibold text-slate-800">Véhicules ({{ vehicules|length }})</h3> <div class="card" style="padding:0">
<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 class="card-header">
<span class="card-title">
<i class="fas fa-car text-electric" style="margin-right:0.4rem"></i>
Véhicules
<span class="badge badge-info ml-2">{{ vehicules|length }}</span>
</span>
<a href="/vehicules/new" class="btn-primary btn-sm">
<i class="fas fa-plus"></i> Ajouter
</a>
</div> </div>
{% if vehicules %} {% 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> <div>
<span class="font-medium text-slate-800">{{ v.immatriculation }}</span> {% for v in vehicules %}
<span class="text-slate-500 ml-2">{{ v.marque }} {{ v.modele }}</span> <div style="padding:1rem 1.5rem;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;transition:background 0.15s" onmouseover="this.style.background='rgba(0,212,255,0.03)'" onmouseout="this.style.background=''">
{% if v.annee %}<span class="text-slate-400 text-sm ml-2">({{ v.annee }})</span>{% endif %} <div>
{% if v.kilometrage %}<span class="text-slate-400 text-sm ml-2">{{ v.kilometrage }} km</span>{% endif %} <span class="font-semibold mono" style="color:var(--accent-electric)">{{ v.immatriculation }}</span>
<span class="text-secondary ml-2">{{ v.marque }} {{ v.modele }}</span>
{% if v.annee %}<span class="text-muted text-xs ml-2">({{ v.annee }})</span>{% endif %}
{% if v.kilometrage %}<span class="text-muted text-xs ml-2 mono">— {{ v.kilometrage }} km</span>{% endif %}
</div> </div>
<div class="flex items-center gap-3"> <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> <span class="badge badge-info">{{ v.nb_interventions }} intervention(s)</span>
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline text-sm">Voir →</a> <a href="/vehicules/{{ v.id }}" class="link-action text-sm">Voir →</a>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400 text-sm">Aucun véhicule enregistré pour ce client.</div> <div class="empty-state">
<i class="fas fa-car"></i>
<p>Aucun véhicule enregistré pour ce client</p>
<a href="/vehicules/new" class="link-action">Ajouter un véhicule →</a>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
+34 -20
View File
@@ -1,38 +1,52 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% if client %}Modifier{% else %}Nouveau{% endif %} client - GarageManager{% endblock %} {% block title %}{% if client %}Modifier{% else %}Nouveau{% endif %} client GarageManager{% endblock %}
{% block content %} {% 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> <a href="/clients" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
<h2 class="text-2xl font-bold text-slate-800">{% if client %}Modifier le client{% else %}Nouveau client{% endif %}</h2>
<div class="page-header">
<h1 class="page-title">
<i class="fas fa-{% if client %}edit{% else %}user-plus{% endif %} text-electric" style="margin-right:0.5rem"></i>
{% if client %}Modifier le client{% else %}Nouveau client{% endif %}
</h1>
</div> </div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4"> <div class="card" style="max-width:580px">
<div class="grid grid-cols-2 gap-4"> <form method="post" style="display:flex;flex-direction:column;gap:1.25rem">
<div class="grid grid-2">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Nom *</label> <label>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"> <input name="nom" value="{{ client.nom if client else '' }}" required>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Prénom *</label> <label>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"> <input name="prenom" value="{{ client.prenom if client else '' }}" required>
</div> </div>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Téléphone</label> <label>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"> <input name="telephone" value="{{ client.telephone if client else '' }}" placeholder="06 xx xx xx xx">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Email</label> <label>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"> <input name="email" type="email" value="{{ client.email if client else '' }}" placeholder="contact@exemple.fr">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Adresse</label> <label>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> <textarea name="adresse" rows="2" placeholder="Adresse complète…">{{ client.adresse if client else '' }}</textarea>
</div> </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> <div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<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> <button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Enregistrer
</button>
<a href="/clients" class="btn-secondary">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
+54 -33
View File
@@ -1,54 +1,75 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Clients - GarageManager{% endblock %} {% block title %}Clients GarageManager{% endblock %}
{% block content %} {% 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> <div class="page-header flex justify-between items-center">
<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> <h1 class="page-title"><i class="fas fa-users text-electric" style="margin-right:0.5rem"></i>Clients</h1>
<div class="bg-white rounded-xl shadow"> <p class="page-subtitle">{{ clients|length }} client(s) enregistré(s)</p>
<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>
<a href="/clients/new" class="btn-primary">
<i class="fas fa-plus"></i> Nouveau client
</a>
</div>
<!-- Search -->
<div class="card mb-4" style="padding:1rem 1.25rem">
<form method="get" class="flex gap-2">
<input name="q" value="{{ q }}" placeholder="Rechercher par nom, téléphone…" style="flex:1">
<button type="submit" class="btn-secondary" style="width:auto;white-space:nowrap">
<i class="fas fa-search"></i> Rechercher
</button>
{% if q %}
<a href="/clients" class="btn-secondary" style="width:auto"></a>
{% endif %}
</form>
</div>
<div class="card" style="padding:0">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
{% if clients %} {% if clients %}
<table class="w-full text-sm"> <table>
<thead class="bg-gray-50 text-slate-600 text-xs uppercase"> <thead>
<tr> <tr>
<th class="px-6 py-3 text-left">Nom</th> <th>Nom</th>
<th class="px-6 py-3 text-left">Téléphone</th> <th>Téléphone</th>
<th class="px-6 py-3 text-left">Email</th> <th>Email</th>
<th class="px-6 py-3 text-left">Véhicules</th> <th>Véhicules</th>
<th class="px-6 py-3 text-left">Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody>
{% for c in clients %} {% for c in clients %}
<tr class="hover:bg-gray-50"> <tr>
<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>
<td class="px-6 py-4 text-slate-600">{{ c.telephone or '-' }}</td> <a href="/clients/{{ c.id }}" class="font-semibold" style="text-decoration:none;color:var(--text-primary)">{{ c.nom }} {{ c.prenom }}</a>
<td class="px-6 py-4 text-slate-600">{{ c.email or '-' }}</td> </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="text-secondary text-sm">{{ c.telephone or '—' }}</td>
<td class="px-6 py-4 flex gap-3"> <td class="text-secondary text-sm">{{ c.email or '—' }}</td>
<a href="/clients/{{ c.id }}" class="text-blue-500 hover:underline">Voir</a> <td>
<a href="/clients/{{ c.id }}/edit" class="text-orange-500 hover:underline">Modifier</a> <span class="badge badge-info">{{ c.nb_vehicules }}</span>
<form method="post" action="/clients/{{ c.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce client ?')"> </td>
<button type="submit" class="text-red-500 hover:underline">Supprimer</button> <td>
<div class="flex gap-3">
<a href="/clients/{{ c.id }}" class="link-action">Voir</a>
<a href="/clients/{{ c.id }}/edit" class="link-edit">Modifier</a>
<form method="post" action="/clients/{{ c.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer ce client ?')">
<button class="link-danger">Supprimer</button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400"> <div class="empty-state">
<i class="fas fa-users text-3xl mb-3"></i><p>Aucun client trouvé</p> <i class="fas fa-users"></i>
<a href="/clients/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a> <p>Aucun client trouvé</p>
<a href="/clients/new" class="link-action">Ajouter le premier →</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
+93 -63
View File
@@ -1,89 +1,119 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard - GarageManager{% endblock %} {% block title %}Dashboard GarageManager{% endblock %}
{% block content %} {% block content %}
<div class="mb-8">
<h2 class="text-2xl font-bold text-slate-800">Dashboard</h2> <div class="page-header flex justify-between items-center">
<p class="text-slate-500 mt-1">Vue d'ensemble de votre garage</p> <div>
<h1 class="page-title"><i class="fas fa-chart-line text-electric" style="margin-right:0.5rem"></i>Dashboard</h1>
<p class="page-subtitle">Vue d'ensemble de votre garage</p>
</div>
<a href="/interventions/new" class="btn-primary">
<i class="fas fa-plus"></i> Nouvelle intervention
</a>
</div> </div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl shadow p-6"> <!-- Stats -->
<div class="flex items-center justify-between"> <div class="grid grid-4 mb-8">
<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="card" style="cursor:default">
<div class="bg-purple-100 p-3 rounded-full"><i class="fas fa-car text-purple-500 text-xl"></i></div> <div class="flex items-center justify-between mb-3">
</div> <div class="stat-label">Véhicules</div>
<a href="/vehicules" class="text-xs text-purple-500 hover:underline mt-3 inline-block">Voir tous →</a> <div style="width:38px;height:38px;background:rgba(0,212,255,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
</div> <i class="fas fa-car text-electric"></i>
<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> </div>
<div class="bg-white rounded-xl shadow p-6"> <div class="stat-value">{{ stats.nb_vehicules }}</div>
<div class="flex items-center justify-between"> <a href="/vehicules" class="link-action text-xs mt-2" style="display:inline-block">Voir tous →</a>
<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 class="card" style="cursor:default">
<div class="flex items-center justify-between mb-3">
<div class="stat-label">Interventions</div>
<div style="width:38px;height:38px;background:rgba(250,204,21,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
<i class="fas fa-tools text-volt"></i>
</div>
</div>
<div class="stat-value" style="color:var(--accent-volt)">{{ stats.nb_interventions }}</div>
<a href="/interventions" class="link-action text-xs mt-2" style="display:inline-block;color:var(--accent-volt)">Voir toutes →</a>
</div>
<div class="card" style="cursor:default">
<div class="flex items-center justify-between mb-3">
<div class="stat-label">Pièces en stock</div>
<div style="width:38px;height:38px;background:rgba(34,197,94,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
<i class="fas fa-cog text-green"></i>
</div>
</div>
<div class="stat-value" style="color:var(--accent-green)">{{ stats.nb_pieces_stock }}</div>
<span class="text-xs text-muted mt-2" style="display:inline-block">Pièces référencées</span>
</div>
<div class="card" style="cursor:default;border-color:rgba(250,204,21,0.2)">
<div class="flex items-center justify-between mb-3">
<div class="stat-label" style="color:var(--accent-volt)">Total dépensé</div>
<div style="width:38px;height:38px;background:rgba(250,204,21,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
<i class="fas fa-euro-sign text-volt"></i>
</div>
</div>
<div class="stat-value" style="color:var(--accent-volt)">{{ "%.0f"|format(stats.cout_total) }} €</div>
<span class="text-xs text-muted mt-2" style="display:inline-block">Toutes interventions</span>
</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"> <!-- Interventions récentes -->
<h3 class="font-semibold text-slate-800">Interventions récentes</h3> <div class="card" style="padding:0">
<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 class="card-header">
<span class="card-title"><i class="fas fa-clock-rotate-left text-electric" style="margin-right:0.4rem"></i>Interventions récentes</span>
<a href="/interventions/new" class="btn-primary btn-sm">
<i class="fas fa-plus"></i> Nouvelle
</a>
</div> </div>
{% if recent %} {% if recent %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-sm"> <table>
<thead class="bg-gray-50 text-slate-600 text-xs uppercase"> <thead>
<tr> <tr>
<th class="px-6 py-3 text-left">Véhicule</th> <th>Véhicule</th>
<th class="px-6 py-3 text-left">Description</th> <th>Description</th>
<th class="px-6 py-3 text-left">Statut</th> <th>Date</th>
<th class="px-6 py-3 text-left">Coût total</th> <th>Kilométrage</th>
<th>Pièces</th>
<th class="text-right">Total</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody>
{% for i in recent %} {% for i in recent %}
<tr class="hover:bg-gray-50"> <tr style="cursor:pointer" onclick="window.location='/interventions/{{ i.id }}'">
<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>
<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> <span class="font-semibold mono" style="color:var(--accent-electric);font-size:0.85rem">{{ i.immatriculation }}</span>
<td class="px-6 py-4"> <br>
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span> <span class="text-xs text-muted">{{ i.marque }} {{ i.modele }}</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> </td>
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %} <td style="max-width:280px">
<span class="truncate" style="display:block;max-width:280px">{{ i.description[:60] }}{% if i.description|length > 60 %}…{% endif %}</span>
</td>
<td class="text-xs text-secondary">{{ i.date_intervention or '—' }}</td>
<td class="text-xs text-secondary mono">{{ (i.kilometrage_intervention|string + ' km') if i.kilometrage_intervention else '—' }}</td>
<td>
<span class="text-xs text-secondary">{{ i.nb_pieces }} pièce(s)</span>
<br>
<span class="text-xs text-muted">{{ "%.0f"|format(i.cout_pieces) }} €</span>
</td>
<td class="text-right">
<span class="font-semibold" style="color:var(--accent-volt)">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</span>
</td> </td>
<td class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400"> <div class="empty-state">
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention pour l'instant</p> <i class="fas fa-tools"></i>
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a> <p>Aucune intervention pour l'instant</p>
<a href="/interventions/new" class="link-action">Créer la première →</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
+142 -81
View File
@@ -1,142 +1,203 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Intervention #{{ intervention.id }} - GarageManager{% endblock %} {% block title %}Intervention GarageManager{% endblock %}
{% block content %} {% 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> <a href="/vehicules/{{ intervention.vid }}" class="back-link"><i class="fas fa-arrow-left"></i> Retour au véhicule</a>
<div class="flex justify-between items-start">
<!-- Header -->
<div class="flex justify-between items-start mb-8">
<div> <div>
<h2 class="text-2xl font-bold text-slate-800">{{ intervention.description }}</h2> <h1 class="page-title" style="max-width:600px">{{ intervention.description }}</h1>
<p class="text-slate-500 mt-1">{{ intervention.immatriculation }} — {{ intervention.marque }} {{ intervention.modele }}</p> <p class="page-subtitle" style="margin-top:0.4rem">
<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> <a href="/vehicules/{{ intervention.vid }}" class="mono" style="color:var(--accent-electric);text-decoration:none;font-size:0.95rem">{{ intervention.immatriculation }}</a>
<span class="text-muted"> — {{ intervention.marque }} {{ intervention.modele }}</span>
</p>
<div class="flex gap-4 mt-2" style="flex-wrap:wrap">
{% if intervention.date_intervention %}
<span class="text-xs text-secondary"><i class="fas fa-calendar mr-1"></i>{{ intervention.date_intervention }}</span>
{% endif %}
{% if intervention.kilometrage_intervention %}
<span class="text-xs text-secondary mono"><i class="fas fa-tachometer-alt mr-1"></i>{{ intervention.kilometrage_intervention }} km</span>
{% endif %}
{% if intervention.technicien %}
<span class="text-xs text-secondary"><i class="fas fa-user-cog mr-1"></i>{{ intervention.technicien }}</span>
{% endif %}
</div> </div>
<div class="flex gap-2 items-center"> </div>
{% if intervention.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm">⏳ En attente</span> <div class="flex gap-2">
{% elif intervention.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm">🔧 En cours</span> <a href="/interventions/{{ intervention.id }}/edit" class="btn-secondary btn-sm">
{% else %}<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm">✅ Terminé</span>{% endif %} <i class="fas fa-edit"></i> Modifier
<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> </a>
<form method="post" action="/interventions/{{ intervention.id }}/delete" onsubmit="return confirm('Supprimer cette intervention ?')"> <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> <button class="btn-danger btn-sm">
<i class="fas fa-trash"></i> Supprimer
</button>
</form> </form>
</div> </div>
</div>
</div> </div>
<!-- Résumé coûts --> <!-- Résumé coûts -->
<div class="grid grid-cols-3 gap-4 mb-6"> <div class="grid grid-3 mb-6">
<div class="bg-white rounded-xl shadow p-5"> <div class="card" style="cursor:default">
<p class="text-xs text-slate-500">Main d'œuvre</p> <div class="stat-label mb-2">Main d'œuvre</div>
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre) }} €</p> <div class="stat-value" style="color:var(--accent-electric)">{{ "%.2f"|format(intervention.cout_main_oeuvre) }} €</div>
</div> </div>
<div class="bg-white rounded-xl shadow p-5"> <div class="card" style="cursor:default">
<p class="text-xs text-slate-500">Pièces</p> <div class="stat-label mb-2">Pièces</div>
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_pieces) }} €</p> <div class="stat-value" style="color:var(--text-secondary)">{{ "%.2f"|format(intervention.cout_pieces) }} €</div>
</div> </div>
<div class="bg-orange-50 rounded-xl shadow p-5 border border-orange-200"> <div class="card" style="cursor:default;border-color:rgba(250,204,21,0.25)">
<p class="text-xs text-orange-600">Total</p> <div class="stat-label mb-2" style="color:var(--accent-volt)">Total</div>
<p class="text-2xl font-bold text-orange-600 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre + intervention.cout_pieces) }} €</p> <div class="stat-value" style="color:var(--accent-volt)">{{ "%.2f"|format(intervention.cout_main_oeuvre + intervention.cout_pieces) }} €</div>
</div> </div>
</div> </div>
{% if intervention.notes %} {% if intervention.notes %}
<div class="bg-white rounded-xl shadow p-5 mb-6"> <div class="card mb-6">
<h3 class="font-semibold text-slate-800 mb-2">📋 Notes</h3> <h3 class="card-title mb-3"><i class="fas fa-clipboard-list text-secondary" style="margin-right:0.4rem"></i>Notes</h3>
<p class="text-sm text-slate-600 whitespace-pre-wrap">{{ intervention.notes }}</p> <p class="text-sm" style="white-space:pre-wrap;color:var(--text-secondary)">{{ intervention.notes }}</p>
</div> </div>
{% endif %} {% endif %}
<!-- Pièces --> <!-- ── Pièces utilisées ── -->
<div class="bg-white rounded-xl shadow"> <div class="card" style="padding:0">
<div class="p-5 border-b border-gray-100 flex justify-between items-center"> <div class="card-header">
<h3 class="font-semibold text-slate-800">🔩 Pièces utilisées ({{ pieces|length }})</h3> <span class="card-title">
<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> <i class="fas fa-cog text-volt" style="margin-right:0.4rem"></i>
Pièces utilisées
<span class="badge badge-warning ml-2">{{ pieces_utilisees|length }}</span>
</span>
<button onclick="toggleAddPiece()" class="btn-primary btn-sm">
<i class="fas fa-plus"></i> Ajouter une pièce
</button>
</div> </div>
<!-- Formulaire ajout pièce --> <!-- Form ajout pièce -->
<div id="add-piece-form" class="hidden p-5 border-b border-gray-100 bg-gray-50"> <div id="add-piece-form" style="display:none;background:var(--bg-secondary);border-bottom:1px solid var(--border);padding:1.25rem 1.5rem">
<form method="post" action="/interventions/{{ intervention.id }}/pieces/add" enctype="multipart/form-data"> <form method="post" action="/interventions/{{ intervention.id }}/pieces/add" enctype="multipart/form-data">
<div class="grid grid-cols-2 gap-3 mb-3">
{% if pieces_connues %}
<div class="mb-4">
<p class="text-xs font-semibold uppercase" style="color:var(--text-muted);letter-spacing:0.06em;margin-bottom:0.5rem">
🔄 Réutiliser une pièce connue sur ce véhicule
</p>
<div class="scroll-list">
{% for p in pieces_connues %}
<label class="scroll-list-item">
<input type="radio" name="_prefill" value="{{ loop.index0 }}"
data-nom="{{ p.nom }}" data-ref="{{ p.reference or '' }}" data-marque="{{ p.marque or '' }}" data-prix="{{ p.prix }}"
onchange="prefillPiece(this)"
style="width:auto;background:none;border:none;padding:0">
<span class="font-medium text-sm" style="flex:1">{{ p.nom }}</span>
{% if p.reference %}<span class="mono text-xs text-muted">{{ p.reference }}</span>{% endif %}
{% if p.marque %}<span class="text-xs text-secondary">{{ p.marque }}</span>{% endif %}
<span class="font-semibold text-sm text-electric mono">{{ "%.2f"|format(p.prix) }} €</span>
</label>
{% endfor %}
</div>
<p class="text-xs text-muted mt-1">Les infos se pré-remplissent automatiquement, tu peux les modifier.</p>
</div>
{% endif %}
<div class="grid grid-2 mb-3">
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Nom de la pièce *</label> <label>Nom *</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"> <input id="p-nom" name="nom" required placeholder="Nom de la pièce">
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Référence</label> <label>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"> <input id="p-ref" name="reference" placeholder="Ex: 06A115561B" style="font-family:'JetBrains Mono',monospace">
</div> </div>
</div> </div>
<div class="grid grid-cols-3 gap-3 mb-3"> <div class="grid grid-3 mb-3">
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Marque</label> <label>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"> <input id="p-marque" name="marque" placeholder="Bosch, NGK…">
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Prix unitaire (€)</label> <label>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"> <input id="p-prix" name="prix" type="number" step="0.01" min="0" value="0" class="mono">
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Quantité</label> <label>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"> <input name="quantite" type="number" min="1" value="1" class="mono">
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="block text-xs font-medium text-slate-600 mb-1">Photo (optionnel)</label> <label>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"> <input type="file" name="photo" accept="image/*" style="background:none;border:none;padding:0.25rem 0;color:var(--text-secondary)">
</div> </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> <button type="submit" class="btn-primary btn-sm">
<i class="fas fa-plus"></i> Ajouter la pièce
</button>
</form> </form>
</div> </div>
<!-- Liste des pièces --> {% if pieces_utilisees %}
{% if pieces %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-sm"> <table>
<thead class="bg-gray-50 text-slate-600 text-xs uppercase"> <thead>
<tr> <tr>
<th class="px-5 py-3 text-left">Photo</th> <th>Photo</th>
<th class="px-5 py-3 text-left">Pièce</th> <th>Pièce</th>
<th class="px-5 py-3 text-left">Référence</th> <th>Référence</th>
<th class="px-5 py-3 text-left">Marque</th> <th>Marque</th>
<th class="px-5 py-3 text-left">Qté</th> <th class="text-center">Qté</th>
<th class="px-5 py-3 text-left">Prix unit.</th> <th class="text-right">Prix unit.</th>
<th class="px-5 py-3 text-left">Total</th> <th class="text-right">Total</th>
<th class="px-5 py-3 text-left"></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody>
{% for p in pieces %} {% for p in pieces_utilisees %}
<tr class="hover:bg-gray-50"> <tr>
<td class="px-5 py-3"> <td>
{% if p.photo %} {% 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')"> <img src="{{ p.photo }}" class="photo-thumb" style="width:48px;height:48px" onclick="window.open('{{ p.photo }}','_blank')">
{% else %} {% 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> <div class="photo-placeholder" style="width:48px;height:48px"><i class="fas fa-cog"></i></div>
{% endif %} {% endif %}
</td> </td>
<td class="px-5 py-3 font-medium text-slate-800">{{ p.nom }}</td> <td class="font-medium">{{ p.nom }}</td>
<td class="px-5 py-3 text-slate-500 font-mono text-xs">{{ p.reference or '-' }}</td> <td class="mono text-xs text-muted">{{ p.reference or '' }}</td>
<td class="px-5 py-3 text-slate-600">{{ p.marque or '-' }}</td> <td class="text-secondary text-sm">{{ p.marque or '' }}</td>
<td class="px-5 py-3 text-slate-600">{{ p.quantite }}</td> <td class="text-center text-secondary mono">{{ p.quantite }}</td>
<td class="px-5 py-3 text-slate-600">{{ "%.2f"|format(p.prix) }} €</td> <td class="text-right mono text-sm">{{ "%.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="text-right font-semibold mono" style="color:var(--accent-volt)">{{ "%.2f"|format(p.prix * p.quantite) }} €</td>
<td class="px-5 py-3"> <td>
<form method="post" action="/interventions/{{ intervention.id }}/pieces/{{ p.id }}/delete" onsubmit="return confirm('Supprimer cette pièce ?')"> <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> <button class="link-danger"></button>
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr class="bg-orange-50"> <tr style="background:var(--bg-secondary)">
<td colspan="6" class="px-5 py-3 text-right font-semibold text-slate-700">Total pièces</td> <td colspan="6" class="text-right font-semibold text-secondary text-sm">Total pièces</td>
<td colspan="2" class="px-5 py-3 font-bold text-orange-600">{{ "%.2f"|format(intervention.cout_pieces) }} €</td> <td class="text-right font-bold mono" style="color:var(--accent-volt)">{{ "%.2f"|format(intervention.cout_pieces) }} €</td>
<td></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400 text-sm"> <div class="empty-state">
Aucune pièce enregistrée.<br> <i class="fas fa-cog"></i>
<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> <p>Aucune pièce pour cette intervention</p>
<button onclick="toggleAddPiece()" class="link-action">Ajouter →</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<script>
function toggleAddPiece() {
const f = document.getElementById('add-piece-form');
f.style.display = f.style.display === 'none' ? 'block' : 'none';
}
function prefillPiece(radio) {
document.getElementById('p-nom').value = radio.dataset.nom;
document.getElementById('p-ref').value = radio.dataset.ref;
document.getElementById('p-marque').value = radio.dataset.marque;
document.getElementById('p-prix').value = radio.dataset.prix;
}
</script>
{% endblock %} {% endblock %}
+44 -27
View File
@@ -1,51 +1,68 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% if intervention %}Modifier{% else %}Nouvelle{% endif %} intervention - GarageManager{% endblock %} {% block title %}{% if intervention %}Modifier{% else %}Nouvelle{% endif %} intervention GarageManager{% endblock %}
{% block content %} {% 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> <a href="/interventions" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux interventions</a>
<h2 class="text-2xl font-bold text-slate-800">{% if intervention %}Modifier l'intervention{% else %}Nouvelle intervention{% endif %}</h2>
<div class="page-header">
<h1 class="page-title">
<i class="fas fa-{% if intervention %}edit{% else %}plus-circle{% endif %} text-electric" style="margin-right:0.5rem"></i>
{% if intervention %}Modifier l'intervention{% else %}Nouvelle intervention{% endif %}
</h1>
</div> </div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4"> <div class="card" style="max-width:680px">
<form method="post" style="display:flex;flex-direction:column;gap:1.25rem">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Véhicule *</label> <label>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"> <select name="vehicule_id" required>
<option value="">-- Sélectionner un véhicule --</option> <option value=""> Sélectionner un véhicule </option>
{% for v in vehicules %} {% 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> <option value="{{ v.id }}" {% if (intervention and intervention.vehicule_id == v.id) or preselect_vehicule == v.id %}selected{% endif %}>{{ v.label }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Description *</label> <label>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> <textarea name="description" rows="2" required placeholder="Ex: Vidange + filtre à huile, Changement courroie de distribution">{{ intervention.description if intervention else '' }}</textarea>
</div> </div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-2">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Statut</label> <label>Date de l'intervention</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"> <input name="date_intervention" type="date" value="{{ intervention.date_intervention if intervention and intervention.date_intervention else '' }}">
<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>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Main d'œuvre ()</label> <label>Kilométrage (km)</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"> <input name="kilometrage_intervention" type="number" value="{{ intervention.kilometrage_intervention if intervention and intervention.kilometrage_intervention else '' }}" placeholder="85000" class="mono">
</div> </div>
</div> </div>
<div class="grid grid-2">
<div>
<label>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="mono">
</div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Technicien</label> <label>Technicien / Garage</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"> <input name="technicien" value="{{ intervention.technicien if intervention else '' }}" placeholder="Nom du garage, soi-même…">
</div> </div>
</div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Notes</label> <label>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> <textarea name="notes" rows="3" placeholder="Observations, prochaine révision, problèmes restants">{{ intervention.notes if intervention else '' }}</textarea>
</div> </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> <div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<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> <button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Enregistrer
</button>
<a href="/interventions" class="btn-secondary">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
+55 -41
View File
@@ -1,63 +1,77 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Interventions - GarageManager{% endblock %} {% block title %}Interventions GarageManager{% endblock %}
{% block content %} {% 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> <div class="page-header flex justify-between items-center">
<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>
<h1 class="page-title"><i class="fas fa-tools text-electric" style="margin-right:0.5rem"></i>Interventions</h1>
<p class="page-subtitle">{{ interventions|length }} intervention(s)</p>
</div>
<a href="/interventions/new" class="btn-primary">
<i class="fas fa-plus"></i> Nouvelle intervention
</a>
</div> </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> <div class="card" style="padding:0">
<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> <div class="overflow-x-auto">
<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 %} {% if interventions %}
<table class="w-full text-sm"> <table>
<thead class="bg-gray-50 text-slate-600 text-xs uppercase"> <thead>
<tr> <tr>
<th class="px-6 py-3 text-left">Véhicule</th> <th>Véhicule</th>
<th class="px-6 py-3 text-left">Description</th> <th>Description</th>
<th class="px-6 py-3 text-left">Statut</th> <th>Date</th>
<th class="px-6 py-3 text-left">Pièces</th> <th>Kilométrage</th>
<th class="px-6 py-3 text-left">Total</th> <th>Pièces</th>
<th class="px-6 py-3 text-left">Date</th> <th class="text-right">Total</th>
<th class="px-6 py-3 text-left">Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody>
{% for i in interventions %} {% for i in interventions %}
<tr class="hover:bg-gray-50"> <tr>
<td class="px-6 py-4 font-medium text-slate-800"> <td>
<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> <a href="/vehicules/{{ i.vehicule_id }}" style="text-decoration:none">
<span class="font-semibold mono" style="color:var(--accent-electric);font-size:0.85rem">{{ i.immatriculation }}</span>
<br>
<span class="text-xs text-muted">{{ i.marque }} {{ i.modele }}</span>
</a>
</td> </td>
<td class="px-6 py-4 text-slate-600 max-w-xs"> <td style="max-width:260px">
<a href="/interventions/{{ i.id }}" class="hover:text-orange-500">{{ i.description[:60] }}{% if i.description|length > 60 %}...{% endif %}</a> <a href="/interventions/{{ i.id }}" class="font-medium" style="text-decoration:none;color:var(--text-primary)">
{{ i.description[:60] }}{% if i.description|length > 60 %}…{% endif %}
</a>
</td> </td>
<td class="px-6 py-4"> <td class="text-secondary text-xs">{{ i.date_intervention or '—' }}</td>
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span> <td class="mono text-xs text-secondary">{{ (i.kilometrage_intervention|string + ' km') if i.kilometrage_intervention else '—' }}</td>
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span> <td>
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %} <span class="text-xs text-secondary">{{ i.nb_pieces }} pièce(s)</span><br>
<span class="text-xs text-muted">{{ "%.2f"|format(i.cout_pieces) }} €</span>
</td> </td>
<td class="px-6 py-4 text-slate-500 text-xs">{{ "%.2f"|format(i.cout_pieces) }} €</td> <td class="text-right font-semibold" style="color:var(--accent-volt)">
<td class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td> {{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €
<td class="px-6 py-4 text-slate-400 text-xs">{{ i.created_at[:10] }}</td> </td>
<td class="px-6 py-4 flex gap-3"> <td>
<a href="/interventions/{{ i.id }}" class="text-blue-500 hover:underline">Voir</a> <div class="flex gap-3">
<a href="/interventions/{{ i.id }}/edit" class="text-orange-500 hover:underline">Modifier</a> <a href="/interventions/{{ i.id }}" class="link-action">Voir</a>
<form method="post" action="/interventions/{{ i.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ?')"> <a href="/interventions/{{ i.id }}/edit" class="link-edit">Modifier</a>
<button class="text-red-500 hover:underline">Supprimer</button> <form method="post" action="/interventions/{{ i.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer cette intervention ?')">
<button class="link-danger">Supprimer</button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400"> <div class="empty-state">
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention trouvée</p> <i class="fas fa-tools"></i>
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a> <p>Aucune intervention pour l'instant</p>
<a href="/interventions/new" class="link-action">Créer la première →</a>
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}
+210 -90
View File
@@ -1,128 +1,248 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ vehicule.immatriculation }} - GarageManager{% endblock %} {% block title %}{{ vehicule.immatriculation }} GarageManager{% endblock %}
{% block content %} {% 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> <a href="/vehicules" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
<div class="flex justify-between items-start">
<!-- Header véhicule -->
<div class="flex justify-between items-start mb-8">
<div> <div>
<h2 class="text-2xl font-bold text-slate-800">{{ vehicule.immatriculation }}</h2> <h1 class="page-title mono" style="color:var(--accent-electric);font-size:2rem;letter-spacing:0.06em">{{ vehicule.immatriculation }}</h1>
<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> <p class="page-subtitle" style="margin-top:0.35rem">
{% if vehicule.vin %}<p class="text-xs font-mono text-slate-400 mt-1">VIN : {{ vehicule.vin }}</p>{% endif %} {{ vehicule.marque }} {{ vehicule.modele }}
{% if vehicule.annee %} — {{ vehicule.annee }}{% endif %}
{% if vehicule.kilometrage %} — <span class="mono">{{ vehicule.kilometrage }} km</span>{% endif %}
</p>
{% if vehicule.vin %}
<p class="text-xs text-muted mono" style="margin-top:0.35rem">VIN : {{ vehicule.vin }}</p>
{% endif %}
</div> </div>
<div class="flex gap-2"> <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="/interventions/new?vehicule_id={{ vehicule.id }}" class="btn-primary btn-sm">
<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> <i class="fas fa-plus"></i> Intervention
<form method="post" action="/vehicules/{{ vehicule.id }}/delete" onsubmit="return confirm('Supprimer ce véhicule ?')"> </a>
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button> <a href="/vehicules/{{ vehicule.id }}/edit" class="btn-secondary btn-sm">
<i class="fas fa-edit"></i> Modifier
</a>
<form method="post" action="/vehicules/{{ vehicule.id }}/delete" onsubmit="return confirm('Supprimer ce véhicule et toutes ses données ?')">
<button class="btn-danger btn-sm">
<i class="fas fa-trash"></i> Supprimer
</button>
</form> </form>
</div> </div>
</div>
</div> </div>
<!-- Infos / Notes --> <!-- ── Historique interventions ── -->
<div class="bg-white rounded-xl shadow mb-6"> <div class="card mb-6" style="padding:0" id="interventions">
<div class="p-5 border-b border-gray-100 flex justify-between items-center"> <div class="card-header">
<h3 class="font-semibold text-slate-800">📎 Infos & Notes</h3> <span class="card-title">
<button onclick="document.getElementById('add-info-form').classList.toggle('hidden')" class="text-orange-500 text-sm hover:underline">+ Ajouter</button> <i class="fas fa-tools text-electric" style="margin-right:0.4rem"></i>
Historique interventions
<span class="badge badge-info ml-2">{{ interventions|length }}</span>
</span>
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="link-action text-xs">+ Nouvelle</a>
</div> </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"> {% if interventions %}
<div class="grid grid-cols-3 gap-3"> <div class="overflow-x-auto">
<table>
<thead>
<tr>
<th>Description</th>
<th>Date</th>
<th>Kilométrage</th>
<th class="text-right">Pièces</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
{% for i in interventions %}
<tr style="cursor:pointer" onclick="window.location='/interventions/{{ i.id }}'">
<td>
<p class="font-medium">{{ i.description }}</p>
{% if i.technicien %}<p class="text-xs text-muted mt-1">{{ i.technicien }}</p>{% endif %}
</td>
<td class="text-secondary text-sm">{{ i.date_intervention or '—' }}</td>
<td class="mono text-sm text-secondary">{{ (i.kilometrage_intervention|string + ' km') if i.kilometrage_intervention else '—' }}</td>
<td class="text-right">
<span class="text-xs text-secondary">{{ i.nb_pieces }} pce(s)</span><br>
<span class="text-xs text-muted">{{ "%.2f"|format(i.cout_pieces) }} €</span>
</td>
<td class="text-right font-semibold" style="color:var(--accent-volt)">
{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr style="background:var(--bg-secondary)">
<td colspan="4" class="text-right text-secondary font-semibold text-sm" style="padding:0.75rem 1rem">Total véhicule</td>
<td class="text-right font-bold" style="color:var(--accent-volt);padding:0.75rem 1rem">
{{ "%.0f"|format(interventions|sum(attribute='cout_main_oeuvre') + interventions|sum(attribute='cout_pieces')) }} €
</td>
</tr>
</tfoot>
</table>
</div>
{% else %}
<div class="empty-state">
<i class="fas fa-tools"></i>
<p>Aucune intervention</p>
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="link-action">Créer →</a>
</div>
{% endif %}
</div>
<!-- ── Toutes les pièces ── -->
<div class="card mb-6" style="padding:0" id="pieces">
<div class="card-header">
<span class="card-title">
<i class="fas fa-cog text-volt" style="margin-right:0.4rem"></i>
Toutes les pièces utilisées
<span class="badge badge-warning ml-2">{{ toutes_pieces|length }}</span>
</span>
</div>
<p class="text-xs text-muted" style="padding:0.6rem 1.5rem;border-bottom:1px solid var(--border)">Récapitulatif de toutes les pièces de toutes les interventions — pour les retrouver et racheter facilement.</p>
{% if toutes_pieces %}
<div class="overflow-x-auto">
<table>
<thead>
<tr>
<th>Photo</th>
<th>Pièce</th>
<th>Référence</th>
<th>Marque</th>
<th>Prix unit.</th>
<th class="text-center">Qté</th>
<th>Intervention</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for p in toutes_pieces %}
<tr>
<td>
{% if p.photo %}
<img src="{{ p.photo }}" class="photo-thumb" onclick="window.open('{{ p.photo }}','_blank')">
{% else %}
<div class="photo-placeholder"><i class="fas fa-cog text-xs"></i></div>
{% endif %}
</td>
<td class="font-medium">{{ p.nom }}</td>
<td class="mono text-xs text-muted">{{ p.reference or '—' }}</td>
<td class="text-secondary text-sm">{{ p.marque or '—' }}</td>
<td class="mono text-sm">{{ "%.2f"|format(p.prix) }} €</td>
<td class="text-center text-secondary">{{ p.quantite }}</td>
<td class="text-xs text-secondary" style="max-width:200px">
<a href="/interventions/{{ p.intervention_id }}" class="link-action truncate" style="display:block;max-width:200px">
{{ p.intervention_desc[:40] }}{% if p.intervention_desc|length > 40 %}…{% endif %}
</a>
</td>
<td class="text-xs text-muted">{{ p.date_intervention or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<i class="fas fa-cog"></i>
<p>Aucune pièce enregistrée</p>
<span class="text-xs text-muted">Les pièces s'affichent ici dès qu'elles sont ajoutées dans une intervention.</span>
</div>
{% endif %}
</div>
<!-- ── Infos & Notes ── -->
<div class="card" style="padding:0" id="infos">
<div class="card-header">
<span class="card-title">
<i class="fas fa-paperclip text-secondary" style="margin-right:0.4rem"></i>
Infos &amp; Notes
</span>
<button onclick="toggleAddInfo()" class="link-action text-xs">+ Ajouter</button>
</div>
<!-- Form ajout info -->
<div id="add-info-form" style="display:none;background:var(--bg-secondary);border-bottom:1px solid var(--border);padding:1.25rem 1.5rem">
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/add" enctype="multipart/form-data">
<div class="grid grid-3 mb-3">
<div> <div>
<label class="block text-xs font-medium text-slate-600 mb-1">Type</label> <label>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"> <select name="type" id="info-type" onchange="toggleInfoType()">
<option value="texte">📝 Texte</option> <option value="texte">📝 Texte libre</option>
<option value="url">🔗 URL / Lien</option> <option value="url">🔗 Lien / URL</option>
<option value="image">🖼️ Image</option> <option value="image">🖼️ Image / Photo</option>
</select> </select>
</div> </div>
<div class="col-span-2"> <div style="grid-column:span 2">
<label class="block text-xs font-medium text-slate-600 mb-1">Titre (optionnel)</label> <label>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"> <input name="titre" placeholder="Ex: Forum, Fiche technique, Photo moteur…">
</div> </div>
</div> </div>
<div class="mb-3">
<div id="input-texte"> <div id="input-texte">
<label class="block text-xs font-medium text-slate-600 mb-1">Contenu *</label> <label>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> <textarea name="contenu" rows="2" placeholder="Notes, infos techniques"></textarea>
</div> </div>
<div id="input-url" class="hidden"> <div id="input-url" style="display:none">
<label class="block text-xs font-medium text-slate-600 mb-1">URL *</label> <label>URL</label>
<input name="contenu" placeholder="https://..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"> <input name="contenu" placeholder="https://">
</div> </div>
<div id="input-image" class="hidden"> <div id="input-image" style="display:none">
<label class="block text-xs font-medium text-slate-600 mb-1">Photo *</label> <label>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="file" name="photo" accept="image/*" style="background:none;border:none;padding:0.25rem 0;color:var(--text-secondary)">
<input type="hidden" name="contenu" value="photo"> <input type="hidden" name="contenu" value="photo">
</div> </div>
<button type="submit" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">Enregistrer</button> </div>
<button type="submit" class="btn-primary btn-sm">
<i class="fas fa-save"></i> Enregistrer
</button>
</form> </form>
</div> </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 --> {% if infos %}
<div class="bg-white rounded-xl shadow"> <div>
<div class="p-5 border-b border-gray-100 flex justify-between items-center"> {% for info in infos %}
<h3 class="font-semibold text-slate-800">🔧 Interventions ({{ interventions|length }})</h3> <div style="padding:1rem 1.5rem;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-start;transition:background 0.15s" onmouseover="this.style.background='rgba(0,212,255,0.03)'" onmouseout="this.style.background=''">
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 text-sm hover:underline">+ Nouvelle intervention</a> <div style="flex:1">
</div> {% if info.titre %}
{% if interventions %} <p class="text-xs font-semibold uppercase" style="letter-spacing:0.06em;color:var(--text-muted);margin-bottom:0.3rem">{{ info.titre }}</p>
<div class="divide-y divide-gray-100"> {% endif %}
{% for i in interventions %} {% if info.type == 'texte' %}
<div class="px-5 py-4 hover:bg-gray-50"> <p class="text-sm" style="white-space:pre-wrap;color:var(--text-primary)">{{ info.contenu }}</p>
<div class="flex justify-between items-start"> {% elif info.type == 'url' %}
<div class="flex-1"> <a href="{{ info.contenu }}" target="_blank" class="link-action text-sm" style="word-break:break-all">🔗 {{ info.contenu }}</a>
<a href="/interventions/{{ i.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ i.description }}</a> {% elif info.type == 'image' %}
{% 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 %} <img src="{{ info.contenu }}" style="max-height:160px;border-radius:8px;border:1px solid var(--border);margin-top:0.25rem;cursor:pointer" onclick="window.open('{{ info.contenu }}','_blank')">
<p class="text-xs text-slate-400 mt-1">{{ i.created_at[:10] }} • {{ i.nb_pieces }} pièce(s)</p> {% endif %}
</div> <p class="text-xs text-muted" style="margin-top:0.35rem">{{ info.created_at[:10] }}</p>
<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>
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/{{ info.id }}/delete" style="margin-left:1rem" onsubmit="return confirm('Supprimer ?')">
<button class="link-danger"></button>
</form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="p-6 text-center text-slate-400 text-sm"> <div class="empty-state">
Aucune intervention.<br> <i class="fas fa-paperclip"></i>
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 hover:underline mt-1 inline-block">Créer la première →</a> <p>Aucune info pour l'instant</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<script> <script>
function toggleAddInfo() {
const f = document.getElementById('add-info-form');
f.style.display = f.style.display === 'none' ? 'block' : 'none';
}
function toggleInfoType() { function toggleInfoType() {
const type = document.getElementById('info-type').value; const type = document.getElementById('info-type').value;
document.getElementById('input-texte').classList.toggle('hidden', type !== 'texte'); document.getElementById('input-texte').style.display = type === 'texte' ? 'block' : 'none';
document.getElementById('input-url').classList.toggle('hidden', type !== 'url'); document.getElementById('input-url').style.display = type === 'url' ? 'block' : 'none';
document.getElementById('input-image').classList.toggle('hidden', type !== 'image'); document.getElementById('input-image').style.display = type === 'image' ? 'block' : 'none';
} }
</script> </script>
{% endblock %} {% endblock %}
+38 -23
View File
@@ -1,44 +1,59 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% if vehicule %}Modifier{% else %}Nouveau{% endif %} véhicule - GarageManager{% endblock %} {% block title %}{% if vehicule %}Modifier{% else %}Nouveau{% endif %} véhicule GarageManager{% endblock %}
{% block content %} {% 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> <a href="/vehicules" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
<h2 class="text-2xl font-bold text-slate-800">{% if vehicule %}Modifier le véhicule{% else %}Nouveau véhicule{% endif %}</h2>
<div class="page-header">
<h1 class="page-title">
<i class="fas fa-{% if vehicule %}edit{% else %}plus-circle{% endif %} text-electric" style="margin-right:0.5rem"></i>
{% if vehicule %}Modifier le véhicule{% else %}Nouveau véhicule{% endif %}
</h1>
</div> </div>
<div class="bg-white rounded-xl shadow p-8 max-w-2xl">
<form method="post" class="space-y-4"> <div class="card" style="max-width:640px">
<form method="post" class="space" style="display:flex;flex-direction:column;gap:1.25rem">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Immatriculation *</label> <label>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"> <input name="immatriculation" value="{{ vehicule.immatriculation if vehicule else '' }}" required placeholder="AB-123-CD" style="text-transform:uppercase;font-family:'JetBrains Mono',monospace;font-size:1rem;letter-spacing:0.05em">
</div> </div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-2">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Marque *</label> <label>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"> <input name="marque" value="{{ vehicule.marque if vehicule else '' }}" required placeholder="Renault, Peugeot…">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Modèle *</label> <label>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"> <input name="modele" value="{{ vehicule.modele if vehicule else '' }}" required placeholder="Clio, 308…">
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-2">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Année</label> <label>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"> <input name="annee" type="number" value="{{ vehicule.annee if vehicule else '' }}" min="1900" max="2030" placeholder="2019">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Kilométrage</label> <label>Kilométrage (km)</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"> <input name="kilometrage" type="number" value="{{ vehicule.kilometrage if vehicule else '' }}" placeholder="85000">
</div> </div>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">VIN (numéro de châssis)</label> <label>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"> <input name="vin" value="{{ vehicule.vin if vehicule else '' }}" placeholder="17 caractères" maxlength="17" style="text-transform:uppercase;font-family:'JetBrains Mono',monospace;letter-spacing:0.08em">
<span class="text-xs text-muted" style="margin-top:0.3rem;display:block">Format : 17 caractères alphanumériques</span>
</div> </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> <div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<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> <button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Enregistrer
</button>
<a href="/vehicules" class="btn-secondary">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
+59 -37
View File
@@ -1,58 +1,80 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Véhicules - GarageManager{% endblock %} {% block title %}Véhicules GarageManager{% endblock %}
{% block content %} {% 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> <div class="page-header flex justify-between items-center">
<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> <h1 class="page-title"><i class="fas fa-car text-electric" style="margin-right:0.5rem"></i>Véhicules</h1>
<div class="bg-white rounded-xl shadow"> <p class="page-subtitle">{{ vehicules|length }} véhicule(s) enregistré(s)</p>
<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>
<a href="/vehicules/new" class="btn-primary">
<i class="fas fa-plus"></i> Nouveau véhicule
</a>
</div>
<!-- Search -->
<div class="card mb-4" style="padding:1rem 1.25rem">
<form method="get" class="flex gap-2">
<input name="q" value="{{ q }}" placeholder="Immatriculation, marque, modèle, VIN…" style="flex:1">
<button type="submit" class="btn-secondary" style="width:auto;white-space:nowrap">
<i class="fas fa-search"></i> Rechercher
</button>
{% if q %}
<a href="/vehicules" class="btn-secondary" style="width:auto"></a>
{% endif %}
</form>
</div>
<!-- Table -->
<div class="card" style="padding:0">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
{% if vehicules %} {% if vehicules %}
<table class="w-full text-sm"> <table>
<thead class="bg-gray-50 text-slate-600 text-xs uppercase"> <thead>
<tr> <tr>
<th class="px-6 py-3 text-left">Immatriculation</th> <th>Immatriculation</th>
<th class="px-6 py-3 text-left">Marque / Modèle</th> <th>Marque / Modèle</th>
<th class="px-6 py-3 text-left">Année</th> <th>Année</th>
<th class="px-6 py-3 text-left">VIN</th> <th>VIN</th>
<th class="px-6 py-3 text-left">Kilométrage</th> <th>Kilométrage</th>
<th class="px-6 py-3 text-left">Interventions</th> <th>Interventions</th>
<th class="px-6 py-3 text-left">Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody>
{% for v in vehicules %} {% for v in vehicules %}
<tr class="hover:bg-gray-50"> <tr>
<td class="px-6 py-4 font-bold text-slate-800">{{ v.immatriculation }}</td> <td>
<td class="px-6 py-4 font-medium text-slate-700">{{ v.marque }} {{ v.modele }}</td> <a href="/vehicules/{{ v.id }}" class="font-bold mono" style="color:var(--accent-electric);font-size:0.95rem;text-decoration:none">{{ v.immatriculation }}</a>
<td class="px-6 py-4 text-slate-500">{{ v.annee or '-' }}</td> </td>
<td class="px-6 py-4 text-slate-400 text-xs font-mono">{{ v.vin or '-' }}</td> <td class="font-medium">{{ v.marque }} {{ v.modele }}</td>
<td class="px-6 py-4 text-slate-600">{{ (v.kilometrage|string + ' km') if v.kilometrage else '-' }}</td> <td class="text-secondary">{{ v.annee or '' }}</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="mono text-xs text-muted">{{ v.vin or '—' }}</td>
<td class="px-6 py-4 flex gap-3"> <td class="mono text-sm">{{ (v.kilometrage|string + ' km') if v.kilometrage else '—' }}</td>
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline">Voir</a> <td>
<a href="/vehicules/{{ v.id }}/edit" class="text-orange-500 hover:underline">Modifier</a> <span class="badge badge-info">{{ v.nb_interventions }}</span>
<form method="post" action="/vehicules/{{ v.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce véhicule ?')"> </td>
<button class="text-red-500 hover:underline">Supprimer</button> <td>
<div class="flex gap-3">
<a href="/vehicules/{{ v.id }}" class="link-action">Voir</a>
<a href="/vehicules/{{ v.id }}/edit" class="link-edit">Modifier</a>
<form method="post" action="/vehicules/{{ v.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer ce véhicule et toutes ses données ?')">
<button class="link-danger">Supprimer</button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<div class="p-8 text-center text-slate-400"> <div class="empty-state">
<i class="fas fa-car text-3xl mb-3"></i><p>Aucun véhicule trouvé</p> <i class="fas fa-car"></i>
<a href="/vehicules/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a> <p>Aucun véhicule trouvé</p>
<a href="/vehicules/new" class="link-action">Ajouter le premier →</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}