# API mobile Lustra v1

Base URL locale: `http://localhost:3200/api/mobile/v1`

Toutes les réponses JSON suivent ce format:

```json
{ "success": true, "data": {} }
```

Erreur:

```json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Message clair",
    "details": {}
  }
}
```

## Authentification

L'API mobile n'utilise pas les sessions web `ag.sid` et ne remplace pas l'auth EJS existante. Elle utilise:

- access token signe HMAC, duree courte, env `MOBILE_ACCESS_TOKEN_TTL_SECONDS`, defaut 15 minutes;
- refresh token opaque, hash SHA-256 stocke en DB, env `MOBILE_REFRESH_TOKEN_TTL_DAYS`, defaut 30 jours;
- tables `mobile_refresh_tokens` et `push_tokens`.

Env recommande en production:

- `MOBILE_JWT_SECRET` avec au moins 32 caracteres;
- `SESSION_SECRET` conserve pour les sessions web.

Header authentifie:

```http
Authorization: Bearer ACCESS_TOKEN
```

## Endpoints

### `GET /`

Auth: non.

Retourne le statut de l'API.

### `POST /auth/login`

Auth: non.

Payload:

```json
{
  "companySlug": "audit-nettoyage-1781088147002",
  "email": "user@email.com",
  "password": "password",
  "deviceId": "iphone-15-pro",
  "deviceName": "iPhone"
}
```

Reponse:

```json
{
  "success": true,
  "data": {
    "accessToken": "...",
    "refreshToken": "...",
    "user": {
      "id": 1,
      "companyId": 1,
      "companySlug": "audit-nettoyage-1781088147002",
      "role": "admin",
      "name": "Nom",
      "email": "user@email.com",
      "capabilities": []
    }
  }
}
```

Erreurs: `LOGIN_PAYLOAD_INVALID`, `INVALID_CREDENTIALS`, `ACCOUNT_BLOCKED`.

### `POST /auth/refresh`

Auth: non.

Payload:

```json
{ "refreshToken": "...", "deviceId": "iphone-15-pro" }
```

Reponse: nouveau `accessToken`, nouveau `refreshToken`, `user`.

Le refresh token precedent est revoque pendant la rotation.

Erreurs: `REFRESH_TOKEN_REQUIRED`, `REFRESH_TOKEN_INVALID`.

### `POST /auth/logout`

Auth: non. Le refresh token suffit pour le revoquer.

Payload:

```json
{ "refreshToken": "..." }
```

Reponse:

```json
{ "success": true, "data": { "loggedOut": true } }
```

### `GET /me`

Auth: oui.

Roles: `admin`, `employee`, `client`, `fiduciaire`.

Reponse: utilisateur mobile courant.

### `GET /dashboard`

Auth: oui.

Roles:

- `admin`: stats principales, interventions du jour, messages recents;
- `employee`: interventions du jour, prochaines missions, resume des heures;
- `client`: devis/factures/interventions/messages visibles;
- `fiduciaire`: donnees comptables et exports.

### `GET /interventions`

Auth: oui.

Roles:

- `admin`: toutes les interventions de l'entreprise;
- `employee` avec `operations` ou `suite`: interventions equipe;
- `employee` terrain: interventions assignees ou dans `intervention_staff`;
- `client`: uniquement ses interventions.

Query:

- `page`, defaut `1`;
- `limit`, defaut `25`, max `100`;
- `status`;
- `updated_since`.

Reponse:

```json
{
  "success": true,
  "data": { "interventions": [] },
  "meta": { "page": 1, "limit": 25, "count": 0 }
}
```

### `GET /interventions/:id`

Auth: oui.

Memes roles que la liste. Retourne le detail, les rapports visibles et la checklist pour admin/employe.

### `GET /interventions/staff`

Auth: oui.

Roles: `admin`, employe avec capability `operations` ou `suite`.

Retourne les utilisateurs actifs assignables aux interventions (`admin`, `employee`).

### `POST /interventions/:id/assign`

Auth: oui.

Roles: `admin`, employe avec capability `operations` ou `suite`.

Payload:

```json
{ "assignedUserId": 14 }
```

