feat: redesign dark futuriste (Luca) — thème cyan/volt, sidebar dark, JetBrains Mono + README
This commit is contained in:
@@ -1,224 +1,154 @@
|
||||
# 🔧 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://docker.com)
|
||||
[](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) |
|
||||
[](https://python.org)
|
||||
[](https://fastapi.tiangolo.com)
|
||||
[](https://docs.docker.com/compose/)
|
||||
|
||||
---
|
||||
|
||||
## ✨ 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 :
|
||||
- **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)
|
||||
## 🎨 Design
|
||||
|
||||
### 🔩 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
|
||||
- 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
|
||||
## 🚀 Démarrage rapide
|
||||
|
||||
### 🔧 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 :
|
||||
- **Nom** de la pièce
|
||||
- **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
|
||||
# Rebuild complet (après modif templates/code)
|
||||
docker compose build --no-cache && docker compose up -d
|
||||
|
||||
### 📊 Tableau de bord
|
||||
# Logs
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
- Vue synthétique en temps réel :
|
||||
- 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
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
garagemanager/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # Application FastAPI (routes, DB, logique)
|
||||
├── templates/
|
||||
│ ├── base.html # Template de base (nav, layout)
|
||||
│ ├── dashboard.html # Tableau de bord
|
||||
│ ├── vehicules.html # Liste des véhicules
|
||||
│ ├── vehicule_form.html # Formulaire ajout/édition véhicule
|
||||
│ ├── vehicule_detail.html # Détail véhicule + interventions + infos
|
||||
│ ├── interventions.html # Liste des interventions
|
||||
│ ├── intervention_form.html # Formulaire ajout/édition intervention
|
||||
│ └── intervention_detail.html # Détail intervention + pièces
|
||||
├── data/ # Volume persistant (ignoré par git)
|
||||
│ └── garage.db # Base SQLite
|
||||
├── Dockerfile # Image Docker Python 3.12-slim
|
||||
├── docker-compose.yml # Déploiement avec Traefik
|
||||
├── requirements.txt # Dépendances Python
|
||||
└── README.md
|
||||
│ └── main.py # FastAPI routes + SQLite logic
|
||||
├── templates/ # Jinja2 templates (thème dark)
|
||||
│ ├── base.html # Layout sidebar + variables CSS
|
||||
│ ├── dashboard.html # Stats cards + tableau récent
|
||||
│ ├── vehicules.html # Liste véhicules
|
||||
│ ├── vehicule_form.html
|
||||
│ ├── vehicule_detail.html # Interventions + pièces + infos
|
||||
│ ├── interventions.html
|
||||
│ ├── intervention_form.html
|
||||
│ ├── intervention_detail.html # Détail + gestion pièces
|
||||
│ ├── clients.html
|
||||
│ ├── client_form.html
|
||||
│ └── client_detail.html
|
||||
├── data/
|
||||
│ └── garage.db # Base SQLite (volume Docker)
|
||||
├── 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
|
||||
|
||||
### 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*
|
||||
*Développé par l'équipe Perco — Design Luca, Backend intégré FastAPI*
|
||||
|
||||
+610
-18
@@ -4,31 +4,623 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}GarageManager{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="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">
|
||||
<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>
|
||||
<body class="bg-gray-100 min-h-screen flex">
|
||||
<div class="w-64 bg-slate-800 min-h-screen flex flex-col fixed left-0 top-0">
|
||||
<div class="p-6 border-b border-slate-700">
|
||||
<h1 class="text-white text-xl font-bold flex items-center gap-2">
|
||||
<i class="fas fa-wrench text-orange-400"></i> GarageManager
|
||||
</h1>
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<aside class="app-sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<i class="fas fa-wrench"></i> GarageManager
|
||||
</div>
|
||||
<nav class="flex-1 p-4 space-y-1">
|
||||
<a href="/" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'dashboard' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
|
||||
<i class="fas fa-chart-line w-5"></i> Dashboard
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">Navigation</div>
|
||||
<a href="/" class="sidebar-link {% if page == 'dashboard' %}active{% endif %}">
|
||||
<i class="fas fa-chart-line"></i> Dashboard
|
||||
</a>
|
||||
<a href="/vehicules" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'vehicules' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
|
||||
<i class="fas fa-car w-5"></i> Véhicules
|
||||
<a href="/vehicules" class="sidebar-link {% if page == 'vehicules' %}active{% endif %}">
|
||||
<i class="fas fa-car"></i> Véhicules
|
||||
</a>
|
||||
<a href="/interventions" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition {% if page == 'interventions' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
|
||||
<i class="fas fa-tools w-5"></i> Interventions
|
||||
<a href="/interventions" class="sidebar-link {% if page == 'interventions' %}active{% endif %}">
|
||||
<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>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-slate-700 text-xs text-slate-500 text-center">GarageManager v2.1</div>
|
||||
</div>
|
||||
<div class="ml-64 flex-1 p-8">
|
||||
<div class="sidebar-footer">GarageManager v2.1</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="app-main">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,49 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ client.nom }} {{ client.prenom }} - GarageManager{% endblock %}
|
||||
{% block title %}{{ client.nom }} {{ client.prenom }} — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<a href="/clients" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<a href="/clients" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{{ client.nom }} {{ client.prenom }}</h2>
|
||||
<div class="flex flex-wrap gap-4 mt-2 text-sm text-slate-500">
|
||||
{% if client.telephone %}<span><i class="fas fa-phone mr-1"></i>{{ client.telephone }}</span>{% endif %}
|
||||
{% if client.email %}<span><i class="fas fa-envelope mr-1"></i>{{ client.email }}</span>{% endif %}
|
||||
{% if client.adresse %}<span><i class="fas fa-map-marker-alt mr-1"></i>{{ client.adresse }}</span>{% endif %}
|
||||
<h1 class="page-title">{{ client.nom }} {{ client.prenom }}</h1>
|
||||
<div class="flex gap-4 mt-2" style="flex-wrap:wrap">
|
||||
{% if client.telephone %}
|
||||
<span class="text-sm text-secondary"><i class="fas fa-phone mr-1 text-electric"></i>{{ client.telephone }}</span>
|
||||
{% endif %}
|
||||
{% if client.email %}
|
||||
<span class="text-sm text-secondary"><i class="fas fa-envelope mr-1 text-electric"></i>{{ client.email }}</span>
|
||||
{% endif %}
|
||||
{% if client.adresse %}
|
||||
<span class="text-sm text-secondary"><i class="fas fa-map-marker-alt mr-1 text-electric"></i>{{ client.adresse }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/clients/{{ client.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
|
||||
<a href="/clients/{{ client.id }}/edit" class="btn-secondary btn-sm">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<form method="post" action="/clients/{{ client.id }}/delete" onsubmit="return confirm('Supprimer ce client et tous ses véhicules ?')">
|
||||
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
||||
<button class="btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i> Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-slate-800">Véhicules ({{ vehicules|length }})</h3>
|
||||
<a href="/vehicules/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Ajouter un véhicule</a>
|
||||
|
||||
<!-- Véhicules du client -->
|
||||
<div class="card" style="padding:0">
|
||||
<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>
|
||||
|
||||
{% if vehicules %}
|
||||
<div class="divide-y divide-gray-100">
|
||||
{% for v in vehicules %}
|
||||
<div class="px-6 py-4 flex justify-between items-center hover:bg-gray-50">
|
||||
<div>
|
||||
<span class="font-medium text-slate-800">{{ v.immatriculation }}</span>
|
||||
<span class="text-slate-500 ml-2">{{ v.marque }} {{ v.modele }}</span>
|
||||
{% if v.annee %}<span class="text-slate-400 text-sm ml-2">({{ v.annee }})</span>{% endif %}
|
||||
{% if v.kilometrage %}<span class="text-slate-400 text-sm ml-2">— {{ v.kilometrage }} km</span>{% endif %}
|
||||
{% for v in vehicules %}
|
||||
<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>
|
||||
<span class="font-semibold mono" style="color:var(--accent-electric)">{{ v.immatriculation }}</span>
|
||||
<span class="text-secondary ml-2">{{ v.marque }} {{ v.modele }}</span>
|
||||
{% if v.annee %}<span class="text-muted text-xs ml-2">({{ v.annee }})</span>{% endif %}
|
||||
{% if v.kilometrage %}<span class="text-muted text-xs ml-2 mono">— {{ v.kilometrage }} km</span>{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">{{ v.nb_interventions }} intervention(s)</span>
|
||||
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline text-sm">Voir →</a>
|
||||
<span class="badge badge-info">{{ v.nb_interventions }} intervention(s)</span>
|
||||
<a href="/vehicules/{{ v.id }}" class="link-action text-sm">Voir →</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-slate-400 text-sm">Aucun véhicule enregistré pour ce client.</div>
|
||||
<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 %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+34
-20
@@ -1,38 +1,52 @@
|
||||
{% 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 %}
|
||||
<div class="mb-8">
|
||||
<a href="/clients" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{% if client %}Modifier le client{% else %}Nouveau client{% endif %}</h2>
|
||||
|
||||
<a href="/clients" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux clients</a>
|
||||
|
||||
<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 class="bg-white rounded-xl shadow p-8 max-w-2xl">
|
||||
<form method="post" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="card" style="max-width:580px">
|
||||
<form method="post" style="display:flex;flex-direction:column;gap:1.25rem">
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Nom *</label>
|
||||
<input name="nom" value="{{ client.nom if client else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Nom *</label>
|
||||
<input name="nom" value="{{ client.nom if client else '' }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Prénom *</label>
|
||||
<input name="prenom" value="{{ client.prenom if client else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Prénom *</label>
|
||||
<input name="prenom" value="{{ client.prenom if client else '' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Téléphone</label>
|
||||
<input name="telephone" value="{{ client.telephone if client else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Téléphone</label>
|
||||
<input name="telephone" value="{{ client.telephone if client else '' }}" placeholder="06 xx xx xx xx">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Email</label>
|
||||
<input name="email" type="email" value="{{ client.email if client else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Email</label>
|
||||
<input name="email" type="email" value="{{ client.email if client else '' }}" placeholder="contact@exemple.fr">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Adresse</label>
|
||||
<textarea name="adresse" rows="2" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ client.adresse if client else '' }}</textarea>
|
||||
<label>Adresse</label>
|
||||
<textarea name="adresse" rows="2" placeholder="Adresse complète…">{{ client.adresse if client else '' }}</textarea>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
|
||||
<a href="/clients" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
|
||||
|
||||
<div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="/clients" class="btn-secondary">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+54
-33
@@ -1,54 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Clients - GarageManager{% endblock %}
|
||||
{% block title %}Clients — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div><h2 class="text-2xl font-bold text-slate-800">Clients</h2><p class="text-slate-500 mt-1">{{ clients|length }} client(s)</p></div>
|
||||
<a href="/clients/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition flex items-center gap-2"><i class="fas fa-plus"></i> Nouveau client</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-4 border-b border-gray-100">
|
||||
<form method="get" class="flex gap-2">
|
||||
<input name="q" value="{{ q }}" placeholder="Rechercher par nom, téléphone..." class="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<button type="submit" class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm hover:bg-slate-800">Rechercher</button>
|
||||
{% if q %}<a href="/clients" class="text-slate-500 px-3 py-2 text-sm hover:text-slate-700 flex items-center">✕</a>{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="page-header flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="page-title"><i class="fas fa-users text-electric" style="margin-right:0.5rem"></i>Clients</h1>
|
||||
<p class="page-subtitle">{{ clients|length }} client(s) enregistré(s)</p>
|
||||
</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">
|
||||
{% if clients %}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left">Nom</th>
|
||||
<th class="px-6 py-3 text-left">Téléphone</th>
|
||||
<th class="px-6 py-3 text-left">Email</th>
|
||||
<th class="px-6 py-3 text-left">Véhicules</th>
|
||||
<th class="px-6 py-3 text-left">Actions</th>
|
||||
<th>Nom</th>
|
||||
<th>Téléphone</th>
|
||||
<th>Email</th>
|
||||
<th>Véhicules</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<tbody>
|
||||
{% for c in clients %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4"><a href="/clients/{{ c.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ c.nom }} {{ c.prenom }}</a></td>
|
||||
<td class="px-6 py-4 text-slate-600">{{ c.telephone or '-' }}</td>
|
||||
<td class="px-6 py-4 text-slate-600">{{ c.email or '-' }}</td>
|
||||
<td class="px-6 py-4"><span class="bg-purple-100 text-purple-700 px-2 py-1 rounded-full text-xs">{{ c.nb_vehicules }}</span></td>
|
||||
<td class="px-6 py-4 flex gap-3">
|
||||
<a href="/clients/{{ c.id }}" class="text-blue-500 hover:underline">Voir</a>
|
||||
<a href="/clients/{{ c.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
|
||||
<form method="post" action="/clients/{{ c.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce client ?')">
|
||||
<button type="submit" class="text-red-500 hover:underline">Supprimer</button>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/clients/{{ c.id }}" class="font-semibold" style="text-decoration:none;color:var(--text-primary)">{{ c.nom }} {{ c.prenom }}</a>
|
||||
</td>
|
||||
<td class="text-secondary text-sm">{{ c.telephone or '—' }}</td>
|
||||
<td class="text-secondary text-sm">{{ c.email or '—' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ c.nb_vehicules }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-3">
|
||||
<a href="/clients/{{ c.id }}" class="link-action">Voir</a>
|
||||
<a href="/clients/{{ c.id }}/edit" class="link-edit">Modifier</a>
|
||||
<form method="post" action="/clients/{{ c.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer ce client ?')">
|
||||
<button class="link-danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-slate-400">
|
||||
<i class="fas fa-users text-3xl mb-3"></i><p>Aucun client trouvé</p>
|
||||
<a href="/clients/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
<p>Aucun client trouvé</p>
|
||||
<a href="/clients/new" class="link-action">Ajouter le premier →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+93
-63
@@ -1,89 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - GarageManager{% endblock %}
|
||||
{% block title %}Dashboard — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-slate-800">Dashboard</h2>
|
||||
<p class="text-slate-500 mt-1">Vue d'ensemble de votre garage</p>
|
||||
|
||||
<div class="page-header flex justify-between items-center">
|
||||
<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 class="grid grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="text-sm text-slate-500">Véhicules</p><p class="text-3xl font-bold text-slate-800 mt-1">{{ stats.nb_vehicules }}</p></div>
|
||||
<div class="bg-purple-100 p-3 rounded-full"><i class="fas fa-car text-purple-500 text-xl"></i></div>
|
||||
</div>
|
||||
<a href="/vehicules" class="text-xs text-purple-500 hover:underline mt-3 inline-block">Voir tous →</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="text-sm text-slate-500">En attente</p><p class="text-3xl font-bold text-yellow-500 mt-1">{{ stats.nb_en_attente }}</p></div>
|
||||
<div class="bg-yellow-100 p-3 rounded-full"><i class="fas fa-clock text-yellow-500 text-xl"></i></div>
|
||||
</div>
|
||||
<a href="/interventions?statut=en_attente" class="text-xs text-yellow-500 hover:underline mt-3 inline-block">Voir →</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="text-sm text-slate-500">En cours</p><p class="text-3xl font-bold text-blue-500 mt-1">{{ stats.nb_en_cours }}</p></div>
|
||||
<div class="bg-blue-100 p-3 rounded-full"><i class="fas fa-spinner text-blue-500 text-xl"></i></div>
|
||||
</div>
|
||||
<a href="/interventions?statut=en_cours" class="text-xs text-blue-500 hover:underline mt-3 inline-block">Voir →</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="text-sm text-slate-500">Terminées</p><p class="text-3xl font-bold text-green-500 mt-1">{{ stats.nb_termine }}</p></div>
|
||||
<div class="bg-green-100 p-3 rounded-full"><i class="fas fa-check-circle text-green-500 text-xl"></i></div>
|
||||
</div>
|
||||
<a href="/interventions?statut=termine" class="text-xs text-green-500 hover:underline mt-3 inline-block">Voir →</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><p class="text-sm text-slate-500">Coût pièces</p><p class="text-3xl font-bold text-slate-800 mt-1">{{ "%.0f"|format(stats.cout_pieces) }} €</p></div>
|
||||
<div class="bg-red-100 p-3 rounded-full"><i class="fas fa-cog text-red-400 text-xl"></i></div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-4 mb-8">
|
||||
<div class="card" style="cursor:default">
|
||||
<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 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 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 class="card" style="cursor:default">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="stat-label">Interventions</div>
|
||||
<div style="width:38px;height:38px;background:rgba(250,204,21,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
|
||||
<i class="fas fa-tools text-volt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value" style="color:var(--accent-volt)">{{ stats.nb_interventions }}</div>
|
||||
<a href="/interventions" class="link-action text-xs mt-2" style="display:inline-block;color:var(--accent-volt)">Voir toutes →</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="cursor:default">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="stat-label">Pièces en stock</div>
|
||||
<div style="width:38px;height:38px;background:rgba(34,197,94,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
|
||||
<i class="fas fa-cog text-green"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value" style="color:var(--accent-green)">{{ stats.nb_pieces_stock }}</div>
|
||||
<span class="text-xs text-muted mt-2" style="display:inline-block">Pièces référencées</span>
|
||||
</div>
|
||||
|
||||
<div class="card" style="cursor:default;border-color:rgba(250,204,21,0.2)">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="stat-label" style="color:var(--accent-volt)">Total dépensé</div>
|
||||
<div style="width:38px;height:38px;background:rgba(250,204,21,0.1);border-radius:8px;display:flex;align-items:center;justify-content:center">
|
||||
<i class="fas fa-euro-sign text-volt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value" style="color:var(--accent-volt)">{{ "%.0f"|format(stats.cout_total) }} €</div>
|
||||
<span class="text-xs text-muted mt-2" style="display:inline-block">Toutes interventions</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-slate-800">Interventions récentes</h3>
|
||||
<a href="/interventions/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition">+ Nouvelle</a>
|
||||
|
||||
<!-- Interventions récentes -->
|
||||
<div class="card" style="padding:0">
|
||||
<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>
|
||||
|
||||
{% if recent %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left">Véhicule</th>
|
||||
<th class="px-6 py-3 text-left">Description</th>
|
||||
<th class="px-6 py-3 text-left">Statut</th>
|
||||
<th class="px-6 py-3 text-left">Coût total</th>
|
||||
<th>Véhicule</th>
|
||||
<th>Description</th>
|
||||
<th>Date</th>
|
||||
<th>Kilométrage</th>
|
||||
<th>Pièces</th>
|
||||
<th class="text-right">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<tbody>
|
||||
{% for i in recent %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium text-slate-800"><a href="/vehicules/{{ i.vehicule_id }}" class="hover:text-orange-500">{{ i.immatriculation }}<br><span class="text-xs font-normal text-slate-400">{{ i.marque }} {{ i.modele }}</span></a></td>
|
||||
<td class="px-6 py-4 text-slate-600"><a href="/interventions/{{ i.id }}" class="hover:text-orange-500">{{ i.description[:60] }}{% if i.description|length > 60 %}...{% endif %}</a></td>
|
||||
<td class="px-6 py-4">
|
||||
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
|
||||
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
|
||||
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
|
||||
<tr style="cursor:pointer" onclick="window.location='/interventions/{{ i.id }}'">
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
<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 class="px-6 py-4 font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-slate-400">
|
||||
<i class="fas fa-tools text-3xl mb-3"></i><p>Aucune intervention pour l'instant</p>
|
||||
<a href="/interventions/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Créer la première →</a>
|
||||
<div 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>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,142 +1,203 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Intervention #{{ intervention.id }} - GarageManager{% endblock %}
|
||||
{% block title %}Intervention — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<a href="/vehicules/{{ intervention.vid }}" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour au véhicule</a>
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<a href="/vehicules/{{ intervention.vid }}" class="back-link"><i class="fas fa-arrow-left"></i> Retour au véhicule</a>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{{ intervention.description }}</h2>
|
||||
<p class="text-slate-500 mt-1">{{ intervention.immatriculation }} — {{ intervention.marque }} {{ intervention.modele }}</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ intervention.created_at[:10] }}{% if intervention.technicien %} • <i class="fas fa-user-cog mr-1"></i>{{ intervention.technicien }}{% endif %}</p>
|
||||
<h1 class="page-title" style="max-width:600px">{{ intervention.description }}</h1>
|
||||
<p class="page-subtitle" style="margin-top:0.4rem">
|
||||
<a href="/vehicules/{{ intervention.vid }}" class="mono" style="color:var(--accent-electric);text-decoration:none;font-size:0.95rem">{{ intervention.immatriculation }}</a>
|
||||
<span class="text-muted"> — {{ intervention.marque }} {{ intervention.modele }}</span>
|
||||
</p>
|
||||
<div class="flex gap-4 mt-2" style="flex-wrap:wrap">
|
||||
{% if intervention.date_intervention %}
|
||||
<span class="text-xs text-secondary"><i class="fas fa-calendar mr-1"></i>{{ intervention.date_intervention }}</span>
|
||||
{% endif %}
|
||||
{% if intervention.kilometrage_intervention %}
|
||||
<span class="text-xs text-secondary mono"><i class="fas fa-tachometer-alt mr-1"></i>{{ intervention.kilometrage_intervention }} km</span>
|
||||
{% endif %}
|
||||
{% if intervention.technicien %}
|
||||
<span class="text-xs text-secondary"><i class="fas fa-user-cog mr-1"></i>{{ intervention.technicien }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
{% if intervention.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm">⏳ En attente</span>
|
||||
{% elif intervention.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm">🔧 En cours</span>
|
||||
{% else %}<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm">✅ Terminé</span>{% endif %}
|
||||
<a href="/interventions/{{ intervention.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
|
||||
</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="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
||||
<button class="btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i> Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Résumé coûts -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-xs text-slate-500">Main d'œuvre</p>
|
||||
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre) }} €</p>
|
||||
<div class="grid grid-3 mb-6">
|
||||
<div class="card" style="cursor:default">
|
||||
<div class="stat-label mb-2">Main d'œuvre</div>
|
||||
<div class="stat-value" style="color:var(--accent-electric)">{{ "%.2f"|format(intervention.cout_main_oeuvre) }} €</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-xs text-slate-500">Pièces</p>
|
||||
<p class="text-2xl font-bold text-slate-800 mt-1">{{ "%.2f"|format(intervention.cout_pieces) }} €</p>
|
||||
<div class="card" style="cursor:default">
|
||||
<div class="stat-label mb-2">Pièces</div>
|
||||
<div class="stat-value" style="color:var(--text-secondary)">{{ "%.2f"|format(intervention.cout_pieces) }} €</div>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-xl shadow p-5 border border-orange-200">
|
||||
<p class="text-xs text-orange-600">Total</p>
|
||||
<p class="text-2xl font-bold text-orange-600 mt-1">{{ "%.2f"|format(intervention.cout_main_oeuvre + intervention.cout_pieces) }} €</p>
|
||||
<div class="card" style="cursor:default;border-color:rgba(250,204,21,0.25)">
|
||||
<div class="stat-label mb-2" style="color:var(--accent-volt)">Total</div>
|
||||
<div class="stat-value" style="color:var(--accent-volt)">{{ "%.2f"|format(intervention.cout_main_oeuvre + intervention.cout_pieces) }} €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if intervention.notes %}
|
||||
<div class="bg-white rounded-xl shadow p-5 mb-6">
|
||||
<h3 class="font-semibold text-slate-800 mb-2">📋 Notes</h3>
|
||||
<p class="text-sm text-slate-600 whitespace-pre-wrap">{{ intervention.notes }}</p>
|
||||
<div class="card mb-6">
|
||||
<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" style="white-space:pre-wrap;color:var(--text-secondary)">{{ intervention.notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pièces -->
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-slate-800">🔩 Pièces utilisées ({{ pieces|length }})</h3>
|
||||
<button onclick="document.getElementById('add-piece-form').classList.toggle('hidden')" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Ajouter une pièce</button>
|
||||
<!-- ── Pièces utilisées ── -->
|
||||
<div class="card" style="padding:0">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<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>
|
||||
|
||||
<!-- Formulaire ajout pièce -->
|
||||
<div id="add-piece-form" class="hidden p-5 border-b border-gray-100 bg-gray-50">
|
||||
<!-- Form ajout pièce -->
|
||||
<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">
|
||||
<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>
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Nom de la pièce *</label>
|
||||
<input name="nom" required placeholder="Ex: Filtre à huile, Courroie..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Nom *</label>
|
||||
<input id="p-nom" name="nom" required placeholder="Nom de la pièce">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Référence</label>
|
||||
<input name="reference" placeholder="Ex: OC90, 06A115561B..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Référence</label>
|
||||
<input id="p-ref" name="reference" placeholder="Ex: 06A115561B…" style="font-family:'JetBrains Mono',monospace">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 mb-3">
|
||||
<div class="grid grid-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Marque</label>
|
||||
<input name="marque" placeholder="Ex: Bosch, NGK, Valeo..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Marque</label>
|
||||
<input id="p-marque" name="marque" placeholder="Bosch, NGK…">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Prix unitaire (€)</label>
|
||||
<input name="prix" type="number" step="0.01" min="0" value="0" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Prix unitaire (€)</label>
|
||||
<input id="p-prix" name="prix" type="number" step="0.01" min="0" value="0" class="mono">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Quantité</label>
|
||||
<input name="quantite" type="number" min="1" value="1" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Quantité</label>
|
||||
<input name="quantite" type="number" min="1" value="1" class="mono">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Photo (optionnel)</label>
|
||||
<input type="file" name="photo" accept="image/*" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
||||
<label>Photo (optionnel)</label>
|
||||
<input type="file" name="photo" accept="image/*" style="background:none;border:none;padding:0.25rem 0;color:var(--text-secondary)">
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Liste des pièces -->
|
||||
{% if pieces %}
|
||||
{% if pieces_utilisees %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 text-left">Photo</th>
|
||||
<th class="px-5 py-3 text-left">Pièce</th>
|
||||
<th class="px-5 py-3 text-left">Référence</th>
|
||||
<th class="px-5 py-3 text-left">Marque</th>
|
||||
<th class="px-5 py-3 text-left">Qté</th>
|
||||
<th class="px-5 py-3 text-left">Prix unit.</th>
|
||||
<th class="px-5 py-3 text-left">Total</th>
|
||||
<th class="px-5 py-3 text-left"></th>
|
||||
<th>Photo</th>
|
||||
<th>Pièce</th>
|
||||
<th>Référence</th>
|
||||
<th>Marque</th>
|
||||
<th class="text-center">Qté</th>
|
||||
<th class="text-right">Prix unit.</th>
|
||||
<th class="text-right">Total</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
{% for p in pieces %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-5 py-3">
|
||||
<tbody>
|
||||
{% for p in pieces_utilisees %}
|
||||
<tr>
|
||||
<td>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
</td>
|
||||
<td class="px-5 py-3 font-medium text-slate-800">{{ p.nom }}</td>
|
||||
<td class="px-5 py-3 text-slate-500 font-mono text-xs">{{ p.reference or '-' }}</td>
|
||||
<td class="px-5 py-3 text-slate-600">{{ p.marque or '-' }}</td>
|
||||
<td class="px-5 py-3 text-slate-600">{{ p.quantite }}</td>
|
||||
<td class="px-5 py-3 text-slate-600">{{ "%.2f"|format(p.prix) }} €</td>
|
||||
<td class="px-5 py-3 font-semibold text-slate-800">{{ "%.2f"|format(p.prix * p.quantite) }} €</td>
|
||||
<td class="px-5 py-3">
|
||||
<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="text-center text-secondary mono">{{ p.quantite }}</td>
|
||||
<td class="text-right mono text-sm">{{ "%.2f"|format(p.prix) }} €</td>
|
||||
<td class="text-right font-semibold mono" style="color:var(--accent-volt)">{{ "%.2f"|format(p.prix * p.quantite) }} €</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="bg-orange-50">
|
||||
<td colspan="6" class="px-5 py-3 text-right font-semibold text-slate-700">Total pièces</td>
|
||||
<td colspan="2" class="px-5 py-3 font-bold text-orange-600">{{ "%.2f"|format(intervention.cout_pieces) }} €</td>
|
||||
<tr style="background:var(--bg-secondary)">
|
||||
<td colspan="6" class="text-right font-semibold text-secondary text-sm">Total pièces</td>
|
||||
<td class="text-right font-bold mono" style="color:var(--accent-volt)">{{ "%.2f"|format(intervention.cout_pieces) }} €</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-slate-400 text-sm">
|
||||
Aucune pièce enregistrée.<br>
|
||||
<button onclick="document.getElementById('add-piece-form').classList.remove('hidden')" class="text-orange-500 hover:underline mt-1 inline-block">Ajouter la première →</button>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-cog"></i>
|
||||
<p>Aucune pièce pour cette intervention</p>
|
||||
<button onclick="toggleAddPiece()" class="link-action">Ajouter →</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
||||
@@ -1,51 +1,68 @@
|
||||
{% 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 %}
|
||||
<div class="mb-8">
|
||||
<a href="/interventions" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{% if intervention %}Modifier l'intervention{% else %}Nouvelle intervention{% endif %}</h2>
|
||||
|
||||
<a href="/interventions" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux interventions</a>
|
||||
|
||||
<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 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>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Véhicule *</label>
|
||||
<select name="vehicule_id" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<option value="">-- Sélectionner un véhicule --</option>
|
||||
<label>Véhicule *</label>
|
||||
<select name="vehicule_id" required>
|
||||
<option value="">— Sélectionner un véhicule —</option>
|
||||
{% for v in vehicules %}
|
||||
<option value="{{ v.id }}" {% if (intervention and intervention.vehicule_id == v.id) or preselect_vehicule == v.id %}selected{% endif %}>{{ v.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Description *</label>
|
||||
<textarea name="description" rows="2" required placeholder="Ex: Vidange + filtre à huile, Changement courroie de distribution..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ intervention.description if intervention else '' }}</textarea>
|
||||
<label>Description *</label>
|
||||
<textarea name="description" rows="2" required placeholder="Ex: Vidange + filtre à huile, Changement courroie de distribution…">{{ intervention.description if intervention else '' }}</textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Statut</label>
|
||||
<select name="statut" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<option value="en_attente" {% if not intervention or intervention.statut == 'en_attente' %}selected{% endif %}>⏳ En attente</option>
|
||||
<option value="en_cours" {% if intervention and intervention.statut == 'en_cours' %}selected{% endif %}>🔧 En cours</option>
|
||||
<option value="termine" {% if intervention and intervention.statut == 'termine' %}selected{% endif %}>✅ Terminé</option>
|
||||
</select>
|
||||
<label>Date de l'intervention</label>
|
||||
<input name="date_intervention" type="date" value="{{ intervention.date_intervention if intervention and intervention.date_intervention else '' }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Main d'œuvre (€)</label>
|
||||
<input name="cout_main_oeuvre" type="number" step="0.01" min="0" value="{{ intervention.cout_main_oeuvre if intervention else '0' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Kilométrage (km)</label>
|
||||
<input name="kilometrage_intervention" type="number" value="{{ intervention.kilometrage_intervention if intervention and intervention.kilometrage_intervention else '' }}" placeholder="85000" class="mono">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label>Main d'œuvre (€)</label>
|
||||
<input name="cout_main_oeuvre" type="number" step="0.01" min="0" value="{{ intervention.cout_main_oeuvre if intervention else '0' }}" class="mono">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Technicien</label>
|
||||
<input name="technicien" value="{{ intervention.technicien if intervention else '' }}" placeholder="Nom du technicien / garage" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Technicien / Garage</label>
|
||||
<input name="technicien" value="{{ intervention.technicien if intervention else '' }}" placeholder="Nom du garage, soi-même…">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Notes</label>
|
||||
<textarea name="notes" rows="3" placeholder="Observations, prochaine révision prévue, problèmes restants..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">{{ intervention.notes if intervention else '' }}</textarea>
|
||||
<label>Notes</label>
|
||||
<textarea name="notes" rows="3" placeholder="Observations, prochaine révision, problèmes restants…">{{ intervention.notes if intervention else '' }}</textarea>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
|
||||
<a href="/interventions" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
|
||||
|
||||
<div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="/interventions" class="btn-secondary">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,63 +1,77 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Interventions - GarageManager{% endblock %}
|
||||
{% block title %}Interventions — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div><h2 class="text-2xl font-bold text-slate-800">Interventions</h2><p class="text-slate-500 mt-1">{{ interventions|length }} intervention(s)</p></div>
|
||||
<a href="/interventions/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 flex items-center gap-2"><i class="fas fa-plus"></i> Nouvelle intervention</a>
|
||||
|
||||
<div class="page-header flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="page-title"><i class="fas fa-tools text-electric" style="margin-right:0.5rem"></i>Interventions</h1>
|
||||
<p class="page-subtitle">{{ interventions|length }} intervention(s)</p>
|
||||
</div>
|
||||
<a href="/interventions/new" class="btn-primary">
|
||||
<i class="fas fa-plus"></i> Nouvelle intervention
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-6 flex-wrap">
|
||||
<a href="/interventions" class="px-4 py-2 rounded-full text-sm {% if not statut %}bg-slate-800 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">Toutes</a>
|
||||
<a href="/interventions?statut=en_attente" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_attente' %}bg-yellow-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">⏳ En attente</a>
|
||||
<a href="/interventions?statut=en_cours" class="px-4 py-2 rounded-full text-sm {% if statut == 'en_cours' %}bg-blue-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">🔧 En cours</a>
|
||||
<a href="/interventions?statut=termine" class="px-4 py-2 rounded-full text-sm {% if statut == 'termine' %}bg-green-500 text-white{% else %}bg-white text-slate-600 border hover:bg-gray-50{% endif %}">✅ Terminées</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow overflow-x-auto">
|
||||
|
||||
<div class="card" style="padding:0">
|
||||
<div class="overflow-x-auto">
|
||||
{% if interventions %}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
||||
<table>
|
||||
<thead>
|
||||
<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>
|
||||
<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 class="divide-y divide-gray-100">
|
||||
<tbody>
|
||||
{% 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>
|
||||
<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 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 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="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 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="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>
|
||||
<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="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 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 %}
|
||||
|
||||
+210
-90
@@ -1,128 +1,248 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ vehicule.immatriculation }} - GarageManager{% endblock %}
|
||||
{% block title %}{{ vehicule.immatriculation }} — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<a href="/vehicules" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<a href="/vehicules" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
|
||||
|
||||
<!-- Header véhicule -->
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{{ vehicule.immatriculation }}</h2>
|
||||
<p class="text-slate-500 mt-1">{{ vehicule.marque }} {{ vehicule.modele }}{% if vehicule.annee %} ({{ vehicule.annee }}){% endif %}{% if vehicule.kilometrage %} • {{ vehicule.kilometrage }} km{% endif %}</p>
|
||||
{% if vehicule.vin %}<p class="text-xs font-mono text-slate-400 mt-1">VIN : {{ vehicule.vin }}</p>{% endif %}
|
||||
<h1 class="page-title mono" style="color:var(--accent-electric);font-size:2rem;letter-spacing:0.06em">{{ vehicule.immatriculation }}</h1>
|
||||
<p class="page-subtitle" style="margin-top:0.35rem">
|
||||
{{ vehicule.marque }} {{ vehicule.modele }}
|
||||
{% if vehicule.annee %} — {{ vehicule.annee }}{% endif %}
|
||||
{% if vehicule.kilometrage %} — <span class="mono">{{ vehicule.kilometrage }} km</span>{% endif %}
|
||||
</p>
|
||||
{% if vehicule.vin %}
|
||||
<p class="text-xs text-muted mono" style="margin-top:0.35rem">VIN : {{ vehicule.vin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">+ Intervention</a>
|
||||
<a href="/vehicules/{{ vehicule.id }}/edit" class="border border-orange-300 text-orange-500 px-4 py-2 rounded-lg text-sm hover:bg-orange-50">Modifier</a>
|
||||
<form method="post" action="/vehicules/{{ vehicule.id }}/delete" onsubmit="return confirm('Supprimer ce véhicule ?')">
|
||||
<button class="border border-red-300 text-red-500 px-4 py-2 rounded-lg text-sm hover:bg-red-50">Supprimer</button>
|
||||
<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>
|
||||
|
||||
<!-- Infos / Notes -->
|
||||
<div class="bg-white rounded-xl shadow mb-6">
|
||||
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-slate-800">📎 Infos & Notes</h3>
|
||||
<button onclick="document.getElementById('add-info-form').classList.toggle('hidden')" class="text-orange-500 text-sm hover:underline">+ Ajouter</button>
|
||||
<!-- ── Historique interventions ── -->
|
||||
<div class="card mb-6" style="padding:0" id="interventions">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<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 id="add-info-form" class="hidden p-5 border-b border-gray-100 bg-gray-50">
|
||||
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/add" enctype="multipart/form-data" class="space-y-3">
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
|
||||
{% if interventions %}
|
||||
<div class="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Date</th>
|
||||
<th>Kilométrage</th>
|
||||
<th class="text-right">Pièces</th>
|
||||
<th class="text-right">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in interventions %}
|
||||
<tr style="cursor:pointer" onclick="window.location='/interventions/{{ i.id }}'">
|
||||
<td>
|
||||
<p class="font-medium">{{ i.description }}</p>
|
||||
{% if i.technicien %}<p class="text-xs text-muted mt-1">{{ i.technicien }}</p>{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary text-sm">{{ i.date_intervention or '—' }}</td>
|
||||
<td class="mono text-sm text-secondary">{{ (i.kilometrage_intervention|string + ' km') if i.kilometrage_intervention else '—' }}</td>
|
||||
<td class="text-right">
|
||||
<span class="text-xs text-secondary">{{ i.nb_pieces }} pce(s)</span><br>
|
||||
<span class="text-xs text-muted">{{ "%.2f"|format(i.cout_pieces) }} €</span>
|
||||
</td>
|
||||
<td class="text-right font-semibold" style="color:var(--accent-volt)">
|
||||
{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="background:var(--bg-secondary)">
|
||||
<td colspan="4" class="text-right text-secondary font-semibold text-sm" style="padding:0.75rem 1rem">Total véhicule</td>
|
||||
<td class="text-right font-bold" style="color:var(--accent-volt);padding:0.75rem 1rem">
|
||||
{{ "%.0f"|format(interventions|sum(attribute='cout_main_oeuvre') + interventions|sum(attribute='cout_pieces')) }} €
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-tools"></i>
|
||||
<p>Aucune intervention</p>
|
||||
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="link-action">Créer →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ── Toutes les pièces ── -->
|
||||
<div class="card mb-6" style="padding:0" id="pieces">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<i class="fas fa-cog text-volt" style="margin-right:0.4rem"></i>
|
||||
Toutes les pièces utilisées
|
||||
<span class="badge badge-warning ml-2">{{ toutes_pieces|length }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted" style="padding:0.6rem 1.5rem;border-bottom:1px solid var(--border)">Récapitulatif de toutes les pièces de toutes les interventions — pour les retrouver et racheter facilement.</p>
|
||||
|
||||
{% if toutes_pieces %}
|
||||
<div class="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Photo</th>
|
||||
<th>Pièce</th>
|
||||
<th>Référence</th>
|
||||
<th>Marque</th>
|
||||
<th>Prix unit.</th>
|
||||
<th class="text-center">Qté</th>
|
||||
<th>Intervention</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in toutes_pieces %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if p.photo %}
|
||||
<img src="{{ p.photo }}" class="photo-thumb" onclick="window.open('{{ p.photo }}','_blank')">
|
||||
{% else %}
|
||||
<div class="photo-placeholder"><i class="fas fa-cog text-xs"></i></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="font-medium">{{ p.nom }}</td>
|
||||
<td class="mono text-xs text-muted">{{ p.reference or '—' }}</td>
|
||||
<td class="text-secondary text-sm">{{ p.marque or '—' }}</td>
|
||||
<td class="mono text-sm">{{ "%.2f"|format(p.prix) }} €</td>
|
||||
<td class="text-center text-secondary">{{ p.quantite }}</td>
|
||||
<td class="text-xs text-secondary" style="max-width:200px">
|
||||
<a href="/interventions/{{ p.intervention_id }}" class="link-action truncate" style="display:block;max-width:200px">
|
||||
{{ p.intervention_desc[:40] }}{% if p.intervention_desc|length > 40 %}…{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-xs text-muted">{{ p.date_intervention or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-cog"></i>
|
||||
<p>Aucune pièce enregistrée</p>
|
||||
<span class="text-xs text-muted">Les pièces s'affichent ici dès qu'elles sont ajoutées dans une intervention.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ── Infos & Notes ── -->
|
||||
<div class="card" style="padding:0" id="infos">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<i class="fas fa-paperclip text-secondary" style="margin-right:0.4rem"></i>
|
||||
Infos & 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 class="block text-xs font-medium text-slate-600 mb-1">Type</label>
|
||||
<select name="type" id="info-type" onchange="toggleInfoType()" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
||||
<option value="texte">📝 Texte</option>
|
||||
<option value="url">🔗 URL / Lien</option>
|
||||
<option value="image">🖼️ Image</option>
|
||||
<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 class="col-span-2">
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Titre (optionnel)</label>
|
||||
<input name="titre" placeholder="Ex: Manuel d'entretien, Lien forum..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
||||
<div style="grid-column:span 2">
|
||||
<label>Titre (optionnel)</label>
|
||||
<input name="titre" placeholder="Ex: Forum, Fiche technique, Photo moteur…">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div id="input-texte">
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Contenu *</label>
|
||||
<textarea name="contenu" rows="3" placeholder="Notes libres, infos techniques..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"></textarea>
|
||||
<label>Contenu</label>
|
||||
<textarea name="contenu" rows="2" placeholder="Notes, infos techniques…"></textarea>
|
||||
</div>
|
||||
<div id="input-url" class="hidden">
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">URL *</label>
|
||||
<input name="contenu" placeholder="https://..." class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
||||
<div id="input-url" style="display:none">
|
||||
<label>URL</label>
|
||||
<input name="contenu" placeholder="https://…">
|
||||
</div>
|
||||
<div id="input-image" class="hidden">
|
||||
<label class="block text-xs font-medium text-slate-600 mb-1">Photo *</label>
|
||||
<input type="file" name="photo" accept="image/*" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm">
|
||||
<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>
|
||||
<button type="submit" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600">Enregistrer</button>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-sm">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if infos %}
|
||||
<div class="divide-y divide-gray-100">
|
||||
{% for info in infos %}
|
||||
<div class="px-5 py-4 flex justify-between items-start hover:bg-gray-50">
|
||||
<div class="flex-1">
|
||||
{% if info.titre %}<p class="text-xs font-semibold text-slate-500 uppercase mb-1">{{ info.titre }}</p>{% endif %}
|
||||
{% if info.type == 'texte' %}
|
||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ info.contenu }}</p>
|
||||
{% elif info.type == 'url' %}
|
||||
<a href="{{ info.contenu }}" target="_blank" class="text-sm text-blue-500 hover:underline break-all">🔗 {{ info.contenu }}</a>
|
||||
{% elif info.type == 'image' %}
|
||||
<img src="{{ info.contenu }}" alt="{{ info.titre or 'image' }}" class="max-h-48 rounded-lg mt-1 cursor-pointer" onclick="window.open('{{ info.contenu }}','_blank')">
|
||||
{% endif %}
|
||||
<p class="text-xs text-slate-400 mt-1">{{ info.created_at[:10] }}</p>
|
||||
</div>
|
||||
<form method="post" action="/vehicules/{{ vehicule.id }}/infos/{{ info.id }}/delete" class="ml-4" onsubmit="return confirm('Supprimer ?')">
|
||||
<button class="text-red-400 hover:text-red-600 text-xs">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-6 text-center text-slate-400 text-sm">Aucune info pour l'instant.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Interventions -->
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-slate-800">🔧 Interventions ({{ interventions|length }})</h3>
|
||||
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 text-sm hover:underline">+ Nouvelle intervention</a>
|
||||
</div>
|
||||
{% if interventions %}
|
||||
<div class="divide-y divide-gray-100">
|
||||
{% for i in interventions %}
|
||||
<div class="px-5 py-4 hover:bg-gray-50">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<a href="/interventions/{{ i.id }}" class="font-medium text-slate-800 hover:text-orange-500">{{ i.description }}</a>
|
||||
{% if i.technicien %}<p class="text-xs text-slate-400 mt-1"><i class="fas fa-user-cog mr-1"></i>{{ i.technicien }}</p>{% endif %}
|
||||
<p class="text-xs text-slate-400 mt-1">{{ i.created_at[:10] }} • {{ i.nb_pieces }} pièce(s)</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 ml-4 flex-shrink-0">
|
||||
<span class="text-sm font-semibold text-slate-700">{{ "%.0f"|format(i.cout_main_oeuvre + i.cout_pieces) }} €</span>
|
||||
{% if i.statut == 'en_attente' %}<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">En attente</span>
|
||||
{% elif i.statut == 'en_cours' %}<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">En cours</span>
|
||||
{% else %}<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">Terminé</span>{% endif %}
|
||||
</div>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-6 text-center text-slate-400 text-sm">
|
||||
Aucune intervention.<br>
|
||||
<a href="/interventions/new?vehicule_id={{ vehicule.id }}" class="text-orange-500 hover:underline mt-1 inline-block">Créer la première →</a>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-paperclip"></i>
|
||||
<p>Aucune info pour l'instant</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleAddInfo() {
|
||||
const f = document.getElementById('add-info-form');
|
||||
f.style.display = f.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
function toggleInfoType() {
|
||||
const type = document.getElementById('info-type').value;
|
||||
document.getElementById('input-texte').classList.toggle('hidden', type !== 'texte');
|
||||
document.getElementById('input-url').classList.toggle('hidden', type !== 'url');
|
||||
document.getElementById('input-image').classList.toggle('hidden', type !== 'image');
|
||||
document.getElementById('input-texte').style.display = type === 'texte' ? 'block' : 'none';
|
||||
document.getElementById('input-url').style.display = type === 'url' ? 'block' : 'none';
|
||||
document.getElementById('input-image').style.display = type === 'image' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,44 +1,59 @@
|
||||
{% 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 %}
|
||||
<div class="mb-8">
|
||||
<a href="/vehicules" class="text-slate-500 hover:text-slate-700 text-sm flex items-center gap-1 mb-4"><i class="fas fa-arrow-left"></i> Retour</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800">{% if vehicule %}Modifier le véhicule{% else %}Nouveau véhicule{% endif %}</h2>
|
||||
|
||||
<a href="/vehicules" class="back-link"><i class="fas fa-arrow-left"></i> Retour aux véhicules</a>
|
||||
|
||||
<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 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>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Immatriculation *</label>
|
||||
<input name="immatriculation" value="{{ vehicule.immatriculation if vehicule else '' }}" required placeholder="AB-123-CD" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300 uppercase">
|
||||
<label>Immatriculation *</label>
|
||||
<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 class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Marque *</label>
|
||||
<input name="marque" value="{{ vehicule.marque if vehicule else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Marque *</label>
|
||||
<input name="marque" value="{{ vehicule.marque if vehicule else '' }}" required placeholder="Renault, Peugeot…">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Modèle *</label>
|
||||
<input name="modele" value="{{ vehicule.modele if vehicule else '' }}" required class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Modèle *</label>
|
||||
<input name="modele" value="{{ vehicule.modele if vehicule else '' }}" required placeholder="Clio, 308…">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Année</label>
|
||||
<input name="annee" type="number" value="{{ vehicule.annee if vehicule else '' }}" min="1900" max="2030" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Année</label>
|
||||
<input name="annee" type="number" value="{{ vehicule.annee if vehicule else '' }}" min="1900" max="2030" placeholder="2019">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Kilométrage</label>
|
||||
<input name="kilometrage" type="number" value="{{ vehicule.kilometrage if vehicule else '' }}" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<label>Kilométrage (km)</label>
|
||||
<input name="kilometrage" type="number" value="{{ vehicule.kilometrage if vehicule else '' }}" placeholder="85000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">VIN (numéro de châssis)</label>
|
||||
<input name="vin" value="{{ vehicule.vin if vehicule else '' }}" placeholder="17 caractères" maxlength="17" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-orange-300 uppercase">
|
||||
<label>VIN (numéro de châssis)</label>
|
||||
<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 class="flex gap-3 pt-4">
|
||||
<button type="submit" class="bg-orange-500 text-white px-6 py-2 rounded-lg hover:bg-orange-600 transition text-sm font-medium">Enregistrer</button>
|
||||
<a href="/vehicules" class="text-slate-500 px-6 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 text-sm">Annuler</a>
|
||||
|
||||
<div style="display:flex;gap:0.75rem;padding-top:0.5rem;border-top:1px solid var(--border)">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="/vehicules" class="btn-secondary">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+59
-37
@@ -1,58 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Véhicules - GarageManager{% endblock %}
|
||||
{% block title %}Véhicules — GarageManager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div><h2 class="text-2xl font-bold text-slate-800">Véhicules</h2><p class="text-slate-500 mt-1">{{ vehicules|length }} véhicule(s)</p></div>
|
||||
<a href="/vehicules/new" class="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-orange-600 transition flex items-center gap-2"><i class="fas fa-plus"></i> Nouveau véhicule</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow">
|
||||
<div class="p-4 border-b border-gray-100">
|
||||
<form method="get" class="flex gap-2">
|
||||
<input name="q" value="{{ q }}" placeholder="Immatriculation, marque, modèle, VIN..." class="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-300">
|
||||
<button type="submit" class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Rechercher</button>
|
||||
{% if q %}<a href="/vehicules" class="text-slate-500 px-3 py-2 text-sm flex items-center">✕</a>{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="page-header flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="page-title"><i class="fas fa-car text-electric" style="margin-right:0.5rem"></i>Véhicules</h1>
|
||||
<p class="page-subtitle">{{ vehicules|length }} véhicule(s) enregistré(s)</p>
|
||||
</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">
|
||||
{% if vehicules %}
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-slate-600 text-xs uppercase">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left">Immatriculation</th>
|
||||
<th class="px-6 py-3 text-left">Marque / Modèle</th>
|
||||
<th class="px-6 py-3 text-left">Année</th>
|
||||
<th class="px-6 py-3 text-left">VIN</th>
|
||||
<th class="px-6 py-3 text-left">Kilométrage</th>
|
||||
<th class="px-6 py-3 text-left">Interventions</th>
|
||||
<th class="px-6 py-3 text-left">Actions</th>
|
||||
<th>Immatriculation</th>
|
||||
<th>Marque / Modèle</th>
|
||||
<th>Année</th>
|
||||
<th>VIN</th>
|
||||
<th>Kilométrage</th>
|
||||
<th>Interventions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<tbody>
|
||||
{% for v in vehicules %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-bold text-slate-800">{{ v.immatriculation }}</td>
|
||||
<td class="px-6 py-4 font-medium text-slate-700">{{ v.marque }} {{ v.modele }}</td>
|
||||
<td class="px-6 py-4 text-slate-500">{{ v.annee or '-' }}</td>
|
||||
<td class="px-6 py-4 text-slate-400 text-xs font-mono">{{ v.vin or '-' }}</td>
|
||||
<td class="px-6 py-4 text-slate-600">{{ (v.kilometrage|string + ' km') if v.kilometrage else '-' }}</td>
|
||||
<td class="px-6 py-4"><span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">{{ v.nb_interventions }}</span></td>
|
||||
<td class="px-6 py-4 flex gap-3">
|
||||
<a href="/vehicules/{{ v.id }}" class="text-blue-500 hover:underline">Voir</a>
|
||||
<a href="/vehicules/{{ v.id }}/edit" class="text-orange-500 hover:underline">Modifier</a>
|
||||
<form method="post" action="/vehicules/{{ v.id }}/delete" class="inline" onsubmit="return confirm('Supprimer ce véhicule ?')">
|
||||
<button class="text-red-500 hover:underline">Supprimer</button>
|
||||
<tr>
|
||||
<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>
|
||||
<td class="font-medium">{{ v.marque }} {{ v.modele }}</td>
|
||||
<td class="text-secondary">{{ v.annee or '—' }}</td>
|
||||
<td class="mono text-xs text-muted">{{ v.vin or '—' }}</td>
|
||||
<td class="mono text-sm">{{ (v.kilometrage|string + ' km') if v.kilometrage else '—' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ v.nb_interventions }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-3">
|
||||
<a href="/vehicules/{{ v.id }}" class="link-action">Voir</a>
|
||||
<a href="/vehicules/{{ v.id }}/edit" class="link-edit">Modifier</a>
|
||||
<form method="post" action="/vehicules/{{ v.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer ce véhicule et toutes ses données ?')">
|
||||
<button class="link-danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-8 text-center text-slate-400">
|
||||
<i class="fas fa-car text-3xl mb-3"></i><p>Aucun véhicule trouvé</p>
|
||||
<a href="/vehicules/new" class="text-orange-500 hover:underline text-sm mt-2 inline-block">Ajouter le premier →</a>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-car"></i>
|
||||
<p>Aucun véhicule trouvé</p>
|
||||
<a href="/vehicules/new" class="link-action">Ajouter le premier →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user