Entités
Modèle de données et relations entre entités
Le modèle de données reflète directement les flux métier. Chaque entité correspond à un concept du domaine, pas à une abstraction technique.
Diagramme ER
erDiagram
User ||--o{ Session : "possède"
User ||--o{ Account : "possède"
User }o--|| Establishment : "appartient à"
User ||--o| UserNotificationConfig : "configure"
User ||--o{ ReportEvent : "agit sur"
Establishment ||--o{ Toilet : "contient"
Establishment ||--o{ User : "a des membres"
Establishment ||--o{ PredefinedIssue : "définit"
Toilet ||--|| QRCode : "possède"
Toilet ||--o{ Report : "reçoit"
Report ||--o{ Notification : "déclenche"
Report ||--o{ ReportEvent : "historise"
Report }o--o| PredefinedIssue : "référence"Entités principales
User
Utilisateur du dashboard (admin Wcare ou membre d'établissement).
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
name | varchar | Nom complet |
email | varchar (unique) | Email de connexion |
role | enum | super_admin, establishment_admin, establishment_member |
establishmentId | uuid (FK, nullable) | null pour super_admin |
emailVerified | boolean | Email vérifié |
createdAt | timestamp | Date de création |
updatedAt | timestamp | Date de mise à jour |
Establishment
Un établissement client (hôtel, restaurant, entreprise...).
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
name | varchar | Nom de l'établissement |
type | enum | hotel, restaurant, company, agency, other |
address | varchar | Adresse postale |
city | varchar | Ville |
postalCode | varchar | Code postal |
country | varchar | Pays (ISO 3166-1 alpha-2) |
timezone | varchar | Timezone IANA (Europe/Paris) |
phone | varchar (nullable) | Téléphone |
email | varchar | Email de contact |
logo | varchar (nullable) | URL du logo |
whatsappNumber | varchar (nullable) | Numéro WhatsApp |
isActive | boolean | Actif/désactivé |
createdAt | timestamp | Date de création |
updatedAt | timestamp | Date de mise à jour |
Toilet
Un WC individuel au sein d'un établissement.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
establishmentId | uuid (FK) | Lien vers l'établissement |
name | varchar | Nom interne ("WC Lobby") |
location | varchar (nullable) | Localisation détaillée |
floor | varchar (nullable) | Étage |
isActive | boolean | Actif/inactif |
createdAt | timestamp | Date de création |
updatedAt | timestamp | Date de mise à jour |
QRCode
Le QR code associé à un WC. Relation 1:1 avec Toilet.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
toiletId | uuid (FK, unique) | Lien vers le WC |
token | varchar (unique) | Token rotatif pour l'URL |
tokenExpiresAt | timestamp | Expiration du token |
previousToken | varchar (nullable) | Ancien token (redirect graceful 24h) |
previousTokenExpiresAt | timestamp (nullable) | Fin de la grace period de redirect |
customization | jsonb | Logo, couleurs, style |
createdAt | timestamp | Date de création |
updatedAt | timestamp | Date de mise à jour |
PredefinedIssue
Problème prédéfini associé à un établissement (personnalisable).
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
establishmentId | uuid (FK) | Issues globales de l'établissement |
category | enum | cleanliness, consumables, equipment, other |
label | varchar | Texte affiché ("Toilettes sales") |
icon | varchar (nullable) | Nom d'icône ou emoji |
sortOrder | integer | Ordre d'affichage |
isActive | boolean | Actif/inactif |
Report
Un signalement de problème par un utilisateur anonyme.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
toiletId | uuid (FK) | WC concerné |
category | enum | cleanliness, consumables, equipment, other |
predefinedIssueId | uuid (FK, nullable) | Issue prédéfinie sélectionnée |
freeText | varchar(200) (nullable) | Texte libre |
status | enum | new, seen, in_progress, resolved, ignored |
fingerprint | varchar | Hash device (anti-spam) |
ipHash | varchar | Hash IP (anti-spam) |
idempotencyKey | varchar (unique) | hash(fingerprint + toiletId + timestamp_30s) |
createdAt | timestamp | Date du signalement |
updatedAt | timestamp | Dernière modification (optimistic lock) |
resolvedAt | timestamp (nullable) | Date de résolution |
ReportEvent
Audit trail — chaque changement de statut d'un signalement.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
reportId | uuid (FK) | Signalement concerné |
status | enum | Nouveau statut |
userId | uuid (FK, nullable) | Qui a fait l'action (null si système) |
createdAt | timestamp | Date de l'événement |
Notification
Notification envoyée suite à un signalement.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
reportId | uuid (FK) | Signalement associé |
userId | uuid (FK) | Destinataire |
channel | enum | whatsapp, email, in_app |
status | enum | pending, sent, failed |
attempts | integer | Nombre de tentatives (max 3) |
sentAt | timestamp (nullable) | Date d'envoi |
error | text (nullable) | Message d'erreur si failed |
UserNotificationConfig
Préférences de notification par utilisateur (pas par établissement).
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
userId | uuid (FK, unique) | Utilisateur concerné |
whatsappEnabled | boolean | WhatsApp activé |
whatsappNumber | varchar (nullable) | Numéro WhatsApp personnel |
emailEnabled | boolean | Email activé |
inAppEnabled | boolean | In-app activé |
aggregationWindowMinutes | integer | Fenêtre d'agrégation WhatsApp (défaut: 5) |
NotificationJob
Queue de jobs pour l'envoi async des notifications.
| Champ | Type | Description |
|---|---|---|
id | uuid (PK) | Identifiant unique |
type | enum | send_whatsapp, send_email, send_inapp |
payload | jsonb | Données du job |
status | enum | pending, processing, completed, failed |
attempts | integer | Tentatives effectuées |
maxAttempts | integer | Max tentatives (défaut: 3) |
scheduledAt | timestamp | Quand exécuter (pour agrégation) |
startedAt | timestamp (nullable) | Début du processing |
completedAt | timestamp (nullable) | Fin du processing |
error | text (nullable) | Dernière erreur |
createdAt | timestamp | Date de création |
Transitions de statut autorisées
stateDiagram-v2
[*] --> new
new --> seen : Ouverture par un membre
new --> ignored : Auto-détecté spam
seen --> in_progress : Intervention démarrée
seen --> ignored : Jugé non pertinent
in_progress --> resolved : Problème corrigé
in_progress --> seen : Intervention annulée
resolved --> [*]
ignored --> [*]Pas de retour en arrière depuis
resolvedouignored. Si le problème revient, c'est un nouveau signalement.
Index recommandés
| Table | Colonnes | Type | Raison |
|---|---|---|---|
report | toiletId, createdAt DESC | btree | Liste des signalements par WC (dashboard) |
report | toiletId, status | btree | Compteurs par statut (dashboard overview) |
report | fingerprint, createdAt | btree | Anti-spam : cooldown par device |
report | ipHash, createdAt | btree | Anti-spam : rate limit par IP |
report | idempotencyKey | unique btree | Déduplications de signalement |
report | createdAt | btree | Pagination cursor-based |
qr_code | token | unique btree | Lookup par token (scan QR) |
qr_code | previousToken | btree | Redirect ancien token |
toilet | establishmentId | btree | Requêtes par établissement |
notification | reportId | btree | Notifications par signalement |
notification_job | status, scheduledAt | btree | Poll des jobs à exécuter |
report_event | reportId, createdAt | btree | Timeline d'un signalement |