`assignedUserIds` peut aussi etre fourni pour assigner une equipe. Le premier id devient `assigned_user_id`, toute la liste est synchronisee dans `intervention_staff`.

### `POST /interventions/:id/start`

Auth: oui.

Roles: `admin`, `employee` autorise sur l'intervention.

Effet: passe l'intervention en `en_cours`, `planning_status='in_progress'`.

### `POST /interventions/:id/report`

Auth: oui.

Roles: `admin`, `employee` autorise sur l'intervention.

Content-Type: `multipart/form-data` ou JSON sans fichiers.

Champs supportes:

- `arrival_status`
- `checklist_item[]`
- `checklist_done[]`
- `checklist_note[]`
- `before_photo`
- `after_photo`
- `before_note`
- `after_note`
- `materials_used`
- `client_signature_name`
- `client_signature_data`

Reponse:

```json
{
  "success": true,
  "data": {
    "report": {
      "id": 10,
      "interventionId": 4,
      "pdfUrl": "/api/mobile/v1/interventions/4/reports/10/pdf"
    }
  }
}
```

### `POST /interventions/:id/complete`

Auth: oui.

Roles: `admin`, `employee` autorise sur l'intervention.

Payload:

```json
{ "durationMinutes": 120, "workNote": "Termine sans reserve." }
```

Effet: passe l'intervention en `terminee`, renseigne `completed_at`.

### `GET /interventions/:id/reports/:reportId/pdf`

Auth: oui.

Controle `company_id`, intervention autorisee, et `customer_visible` pour les clients.

Retour: PDF.

### `GET /clients`

Auth: oui.

Roles: `admin`, employe avec `clients`, `clients_read`, `sales` ou `operations`.

Query: `page`, `limit`, `updated_since`.

Retour: liste DTO clients.

### `GET /quotes`

Auth: oui.

Roles:

- `admin` ou capability ventes/clients: devis entreprise;
- `client`: uniquement ses devis visibles, statuts `envoye`, `accepte`, `refuse`, `expire`.

Les brouillons ne sont pas exposes aux clients.

Query: `page`, `limit`, `updated_since`.

### `GET /quotes/:id`

Auth: oui.

Memes roles et filtres de visibilite que `GET /quotes`.

### `POST /quotes`

Auth: oui.

Roles: `admin`, employe avec capability ventes/clients/operations.

Payload:

```json
{
  "clientId": 12,
  "serviceType": "Nettoyage bureaux",
  "items": [
    { "description": "Main-d'oeuvre", "quantity": 3, "unit": "heure", "unitPrice": 65 }
  ],
  "interventionAddress": "Rue Exemple 1, Lausanne",
  "validUntil": "2026-07-12",
  "conditions": "Validite 30 jours",
  "notes": "Preparation depuis mobile"
}
```

Le backend recalcule les totaux et la TVA depuis les parametres entreprise, cree le PDF et retourne le devis.

### `POST /quotes/:id/accept`

Auth: oui.

Roles: `client` proprietaire du devis envoye, ou utilisateur avec acces ventes.

Payload:

```json
{ "signerName": "Marie Dubois", "signatureData": "data:image/png;base64,..." }
```

`signatureData` est optionnel pour la signature saisie par nom; s'il est fourni, il est stocke dans `signature_records`.

### `POST /quotes/:id/refuse`

Auth: oui.

Roles: `client` proprietaire du devis envoye/accepte, ou utilisateur avec acces ventes.

### `GET /quotes/:id/pdf`

Auth: oui via Bearer token.

Retour: PDF du devis. Les clients ne peuvent pas telecharger les brouillons.

### `GET /invoices`

Auth: oui.

Roles:

- `admin` ou capability `finance`: factures entreprise;
- `client`: uniquement ses factures visibles, statuts `envoyee`, `partiellement_payee`, `payee`, `en_retard`, `annulee`.

Les brouillons ne sont pas exposes aux clients.

Query: `page`, `limit`, `updated_since`.

### `GET /invoices/:id/pdf`

Auth: oui via Bearer token.

Retour: PDF de facture. Les clients ne peuvent pas telecharger les brouillons.

### `GET /messages`

Auth: oui.

Roles:

