# Système de ticketing support (MVP)

**Date :** 2026-06-11 · **Origine :** specs user (MVP Next/TS/Postgres). Support client intégré à l'app.

## Mapping rôles (décision, multi-tenant)
L'app est un SaaS multi-org. Donc :
- **client** = tout utilisateur authentifié (membre d'une org) — ouvre des tickets de support.
- **staff** = équipe ELEVA = **super-admins** (`SUPER_ADMIN_EMAILS`, gate `assertSuperAdmin` existant). Le staff voit/traite **tous** les tickets, cross-org.
Pas de nouveau champ `role` global (Better Auth) : on réutilise le gate super-admin pour « staff ».

## Données (migration Drizzle)
### `support_tickets`
- `id` text pk (uuid)
- `title` varchar(256) notNull
- `status` enum-like text notNull default `"open"` — `open | in_progress | resolved`
- `priority` text notNull default `"medium"` — `low | medium | high`
- `userId` text notNull → user.id (créateur), index
- `orgId` text → organization.id (contexte de l'org du créateur, cascade)
- `assignedTo` text nullable → user.id (staff qui gère)
- `createdAt` / `updatedAt` timestamptz
- index sur `status` (inbox tri)

### `support_messages` (fil de discussion)
- `id` text pk
- `ticketId` text notNull → support_tickets.id **onDelete cascade**, index
- `senderId` text notNull (user.id)
- `senderType` text notNull — `client | staff` (style des bulles)
- `content` text notNull (markdown basique)
- `createdAt` timestamptz

## Server Actions (`src/lib/support/actions.ts`, "use server", Zod)
- `createTicketAction({title, content, priority})` — auth requise ; insère le ticket (userId/orgId du caller, status open) + le 1er message (senderType client) ; email staff (slice 3). Zod : title 1..256, content non vide, priority enum.
- `postMessageAction(ticketId, content)` — auth ; vérifie l'accès (le client propriétaire OU un staff) ; refuse si ticket `resolved` (lock) ; insère message (senderType = staff si super-admin sinon client) ; bump `updatedAt` ; email à l'autre partie (slice 3).
- `getMyTicketsAction()` — tickets du caller (client).
- `getTicketThreadAction(ticketId)` — ticket + messages, accès vérifié (propriétaire ou staff).
- **Staff (super-admin gate)** `src/lib/support/staff-actions.ts` : `getStaffInboxAction({filter})` (mine/unassigned/open, tri priorité puis date), `assignTicketAction(ticketId, staffId|null)` (self-assign = caller), `setTicketStatusAction(ticketId, status)`. Toutes `assertSuperAdmin()` en tête.

## UI client (`/support`)
- **Création** : formulaire (Titre, Description = 1er message, Priorité) — dialog ou page.
- **Liste** : tableau des tickets du client (statut + priorité badges, date).
- **Discussion** : fil type chat (bulles client/staff stylées via senderType), indicateur de statut ; si `resolved` → formulaire de réponse **bloqué** (« Ticket résolu — rouvrez-en un nouveau si besoin »).

## UI staff (`/support/staff`, super-admin only)
- **Inbox** : table de tous les tickets actifs, triée Priorité puis Date.
- **Filtres rapides** : « Mes tickets » (assignedTo = moi) · « Non assignés » · « Tous les ouverts ».
- **Vue ticket** : le fil + actions : **S'assigner** (ou assigner à un staff) — affiche l'avatar/nom de l'assigné aux autres ; **Changer le statut** (open → in_progress → resolved).
- **Verrouillage concurrence** (MVP) : cliquer un ticket non assigné déclenche un self-assign en arrière-plan (`assignedTo = moi`) ; les autres voient qui le tient.

## Email (slice 3, Resend — `src/lib/email.ts` existant)
- Nouveau ticket → email au staff (« Nouveau ticket support »).
- Réponse staff → email au client (« Réponse à ton ticket »).
- Best-effort (waitUntil en prod), gated sur `RESEND_API_KEY` comme l'existant.

## Sécurité
- Toute action vérifie la session ; les actions staff **assertSuperAdmin** strict (pages + actions). Le client n'accède qu'à ses tickets (vérif propriétaire). Zod partout (pas de message vide, title borné). Rate-limit best-effort sur la création (suivi).

## Découpage
- **T1 — fondation** : migrations + actions client (create/list/thread/postMessage avec lock resolved) + UI client (/support : create, list, discussion). Le staff voit via super-admin mais l'inbox dédiée vient en T2.
- **T2 — espace staff** : inbox + filtres + assign + statut (super-admin).
- **T3 — emails** : notifications Resend.
