# BACKOFFICE.md — Référence technique complète Backend ↔ Android

> **Destinataire :** Agent Android (Android Studio / VS Code)
> Ce fichier documente **tout** ce que l'application Android doit connaître pour s'interfacer avec le back-office FleetApp.
> Base URL officielle : `https://admin.fleetapp.be` (identique à `https://decube.fleetapp.be`)

---

## SOMMAIRE

1. [Authentification & JWT](#1-authentification--jwt)
2. [Profil utilisateur](#2-profil-utilisateur)
3. [Véhicules](#3-véhicules)
4. [Tickets](#4-tickets)
5. [Communications](#5-communications)
6. [Contacts](#6-contacts)
7. [Thème visuel](#7-thème-visuel)
8. [Notifications push FCM](#8-notifications-push-fcm)
9. [Onboarding QR code](#9-onboarding-qr-code)
10. [Codes d'erreur HTTP](#10-codes-derreur-http)
11. [Modèle de données complet](#11-modèle-de-données-complet)
12. [Règles métier importantes](#12-règles-métier-importantes)

---

## 1. Authentification & JWT

### POST /api/login.php — Connexion utilisateur Android

Aucun header d'auth requis.

**Body :**
```json
{ "username": "nicolas.goossens@hotmail.com", "password": "motdepasse" }
```

**Réponse 200 :**
```json
{
  "token": "eyJ0eXAiOiJKV1Qi...",
  "user_id": 2,
  "nom": "Nicolas Goossens",
  "username": "nicolas.goossens@hotmail.com"
}
```

**Réponse 401 :**
```json
{ "error": "Identifiants invalides" }
```

> - Succès = HTTP 200 (pas de champ `"success"`)
> - JWT expire après **8 heures** (`JWT_EXPIRY = 28800` secondes)
> - Stocker le token dans `EncryptedSharedPreferences`
> - À chaque démarrage : vérifier expiration locale, si expiré → `GET /api/me.php` pour valider côté serveur

### Utilisation du JWT dans toutes les requêtes suivantes

```
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
```

Le JWT contient (payload décodable) :
```json
{
  "user_id": 2,
  "tenant_id": 1,
  "username": "nicolas.goossens@hotmail.com",
  "nom": "Nicolas Goossens",
  "iat": 1711234567,
  "exp": 1711262567
}
```

---

## 2. Profil utilisateur

### GET /api/me.php — Lire le profil

**Réponse 200 :**
```json
{
  "user": {
    "user_id": 2,
    "username": "nicolas.goossens@hotmail.com",
    "first_name": "Nicolas",
    "last_name": "Goossens",
    "phone": "0495/24.81.74",
    "company_id": 2,
    "company_name": "Monnaie SA"
  }
}
```

Usage : validation de session au démarrage + affichage AccountFragment.

---

### POST /api/me.php — Modifier le profil

**Body (mise à jour des infos) :**
```json
{ "first_name": "Nicolas", "last_name": "Goossens", "phone": "0495/24.81.74" }
```

**Body (changement de mot de passe) :**
```json
{
  "action": "change_password",
  "current_password": "ancien",
  "new_password": "nouveau"
}
```

**Réponse 200 :**
```json
{ "updated": true }
```

---

## 3. Véhicules

### GET /api/vehicles.php — Liste des véhicules accessibles

Filtrée automatiquement par les groupes assignés à l'utilisateur (JWT).

**Réponse 200 :**
```json
{
  "vehicles": [
    {
      "vehicle_id": 1,
      "plate": "1-XHS-856",
      "brand": "BMW",
      "model": "X3 20d",
      "fuel_type": "Diesel",
      "category_id": 2,
      "company_id": 2,
      "department": "Batiment",
      "status": "active",
      "ct_valid_until": "2027-01-09",
      "state_id": 6
    }
  ]
}
```

**Champs notables :**
| Champ | Type | Description |
|-------|------|-------------|
| `status` | `"active"` \| `"inactive"` | Véhicule actif ou supprimé (soft delete) |
| `ct_valid_until` | `"YYYY-MM-DD"` \| `null` | Date expiration contrôle technique |
| `state_id` | int \| `null` | FK vers `vehicle_states` (en service, panne, garage...) |
| `category_id` | int \| `null` | FK vers `vehicle_categories` |
| `fuel_type` | string | "Diesel", "Essence", "Hybride", "Électrique"... |

---

### GET /api/vehicle.php?id={id} — Détail d'un véhicule

**Réponse 200 :**
```json
{
  "vehicle": {
    "vehicle_id": 1,
    "plate": "1-XHS-856",
    "brand": "BMW",
    "model": "X3 20d",
    "fuel_type": "Diesel",
    "department": "Batiment",
    "company_id": 2,
    "company_name": "Monnaie SA",
    "status": "active",
    "ct_valid_until": "2027-01-09",
    "rdv_ct": null,
    "category_id": 2,
    "state_id": 6,
    "doc_ct_url": "https://admin.fleetapp.be/admin/uploads/vehicles/1-XHS-856/doc_ct.pdf",
    "doc_assurance_url": "https://admin.fleetapp.be/admin/uploads/vehicles/1-XHS-856/doc_assurance.pdf",
    "doc_immat_url": null,
    "doc_coc_url": null
  }
}
```

> Les URLs de documents sont `null` si le fichier n'existe pas (jamais une chaîne vide).
> Documents disponibles : `doc_ct_url`, `doc_assurance_url`, `doc_immat_url`, `doc_coc_url`
> Format : PDF — à ouvrir via un intent ou un PDF viewer intégré.

---

### GET /api/vehicles-search.php?q={texte} — Recherche véhicule

Minimum **1 caractère**. Résultats filtrés par les groupes de l'utilisateur.

**Réponse 200 :**
```json
{
  "vehicles": [
    { "vehicle_id": 1, "plate": "1-XHS-856", "brand": "BMW", "model": "X3 20d" }
  ]
}
```

Maximum 10 résultats. Recherche sur le champ `plate` (`LIKE %q%`).

---

### GET /api/issue-types.php — Types de problèmes (tickets)

Aucun paramètre. Retourne la liste fixe des types de problèmes.

**Réponse 200 :**
```json
{
  "issue_types": [
    { "key": "flat_tire",    "label": "Pneu crevé" },
    { "key": "battery",      "label": "Batterie" },
    { "key": "engine_light", "label": "Voyant moteur" },
    { "key": "glass",        "label": "Bris de glace" },
    { "key": "accident",     "label": "Accident" },
    { "key": "fuel",         "label": "Carburant" },
    { "key": "other",        "label": "Autre" }
  ]
}
```

> Le champ `title` d'un ticket contient la **clé** (ex: `"flat_tire"`), pas le libellé humain.
> L'app doit mapper la clé → libellé via cette liste.

---

## 4. Tickets

### GET /api/tickets.php — Liste des tickets de l'utilisateur

**Réponse 200 :**
```json
{
  "tickets": [
    {
      "id": 1,
      "public_code": "E25FF499-443",
      "vehicle_id": 2,
      "title": "flat_tire",
      "description": null,
      "priority": "MEDIUM",
      "status": "NEW",
      "created_at": "2026-03-01 17:55:08",
      "updated_at": "2026-03-01 17:55:08",
      "plate": "2-CGP-516",
      "brand": "BMW",
      "model": "X3 20d"
    }
  ]
}
```

---

### POST /api/tickets.php — Créer un ticket

**Body :**
```json
{
  "vehicle_id": 1,
  "title": "flat_tire",
  "description": "Pneu arrière gauche à plat",
  "priority": "MEDIUM"
}
```

> `priority` : `"LOW"` | `"MEDIUM"` (défaut) | `"HIGH"`
> `description` : optionnel

**Réponse 201 :**
```json
{
  "ticket": {
    "id": 31,
    "public_code": "A1B2C3D4-456",
    "vehicle_id": 1,
    "title": "flat_tire",
    "description": null,
    "priority": "MEDIUM",
    "status": "NEW"
  }
}
```

> ✅ Déclenche automatiquement une notification Slack avec boutons d'action pour le fleet manager.

---

### GET /api/ticket.php?id={id} — Détail d'un ticket + historique

**Réponse 200 :**
```json
{
  "ticket": {
    "id": 1,
    "public_code": "E25FF499-443",
    "title": "flat_tire",
    "description": null,
    "priority": "MEDIUM",
    "status": "NEW",
    "created_at": "2026-03-01 17:55:08",
    "updated_at": "2026-03-01 17:55:08",
    "vehicle": {
      "vehicle_id": 2,
      "plate": "2-CGP-516",
      "brand": "BMW",
      "model": "X3 20d"
    },
    "photos": [
      { "id": 1, "url": "https://admin.fleetapp.be/uploads/tickets/1_2_abc123.jpg" }
    ],
    "events": [
      {
        "event_type": "CREATED",
        "from_status": null,
        "to_status": "NEW",
        "note": null,
        "created_at": "2026-03-01 17:55:08"
      },
      {
        "event_type": "NOTE_ADDED",
        "from_status": null,
        "to_status": null,
        "note": "Pneu arrière gauche à plat",
        "created_at": "2026-03-01 18:44:41"
      },
      {
        "event_type": "STATUS_CHANGED",
        "from_status": "NEW",
        "to_status": "ACK",
        "note": null,
        "created_at": "2026-03-01 19:00:00"
      }
    ]
  }
}
```

**Types d'événements (`event_type`) :**
| Valeur | Description |
|--------|-------------|
| `CREATED` | Ticket créé |
| `NOTE_ADDED` | Message ajouté (utilisateur ou fleet manager) |
| `STATUS_CHANGED` | Changement de statut |
| `ASSIGNED` | Assignation à un technicien |

**Statuts possibles (`status`) :**
| Statut | Signification | Transitions possibles |
|--------|--------------|----------------------|
| `NEW` | Nouveau, pas encore traité | → ACK, REJECTED |
| `ACK` | Pris en compte | → IN_PROGRESS, WAITING, REJECTED |
| `IN_PROGRESS` | En cours de traitement | → WAITING, RESOLVED, REJECTED |
| `WAITING` | En attente (pièces, garage...) | → IN_PROGRESS, RESOLVED, REJECTED |
| `RESOLVED` | Résolu | → CLOSED, IN_PROGRESS (réouverture) |
| `CLOSED` | Clôturé définitivement | aucune |
| `REJECTED` | Rejeté | aucune |

> L'app utilise ce statut pour afficher les badges et l'état des tickets.
> TicketDetailFragment poll `GET /api/ticket.php?id=X` toutes les 5 secondes pour voir les mises à jour en temps réel.

---

### POST /api/ticket-message.php — Envoyer un message dans un ticket

**Body :**
```json
{ "ticket_id": 1, "message": "Pneu arrière gauche à plat" }
```

**Réponse 201 :**
```json
{
  "event": {
    "event_type": "NOTE_ADDED",
    "note": "Pneu arrière gauche à plat",
    "created_at": "2026-03-31 10:00:00"
  }
}
```

**Erreurs :**
- `400` — ticket_id manquant ou message < 2 caractères
- `404` — ticket introuvable (ou n'appartient pas à l'utilisateur)
- `409` — ticket clôturé (statut RESOLVED, CLOSED ou REJECTED) → lecture seule

> ✅ Envoie automatiquement le message en thread Slack du ticket.

---

### POST /api/ticket-photo.php — Uploader une photo

**Type** : `multipart/form-data`

**Champs :**
- `ticket_id` (int)
- `photo` (fichier image : JPEG / PNG / WebP, max 10 Mo)

**Réponse 201 :**
```json
{
  "photo": {
    "id": 1,
    "url": "https://admin.fleetapp.be/uploads/tickets/31_2_abc123.jpg",
    "filename": "31_2_abc123.jpg"
  }
}
```

> Stocker les photos localement dans `getFilesDir()/documents/PLAQUE/` pour accès offline.

---

## 5. Communications

### GET /api/communications.php — Annonces de la flotte

**Réponse 200 :**
```json
{
  "communications": [
    {
      "id": 5,
      "title": "Réunion flotte mars 2026",
      "content_html": "<!DOCTYPE html>...",
      "published_at": "2026-03-11 16:46:28"
    }
  ]
}
```

> Ordonnées par `published_at DESC`. Le `content_html` est un HTML complet — afficher dans un WebView.

**Badge non-lu :** Pas de tracking lu/non-lu côté serveur.
Implémenter localement : stocker le `id` de la dernière communication vue dans `SharedPreferences`.
`badge_count = communications.count { it.id > dernier_id_vu }`

**Notification push à la création (côté back-office) :**
Quand l'admin crée une communication, le serveur envoie automatiquement :
```json
{
  "type": "communication",
  "communication_id": "5",
  "title": "📢 Réunion flotte mars 2026",
  "body": "Nouvelle communication disponible"
}
```
via le topic FCM `fleet_comms`. L'app doit s'y abonner :
```java
FirebaseMessaging.getInstance().subscribeToTopic("fleet_comms");
```

---

## 6. Contacts

### GET /api/contacts.php — Contacts d'urgence

**Réponse 200 :**
```json
{
  "contacts": [
    { "id": 1, "name": "Touring secours",   "phone": "+32 78 17 81 78", "description": "Assistance routière" },
    { "id": 4, "name": "Nicolas Goossens",  "phone": "0495/24.81.74",   "description": "Fleet Manager" }
  ]
}
```

> Uniquement les contacts actifs (`is_active = 1`). Ordonnés par `name`.
> Permettre un appel direct via `tel:` intent sur le champ `phone`.

---

## 7. Thème visuel

### GET /api/theme.php — Couleurs du tenant

**Réponse 200 :**
```json
{
  "theme": {
    "toolbar_color": "#4A154B",
    "button_color":  "#4A154B",
    "card_bg_color": "#FFFFFF",
    "text_color":    "#000000"
  }
}
```

**Valeurs par défaut (si non configuré) :**
| Champ | Valeur |
|-------|--------|
| `toolbar_color` | `#4A154B` |
| `button_color` | `#4A154B` |
| `card_bg_color` | `#FFFFFF` |
| `text_color` | `#000000` |

> Appliquer au démarrage. Format `#RRGGBB` → parser avec `Color.parseColor()`.
> Mettre en cache locale, rafraîchir à chaque login.

---

## 8. Notifications push FCM

### POST /api/fcm-token.php — Enregistrer le token FCM

À appeler après login et à chaque fois que `FirebaseMessaging.getInstance().getToken()` retourne un nouveau token.

**Body :**
```json
{ "fcm_token": "fkVJGL9TRu-..." }
```

**Réponse 201 :**
```json
{ "saved": true }
```

---

### Types de notifications reçues

Toutes les notifications sont **data-only** (pas de bloc `notification`).
L'app doit construire la notification UI manuellement dans `onMessageReceived()`.

| `data.type` | Déclencheur | Champs `data` supplémentaires |
|------------|-------------|-------------------------------|
| `ticket_update` | Admin change statut via Slack | `ticket_id`, `status` |
| `communication` | Admin crée une annonce | `communication_id`, `title`, `body` |

**Exemple payload reçu pour un changement de statut :**
```json
{
  "type": "ticket_update",
  "ticket_id": "31",
  "status": "ACK",
  "title": "Ticket mis à jour",
  "body": "Statut : Pris en compte"
}
```

**Exemple payload reçu pour une communication :**
```json
{
  "type": "communication",
  "communication_id": "5",
  "title": "📢 Réunion flotte mars 2026",
  "body": "Nouvelle communication disponible"
}
```

**Comportement Android attendu :**
- `ticket_update` → afficher notification, si TicketDetailFragment ouvert → rafraîchir immédiatement
- `communication` → incrémenter badge, afficher notification

---

## 9. Onboarding QR code

Voir [QRCODE.md](QRCODE.md) pour le flux complet.

**Résumé :**
1. Admin génère QR → encode `https://admin.fleetapp.be/join.php?token=XXXX`
2. Scan → redirect Play Store avec `referrer=token%3DXXXX%26vps%3D...`
3. App lit referrer via **Play Install Referrer API** → extrait `token` + `vps_url`
4. App stocke `vps_url` dans `EncryptedSharedPreferences`
5. Utilisateur saisit le code 4 chiffres reçu séparément
6. App appelle `POST /api/join.php` avec `{ token, code }` + JWT

**GET /api/join.php?token=XXXX — Vérifier un token :**
```json
{ "valid": true, "company_id": 2, "company": "Monnaie SA", "expires_at": "2026-04-03 10:00:00" }
```

**POST /api/join.php — Activer :**
```json
// Body
{ "token": "abc123...", "code": "1234" }

// Réponse 200
{ "success": true, "company": "Monnaie SA", "company_id": 2 }
```

---

## 10. Codes d'erreur HTTP

| HTTP | Signification |
|------|---------------|
| 200 | Succès |
| 201 | Ressource créée |
| 400 | Paramètre manquant ou invalide |
| 401 | JWT absent, invalide ou expiré → renvoyer vers login |
| 403 | Accès refusé (hors périmètre) |
| 404 | Ressource introuvable |
| 405 | Mauvaise méthode HTTP |
| 409 | Conflit (ex: ticket clôturé, action impossible) |
| 410 | Token expiré ou déjà utilisé |
| 413 | Fichier trop lourd (photos > 10 Mo) |
| 415 | Format de fichier non supporté |
| 500 | Erreur serveur |

**Format erreur standard :**
```json
{ "error": "Message d'erreur lisible" }
```

---

## 11. Modèle de données complet

### Tables clés

#### connexion (utilisateurs Android)
| Colonne | Type | Description |
|---------|------|-------------|
| `ID` | int PK | Identifiant (= `user_id` dans JWT) |
| `tenant_id` | int | Tenant de l'utilisateur |
| `username` | varchar | Email de connexion |
| `userpassword` | varchar | Hash bcrypt |
| `first_name` | varchar | Prénom |
| `last_name` | varchar | Nom |
| `phone` | varchar | Téléphone |
| `company_id` | int FK | Société assignée (via QR code onboarding) |

#### vehicles
| Colonne | Description |
|---------|-------------|
| `vehicle_id` | PK |
| `plate` | Plaque (ex: `1-XHS-856`) |
| `brand` / `model` | Marque / modèle |
| `fuel_type` | Carburant |
| `company_id` | Société propriétaire |
| `department` | Département |
| `status` | `active` ou `inactive` |
| `ct_valid_until` | Date CT |
| `rdv_ct` | Date rendez-vous CT |
| `state_id` | État (FK vehicle_states) |
| `category_id` | Catégorie (FK vehicle_categories) |
| `doc_ct` / `doc_assurance` / `doc_immat` / `doc_coc` | Noms de fichiers PDF |

#### tickets
| Colonne | Description |
|---------|-------------|
| `id` | PK |
| `public_code` | Code lisible (ex: `E25FF499-443`) |
| `vehicle_id` | FK vehicles |
| `created_by` | FK connexion (user_id) |
| `title` | Clé issue_type (ex: `flat_tire`) |
| `description` | Texte libre (nullable) |
| `priority` | `LOW` / `MEDIUM` / `HIGH` |
| `status` | Voir [statuts](#statuts-possibles-status) |
| `slack_message_ts` | Timestamp Slack du message principal (pour threading) |

#### groups & filtrage d'accès véhicules
```
connexion (user_id) 
    → user_groups (user_id, group_id)
    → groups (filter_company_id, filter_department) 
    → vehicles filtrés par company_id et/ou department

OU

groups (sans filtre) 
    → vehicle_groups (group_id, vehicle_id) 
    → liste explicite de véhicules
```
Un admin assigne des groupes à l'utilisateur dans **Admin → Utilisateurs → Modifier**.

#### user_category_access — accès par catégorie
- Si l'utilisateur a des lignes dans cette table → il ne voit que les véhicules des catégories listées.
- Si aucune ligne → il voit tous les véhicules de ses groupes.

---

## 12. Règles métier importantes

### Filtrage des véhicules
L'app ne doit **jamais** afficher tous les véhicules du tenant.
`GET /api/vehicles.php` applique déjà le filtre côté serveur — l'app n'a rien à faire.

### Tickets — règles d'affichage
- Un utilisateur ne voit que **ses propres tickets** (`created_by = user_id`)
- Un ticket en `RESOLVED`, `CLOSED` ou `REJECTED` est **en lecture seule** (plus de messages ni de photos)
- Le champ `title` est une **clé** (`flat_tire`) → toujours mapper via `GET /api/issue-types.php`

### Photos de tickets
- Stocker localement dans `getFilesDir()/documents/{PLATE}/` pour accès offline
- Upload via `POST /api/ticket-photo.php` avec `multipart/form-data`
- L'URL retournée (`https://admin.fleetapp.be/uploads/tickets/...`) est publiquement accessible (pas de JWT requis pour télécharger le fichier)

### JWT — gestion de l'expiration
- Expiration : 8 heures
- Stocker `expires_at` localement (calculer : `iat + 28800`)
- Si expiré : `GET /api/me.php` → si 401 → renvoyer vers login
- Ne pas envoyer de requête avec un JWT expiré → 401 systématique

### Communications — badge
- Pas de tracking côté serveur
- Stocker en local : `last_seen_communication_id` dans `SharedPreferences`
- Badge = `communications.filter { it.id > lastSeenId }.count()`
- Mettre à jour `last_seen_communication_id` quand l'utilisateur ouvre l'onglet

### Notifications push — topic FCM
- S'abonner au topic `fleet_comms` après chaque login :
  ```java
  FirebaseMessaging.getInstance().subscribeToTopic("fleet_comms");
  ```
- Se désabonner à la déconnexion :
  ```java
  FirebaseMessaging.getInstance().unsubscribeFromTopic("fleet_comms");
  ```

### Documents PDF — URLs
- Format : `https://admin.fleetapp.be/admin/uploads/vehicles/{PLATE}/{type}.pdf`
- Types : `doc_ct.pdf`, `doc_assurance.pdf`, `doc_immat_url`, `doc_coc_url`
- Valeur `null` dans l'API = fichier inexistant → masquer le bouton dans l'UI

### Multi-tenant
- Le `tenant_id` est extrait du JWT côté serveur — l'app **n'a pas à le gérer**
- Tous les endpoints filtrent automatiquement par tenant
- Le `vps_url` détermine sur quel VPS (quel tenant) l'utilisateur est connecté

---

## Récapitulatif de tous les endpoints

| Méthode | Endpoint | Auth | Fonctionnalité |
|---------|----------|------|----------------|
| POST | `/api/login.php` | Non | Login → JWT |
| GET | `/api/me.php` | JWT | Lire profil |
| POST | `/api/me.php` | JWT | Modifier profil / mot de passe |
| GET | `/api/vehicles.php` | JWT | Liste véhicules (filtrée par groupes) |
| GET | `/api/vehicle.php?id=X` | JWT | Détail véhicule + URLs documents PDF |
| GET | `/api/vehicles-search.php?q=` | JWT | Recherche véhicule par plaque (min 1 car.) |
| GET | `/api/issue-types.php` | JWT | Types de problèmes pour tickets |
| GET | `/api/tickets.php` | JWT | Liste tickets de l'utilisateur |
| POST | `/api/tickets.php` | JWT | Créer ticket (→ Slack) |
| GET | `/api/ticket.php?id=X` | JWT | Détail ticket + photos + historique |
| POST | `/api/ticket-message.php` | JWT | Ajouter message au ticket (→ Slack) |
| POST | `/api/ticket-photo.php` | JWT | Uploader photo (multipart) |
| GET | `/api/communications.php` | JWT | Annonces de la flotte |
| GET | `/api/contacts.php` | JWT | Contacts d'urgence |
| GET | `/api/theme.php` | JWT | Couleurs du tenant |
| POST | `/api/fcm-token.php` | JWT | Enregistrer token push FCM |
| GET | `/api/join.php?token=X` | Non | Vérifier token QR code |
| POST | `/api/join.php` | JWT | Activer compte via token QR |