- `client`: ses conversations;
- `admin` ou capability `messages`: conversations entreprise.

Query:

- `threadId` optionnel pour retourner les messages d'une conversation.

### `POST /messages`

Auth: oui.

Client, nouvelle conversation:

```json
{ "subject": "Question", "body": "Bonjour..." }
```

Client/admin, reponse:

```json
{ "threadId": 12, "body": "Bonjour..." }
```

### `GET /time-entries`

Auth: oui.

Roles: `admin`, `employee`.

Query:

- `month` optionnel, format `YYYY-MM` (défaut: mois courant).

Reponse:

```json
{
  "success": true,
  "data": {
    "month": "2026-06",
    "totals": { "entries": 3, "minutes": 1350, "approvedMinutes": 450, "pendingEntries": 2, "expenses": 0 },
    "entries": [
      { "id": 12, "workDate": "2026-06-11", "minutes": 450, "status": "submitted", "interventionId": 4, "serviceType": "Nettoyage bureaux", "clientName": "Sofia Rossi", "workNote": "" }
    ],
    "interventions": [
      { "id": 4, "serviceType": "Nettoyage bureaux", "clientName": "Sofia Rossi", "startsAt": "2026-06-11 06:00:00", "status": "terminee" }
    ]
  }
}
```

### `POST /time-entries`

Auth: oui.

Roles: `admin`, `employee`. La saisie est toujours rattachée à l'utilisateur courant (`status='submitted'`, `source_type='mobile'`).

Payload:

```json
{
  "workDate": "2026-06-11",
  "morningStartTime": "08:00",
  "morningEndTime": "12:00",
  "afternoonStartTime": "13:00",
  "afternoonEndTime": "17:30",
  "expenseAmount": 12.5,
  "interventionId": 4,
  "workNote": "RAS"
}
```

La logique est celle du backend web `field_time_entries`: matin/apres-midi, frais, `status='submitted'`, `source_type='mobile'`. `duration`, `startTime`/`endTime` restent acceptes en compatibilite, mais l'app mobile utilise les quatre champs horaires.

### `GET /time-entries/team`

Auth: oui.

Roles: `admin`, employe avec capability `hr`, `suite` ou `operations`.

Query: `status` (`submitted` par defaut), `page`, `limit`.

### `POST /time-entries/:id/status`

Auth: oui.

Roles: `admin`, employe avec capability `hr`, `suite` ou `operations`.

Payload:

```json
{ "status": "approved" }
```

Erreurs: `TIME_ENTRIES_FORBIDDEN`, `TEAM_TIME_FORBIDDEN`, `TIME_ENTRY_NOT_FOUND`.

### `POST /push-tokens`

Auth: oui.

Roles: tous.

Payload:

```json
{
  "platform": "expo",
  "token": "ExponentPushToken[...]",
  "deviceId": "iphone-15-pro",
  "deviceName": "iPhone"
}
```

Plateformes acceptees: `ios`, `android`, `expo`, `web`.

### `POST /sync/offline-events`

Auth: oui.

Roles: tous, mais l'exploitation metier des evenements reste a implementer en phase suivante.

Payload:

```json
{
  "deviceId": "iphone-15-pro",
  "events": [
    {
      "clientId": "local-1",
      "type": "intervention_note",
      "payload": { "interventionId": 1, "note": "..." }
    }
  ]
}
```

Effet: insere dans `offline_queue_events` avec `sync_status='queued'`.

## Securite Phase 1

- L'API est montee avant les routes publiques pour eviter le catch-all public.
- Aucune route web EJS n'a ete supprimee.
- Les sessions web restent independantes.
- Les mutations API ne passent pas par CSRF.
- Toutes les requetes metier filtrent `company_id`.
- Les clients ne voient pas les devis/factures brouillons.
- L'acceptation web d'un devis refuse maintenant une signature manquante ou invalide.

## Points restants Phase 2

- Ajouter OpenAPI formel.
- Ajouter tests automatises API.
- Ajouter paiement mobile reel si necessaire.
- Ajouter vraie execution des evenements offline.
- Ajouter notifications push sortantes APNs/FCM/Expo.
- Ajouter endpoints creation/modification devis/factures si l'app patron doit ecrire.
