feat: redesign dark futuriste (Luca) — thème cyan/volt, sidebar dark, JetBrains Mono + README
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
[](https://fastapi.tiangolo.com)
|
[](https://python.org)
|
||||||
[](https://python.org)
|
[](https://fastapi.tiangolo.com)
|
||||||
[](https://docker.com)
|
[](https://docs.docker.com/compose/)
|
||||||
[](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
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
|
||||||
<div>
|
<!-- Header -->
|
||||||
<h2 class="text-2xl font-bold text-slate-800">{{ client.nom }} {{ client.prenom }}</h2>
|
<div class="flex justify-between items-start mb-8">
|
||||||
<div class="flex flex-wrap gap-4 mt-2 text-sm text-slate-500">
|
<div>
|
||||||
{% if client.telephone %}<span><i class="fas fa-phone mr-1"></i>{{ client.telephone }}</span>{% endif %}
|
<h1 class="page-title">{{ client.nom }} {{ client.prenom }}</h1>
|
||||||
{% if client.email %}<span><i class="fas fa-envelope mr-1"></i>{{ client.email }}</span>{% endif %}
|
<div class="flex gap-4 mt-2" style="flex-wrap:wrap">
|
||||||
{% if client.adresse %}<span><i class="fas fa-map-marker-alt mr-1"></i>{{ client.adresse }}</span>{% endif %}
|
{% if client.telephone %}
|
||||||
</div>
|
<span class="text-sm text-secondary"><i class="fas fa-phone mr-1 text-electric"></i>{{ client.telephone }}</span>
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="flex gap-2">
|
{% if client.email %}
|
||||||
<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>
|
<span class="text-sm text-secondary"><i class="fas fa-envelope mr-1 text-electric"></i>{{ client.email }}</span>
|
||||||
<form method="post" action="/clients/{{ client.id }}/delete" onsubmit="return confirm('Supprimer ce client et tous ses véhicules ?')">
|
{% endif %}
|
||||||
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
{% if client.adresse %}
|
||||||
</form>
|
<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">
|
||||||
|
<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 ?')">
|
||||||
|
<button class="btn-danger btn-sm">
|
||||||
|
<i class="fas fa-trash"></i> Supprimer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow">
|
|
||||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
<!-- 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">
|
<div>
|
||||||
{% for v in vehicules %}
|
{% for v in vehicules %}
|
||||||
<div class="px-6 py-4 flex justify-between items-center hover:bg-gray-50">
|
<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=''">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-slate-800">{{ v.immatriculation }}</span>
|
<span class="font-semibold mono" style="color:var(--accent-electric)">{{ v.immatriculation }}</span>
|
||||||
<span class="text-slate-500 ml-2">{{ v.marque }} {{ v.modele }}</span>
|
<span class="text-secondary ml-2">{{ v.marque }} {{ v.modele }}</span>
|
||||||
{% if v.annee %}<span class="text-slate-400 text-sm ml-2">({{ v.annee }})</span>{% endif %}
|
{% if v.annee %}<span class="text-muted text-xs ml-2">({{ v.annee }})</span>{% endif %}
|
||||||
{% if v.kilometrage %}<span class="text-slate-400 text-sm ml-2">— {{ v.kilometrage }} km</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
@@ -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 %}
|
||||||
|
|||||||
+55
-34
@@ -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>
|
||||||
</form>
|
<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>
|
||||||
|
</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 %}
|
||||||
|
|||||||
+89
-59
@@ -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 class="stat-label">Véhicules</div>
|
||||||
|
<div style="width:38px;height:38px;background:rgba(0,212,255,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
|
||||||
|
<i class="fas fa-car text-electric"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/vehicules" class="text-xs text-purple-500 hover:underline mt-3 inline-block">Voir tous →</a>
|
<div class="stat-value">{{ stats.nb_vehicules }}</div>
|
||||||
|
<a href="/vehicules" class="link-action text-xs mt-2" style="display:inline-block">Voir tous →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="card" style="cursor:default">
|
||||||
<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="flex items-center justify-between mb-3">
|
||||||
<div class="bg-yellow-100 p-3 rounded-full"><i class="fas fa-clock text-yellow-500 text-xl"></i></div>
|
<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>
|
||||||
<a href="/interventions?statut=en_attente" class="text-xs text-yellow-500 hover:underline mt-3 inline-block">Voir →</a>
|
<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>
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="card" style="cursor:default">
|
||||||
<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="flex items-center justify-between mb-3">
|
||||||
<div class="bg-blue-100 p-3 rounded-full"><i class="fas fa-spinner text-blue-500 text-xl"></i></div>
|
<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>
|
||||||
<a href="/interventions?statut=en_cours" class="text-xs text-blue-500 hover:underline mt-3 inline-block">Voir →</a>
|
<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>
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="card" style="cursor:default;border-color:rgba(250,204,21,0.2)">
|
||||||
<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="flex items-center justify-between mb-3">
|
||||||
<div class="bg-green-100 p-3 rounded-full"><i class="fas fa-check-circle text-green-500 text-xl"></i></div>
|
<div class="stat-label" style="color:var(--accent-volt)">Total dépensé</div>
|
||||||
</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">
|
||||||
<a href="/interventions?statut=termine" class="text-xs text-green-500 hover:underline mt-3 inline-block">Voir →</a>
|
<i class="fas fa-euro-sign text-volt"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div><p class="text-sm text-slate-500">Coût pièces</p><p class="text-3xl font-bold text-slate-800 mt-1">{{ "%.0f"|format(stats.cout_pieces) }} €</p></div>
|
|
||||||
<div class="bg-red-100 p-3 rounded-full"><i class="fas fa-cog text-red-400 text-xl"></i></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div><p class="text-sm text-slate-500">Total dépensé</p><p class="text-3xl font-bold text-orange-500 mt-1">{{ "%.0f"|format(stats.cout_total) }} €</p></div>
|
|
||||||
<div class="bg-orange-100 p-3 rounded-full"><i class="fas fa-euro-sign text-orange-500 text-xl"></i></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div 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 %}
|
||||||
|
|||||||
@@ -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">
|
|
||||||
<div>
|
<!-- Header -->
|
||||||
<h2 class="text-2xl font-bold text-slate-800">{{ intervention.description }}</h2>
|
<div class="flex justify-between items-start mb-8">
|
||||||
<p class="text-slate-500 mt-1">{{ intervention.immatriculation }} — {{ intervention.marque }} {{ intervention.modele }}</p>
|
<div>
|
||||||
<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>
|
<h1 class="page-title" style="max-width:600px">{{ intervention.description }}</h1>
|
||||||
</div>
|
<p class="page-subtitle" style="margin-top:0.4rem">
|
||||||
<div class="flex gap-2 items-center">
|
<a href="/vehicules/{{ intervention.vid }}" class="mono" style="color:var(--accent-electric);text-decoration:none;font-size:0.95rem">{{ intervention.immatriculation }}</a>
|
||||||
{% if intervention.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm">⏳ En attente</span>
|
<span class="text-muted"> — {{ intervention.marque }} {{ intervention.modele }}</span>
|
||||||
{% elif intervention.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm">🔧 En cours</span>
|
</p>
|
||||||
{% else %}<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm">✅ Terminé</span>{% endif %}
|
<div class="flex gap-4 mt-2" style="flex-wrap:wrap">
|
||||||
<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>
|
{% if intervention.date_intervention %}
|
||||||
<form method="post" action="/interventions/{{ intervention.id }}/delete" onsubmit="return confirm('Supprimer cette intervention ?')">
|
<span class="text-xs text-secondary"><i class="fas fa-calendar mr-1"></i>{{ intervention.date_intervention }}</span>
|
||||||
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
{% endif %}
|
||||||
</form>
|
{% 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>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="/interventions/{{ intervention.id }}/edit" class="btn-secondary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Modifier
|
||||||
|
</a>
|
||||||
|
<form method="post" action="/interventions/{{ intervention.id }}/delete" onsubmit="return confirm('Supprimer cette intervention ?')">
|
||||||
|
<button class="btn-danger btn-sm">
|
||||||
|
<i class="fas fa-trash"></i> Supprimer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</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 %}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
<label class="block text-sm font-medium text-slate-700 mb-1">Technicien</label>
|
<div class="grid grid-2">
|
||||||
<input name="technicien" value="{{ intervention.technicien if intervention else '' }}" placeholder="Nom du technicien / garage" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
<div>
|
||||||
|
<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>
|
||||||
|
<label>Technicien / Garage</label>
|
||||||
|
<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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
</div>
|
<h1 class="page-title"><i class="fas fa-tools text-electric" style="margin-right:0.5rem"></i>Interventions</h1>
|
||||||
<div class="flex gap-2 mb-6 flex-wrap">
|
<p class="page-subtitle">{{ interventions|length }} intervention(s)</p>
|
||||||
<a href="/interventions" class="px-4 py-2 rounded-full text-sm {% if not statut %}bg-slate-800 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">Toutes</a>
|
|
||||||
<a href="/interventions?statut=en_attente" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_attente' %}bg-yellow-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">⏳ En attente</a>
|
|
||||||
<a href="/interventions?statut=en_cours" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_cours' %}bg-blue-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">🔧 En cours</a>
|
|
||||||
<a href="/interventions?statut=termine" class="px-4 py-2 rounded-full text-sm {% if statut == 'termine' %}bg-green-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">✅ Terminées</a>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white rounded-xl shadow overflow-x-auto">
|
|
||||||
{% if interventions %}
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left">Véhicule</th>
|
|
||||||
<th class="px-6 py-3 text-left">Description</th>
|
|
||||||
<th class="px-6 py-3 text-left">Statut</th>
|
|
||||||
<th class="px-6 py-3 text-left">Pièces</th>
|
|
||||||
<th class="px-6 py-3 text-left">Total</th>
|
|
||||||
<th class="px-6 py-3 text-left">Date</th>
|
|
||||||
<th class="px-6 py-3 text-left">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-100">
|
|
||||||
{% for i in interventions %}
|
|
||||||
<tr class="hover:bg-gray-50">
|
|
||||||
<td class="px-6 py-4 font-medium text-slate-800">
|
|
||||||
<a href="/vehicules/{{ i.vehicule_id }}" class="hover:text-orange-500">{{ i.immatriculation }}<br><span class="text-xs font-normal text-slate-400">{{ i.marque }} {{ i.modele }}</span></a>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-slate-600 max-w-xs">
|
|
||||||
<a href="/interventions/{{ i.id }}" class="hover:text-orange-500">{{ i.description[:60] }}{% if i.description|length > 60 %}...{% endif %}</a>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
|
|
||||||
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
|
|
||||||
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-slate-500 text-xs">{{ "%.2f"|format(i.cout_pieces) }} €</td>
|
|
||||||
<td class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td>
|
|
||||||
<td class="px-6 py-4 text-slate-400 text-xs">{{ i.created_at[:10] }}</td>
|
|
||||||
<td class="px-6 py-4 flex gap-3">
|
|
||||||
<a href="/interventions/{{ i.id }}" class="text-blue-500 hover:underline">Voir</a>
|
|
||||||
<a href="/interventions/{{ i.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
|
|
||||||
<form method="post" action="/interventions/{{ i.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ?')">
|
|
||||||
<button class="text-red-500 hover:underline">Supprimer</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<div class="p-8 text-center text-slate-400">
|
|
||||||
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention trouvée</p>
|
|
||||||
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<a href="/interventions/new" class="btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nouvelle intervention
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="padding:0">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
{% if interventions %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Véhicule</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Kilométrage</th>
|
||||||
|
<th>Pièces</th>
|
||||||
|
<th class="text-right">Total</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in interventions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<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 style="max-width:260px">
|
||||||
|
<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 class="text-secondary text-xs">{{ i.date_intervention or '—' }}</td>
|
||||||
|
<td class="mono text-xs text-secondary">{{ (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">{{ "%.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>
|
||||||
|
<td>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<a href="/interventions/{{ i.id }}" class="link-action">Voir</a>
|
||||||
|
<a href="/interventions/{{ i.id }}/edit" class="link-edit">Modifier</a>
|
||||||
|
<form method="post" action="/interventions/{{ i.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer cette intervention ?')">
|
||||||
|
<button class="link-danger">Supprimer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-tools"></i>
|
||||||
|
<p>Aucune intervention pour l'instant</p>
|
||||||
|
<a href="/interventions/new" class="link-action">Créer la première →</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+219
-99
@@ -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">
|
|
||||||
<div>
|
<!-- Header véhicule -->
|
||||||
<h2 class="text-2xl font-bold text-slate-800">{{ vehicule.immatriculation }}</h2>
|
<div class="flex justify-between items-start mb-8">
|
||||||
<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>
|
<div>
|
||||||
{% if vehicule.vin %}<p class="text-xs font-mono text-slate-400 mt-1">VIN : {{ vehicule.vin }}</p>{% endif %}
|
<h1 class="page-title mono" style="color:var(--accent-electric);font-size:2rem;letter-spacing:0.06em">{{ vehicule.immatriculation }}</h1>
|
||||||
</div>
|
<p class="page-subtitle" style="margin-top:0.35rem">
|
||||||
<div class="flex gap-2">
|
{{ vehicule.marque }} {{ vehicule.modele }}
|
||||||
<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>
|
{% if vehicule.annee %} — {{ vehicule.annee }}{% endif %}
|
||||||
<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>
|
{% if vehicule.kilometrage %} — <span class="mono">{{ vehicule.kilometrage }} km</span>{% endif %}
|
||||||
<form method="post" action="/vehicules/{{ vehicule.id }}/delete" onsubmit="return confirm('Supprimer ce véhicule ?')">
|
</p>
|
||||||
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
{% if vehicule.vin %}
|
||||||
</form>
|
<p class="text-xs text-muted mono" style="margin-top:0.35rem">VIN : {{ vehicule.vin }}</p>
|
||||||
</div>
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="btn-primary btn-sm">
|
||||||
|
<i class="fas fa-plus"></i> Intervention
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
</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">
|
||||||
<div>
|
<table>
|
||||||
<label class="block text-xs font-medium text-slate-600 mb-1">Type</label>
|
<thead>
|
||||||
<select name="type" id="info-type" onchange="toggleInfoType()" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
<tr>
|
||||||
<option value="texte">📝 Texte</option>
|
<th>Description</th>
|
||||||
<option value="url">🔗 URL / Lien</option>
|
<th>Date</th>
|
||||||
<option value="image">🖼️ Image</option>
|
<th>Kilométrage</th>
|
||||||
</select>
|
<th class="text-right">Pièces</th>
|
||||||
</div>
|
<th class="text-right">Total</th>
|
||||||
<div class="col-span-2">
|
</tr>
|
||||||
<label class="block text-xs font-medium text-slate-600 mb-1">Titre (optionnel)</label>
|
</thead>
|
||||||
<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">
|
<tbody>
|
||||||
</div>
|
{% for i in interventions %}
|
||||||
</div>
|
<tr style="cursor:pointer" onclick="window.location='/interventions/{{ i.id }}'">
|
||||||
<div id="input-texte">
|
<td>
|
||||||
<label class="block text-xs font-medium text-slate-600 mb-1">Contenu *</label>
|
<p class="font-medium">{{ i.description }}</p>
|
||||||
<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>
|
{% if i.technicien %}<p class="text-xs text-muted mt-1">{{ i.technicien }}</p>{% endif %}
|
||||||
</div>
|
</td>
|
||||||
<div id="input-url" class="hidden">
|
<td class="text-secondary text-sm">{{ i.date_intervention or '—' }}</td>
|
||||||
<label class="block text-xs font-medium text-slate-600 mb-1">URL *</label>
|
<td class="mono text-sm text-secondary">{{ (i.kilometrage_intervention|string + ' km') if i.kilometrage_intervention else '—' }}</td>
|
||||||
<input name="contenu" placeholder="https://..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
<td class="text-right">
|
||||||
</div>
|
<span class="text-xs text-secondary">{{ i.nb_pieces }} pce(s)</span><br>
|
||||||
<div id="input-image" class="hidden">
|
<span class="text-xs text-muted">{{ "%.2f"|format(i.cout_pieces) }} €</span>
|
||||||
<label class="block text-xs font-medium text-slate-600 mb-1">Photo *</label>
|
</td>
|
||||||
<input type="file" name="photo" accept="image/*" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
<td class="text-right font-semibold" style="color:var(--accent-volt)">
|
||||||
<input type="hidden" name="contenu" value="photo">
|
{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €
|
||||||
</div>
|
</td>
|
||||||
<button type="submit" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">Enregistrer</button>
|
</tr>
|
||||||
</form>
|
{% endfor %}
|
||||||
</div>
|
</tbody>
|
||||||
{% if infos %}
|
<tfoot>
|
||||||
<div class="divide-y divide-gray-100">
|
<tr style="background:var(--bg-secondary)">
|
||||||
{% for info in infos %}
|
<td colspan="4" class="text-right text-secondary font-semibold text-sm" style="padding:0.75rem 1rem">Total véhicule</td>
|
||||||
<div class="px-5 py-4 flex justify-between items-start hover:bg-gray-50">
|
<td class="text-right font-bold" style="color:var(--accent-volt);padding:0.75rem 1rem">
|
||||||
<div class="flex-1">
|
{{ "%.0f"|format(interventions|sum(attribute='cout_main_oeuvre') + interventions|sum(attribute='cout_pieces')) }} €
|
||||||
{% if info.titre %}<p class="text-xs font-semibold text-slate-500 uppercase mb-1">{{ info.titre }}</p>{% endif %}
|
</td>
|
||||||
{% if info.type == 'texte' %}
|
</tr>
|
||||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ info.contenu }}</p>
|
</tfoot>
|
||||||
{% elif info.type == 'url' %}
|
</table>
|
||||||
<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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="p-6 text-center text-slate-400 text-sm">Aucune info pour l'instant.</div>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Interventions -->
|
<!-- ── Toutes les pièces ── -->
|
||||||
<div class="bg-white rounded-xl shadow">
|
<div class="card mb-6" style="padding:0" id="pieces">
|
||||||
<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">🔧 Interventions ({{ interventions|length }})</h3>
|
<span class="card-title">
|
||||||
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 text-sm hover:underline">+ Nouvelle intervention</a>
|
<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>
|
</div>
|
||||||
{% if interventions %}
|
<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>
|
||||||
<div class="divide-y divide-gray-100">
|
|
||||||
{% for i in interventions %}
|
{% if toutes_pieces %}
|
||||||
<div class="px-5 py-4 hover:bg-gray-50">
|
<div class="overflow-x-auto">
|
||||||
<div class="flex justify-between items-start">
|
<table>
|
||||||
<div class="flex-1">
|
<thead>
|
||||||
<a href="/interventions/{{ i.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ i.description }}</a>
|
<tr>
|
||||||
{% 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 %}
|
<th>Photo</th>
|
||||||
<p class="text-xs text-slate-400 mt-1">{{ i.created_at[:10] }} • {{ i.nb_pieces }} pièce(s)</p>
|
<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 & 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>
|
||||||
|
<label>Type</label>
|
||||||
|
<select name="type" id="info-type" onchange="toggleInfoType()">
|
||||||
|
<option value="texte">📝 Texte libre</option>
|
||||||
|
<option value="url">🔗 Lien / URL</option>
|
||||||
|
<option value="image">🖼️ Image / Photo</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 ml-4 flex-shrink-0">
|
<div style="grid-column:span 2">
|
||||||
<span class="text-sm font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</span>
|
<label>Titre (optionnel)</label>
|
||||||
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
|
<input name="titre" placeholder="Ex: Forum, Fiche technique, Photo moteur…">
|
||||||
{% 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>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div id="input-texte">
|
||||||
|
<label>Contenu</label>
|
||||||
|
<textarea name="contenu" rows="2" placeholder="Notes, infos techniques…"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="input-url" style="display:none">
|
||||||
|
<label>URL</label>
|
||||||
|
<input name="contenu" placeholder="https://…">
|
||||||
|
</div>
|
||||||
|
<div id="input-image" style="display:none">
|
||||||
|
<label>Photo</label>
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary btn-sm">
|
||||||
|
<i class="fas fa-save"></i> Enregistrer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if infos %}
|
||||||
|
<div>
|
||||||
|
{% for info in infos %}
|
||||||
|
<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=''">
|
||||||
|
<div style="flex:1">
|
||||||
|
{% if info.titre %}
|
||||||
|
<p class="text-xs font-semibold uppercase" style="letter-spacing:0.06em;color:var(--text-muted);margin-bottom:0.3rem">{{ info.titre }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if info.type == 'texte' %}
|
||||||
|
<p class="text-sm" style="white-space:pre-wrap;color:var(--text-primary)">{{ info.contenu }}</p>
|
||||||
|
{% elif info.type == 'url' %}
|
||||||
|
<a href="{{ info.contenu }}" target="_blank" class="link-action text-sm" style="word-break:break-all">🔗 {{ info.contenu }}</a>
|
||||||
|
{% elif info.type == 'image' %}
|
||||||
|
<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')">
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-xs text-muted" style="margin-top:0.35rem">{{ info.created_at[:10] }}</p>
|
||||||
|
</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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
+60
-38
@@ -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>
|
||||||
</form>
|
<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>
|
||||||
|
</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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user