My App
Vue d'ensemble

Spécifications fonctionnelles

Spec fonctionnelle complète par module

Chaque module est une unité fonctionnelle autonome. Cette spec sert de contrat entre le produit et le développement.

Modules V1

#ModulePrioritéDescription
1Auth & ComptesP0Gestion des comptes admin et établissement
2ÉtablissementsP0CRUD des établissements
3WC (Toilets)P0CRUD des WC par établissement
4QR CodesP0Génération, personnalisation, impression
5SignalementP0Page publique de signalement
6NotificationsP0WhatsApp + Email + In-app (temps réel)
7Dashboard établissementP0Vue signalements, historique, analytics
8Dashboard adminP1Gestion globale des établissements
9Anti-spamP0Protection contre l'abus
10Temps réel (SSE)P0Push events vers le dashboard
11Job queueP0Notifications async, agrégation
12AnalyticsP2Statistiques avancées

Module 1 — Auth & Comptes

Rôles

RôleScopeCréation
super_adminAccès total, gestion des établissementsManuel (seed)
establishment_adminGestion de son établissementPar le super_admin
establishment_memberRéception alertes, changement statut signalementsPar l'establishment_admin

Fonctionnalités

  • Connexion magic link — L'admin Wcare envoie un lien par email. Clic → session créée.
  • Connexion email/password — Alternative pour les connexions régulières.
  • Session — Token de session via Better Auth, cookies httpOnly secure.
  • Première connexion — Option de définir un mot de passe.
  • Magic link expiré — Page "Lien expiré" avec bouton "Renvoyer un nouveau lien".

Isolation multi-tenant

Critique — Chaque requête authentifiée doit être scoped par establishmentId.

// Middleware tRPC qui injecte le scope établissement
const scopedProcedure = protectedProcedure.use(({ ctx, next }) => {
  const establishmentId = ctx.session.user.establishmentId;
  if (!establishmentId && ctx.session.user.role !== "super_admin") {
    throw new TRPCError({ code: "FORBIDDEN" });
  }
  return next({ ctx: { ...ctx, establishmentId } });
});

Toutes les queries dashboard passent par scopedProcedure qui garantit l'isolation.


Module 2 — Établissements

Champs

ChampTypeRequisDescription
namestringNom de l'établissement
typeenumhotel, restaurant, company, agency, other
addressstringAdresse postale
citystringVille
postalCodestringCode postal
countrystringPays (ISO 3166-1)
phonestringTéléphone principal
emailstringEmail de contact
logostringURL du logo (pour QR code)
whatsappNumberstringNuméro WhatsApp pour notifications
timezonestringTimezone IANA (ex: Europe/Paris)
isActivebooleanActif/désactivé

Opérations

  • Créer — Admin Wcare uniquement
  • Modifier — Admin Wcare + establishment_admin
  • Désactiver — Admin Wcare (soft delete). Les QR codes affichent "Service inactif"
  • Lister — Admin Wcare (tous) / Établissement (le sien)

Edge case : désactivation

Quand un établissement est désactivé :

  • Les QR codes redirigent vers une page "Ce service n'est plus disponible"
  • Les signalements en cours restent consultables mais aucun nouveau n'est accepté
  • Les notifications sont stoppées
  • Le compte reste accessible en lecture seule

Module 3 — WC (Toilets)

Champs

ChampTypeRequisDescription
namestringIdentifiant interne ("WC Lobby", "WC 1er étage")
locationstringLocalisation détaillée
floorstringÉtage
establishmentIdFKLien vers l'établissement
isActivebooleanActif/inactif

Opérations

  • Créer — establishment_admin (génère automatiquement le QR code)
  • Modifier — establishment_admin
  • Désactiver — establishment_admin (désactive aussi le QR code)
  • Supprimer — establishment_admin (soft delete, archive les signalements)

Module 4 — QR Codes

Champs

ChampTypeRequisDescription
toiletIdFKLien vers le WC
tokenstringToken unique rotatif (UUID v4)
tokenExpiresAtdatetimeDate d'expiration du token
previousTokenstringAncien token (redirect graceful 24h)
customizationjsonbLogo, couleurs, style
urlstringURL complète générée

Personnalisation

{
  "logo": "https://..../logo.png",
  "foregroundColor": "#000000",
  "backgroundColor": "#FFFFFF",
  "style": "rounded",
  "errorCorrection": "H"
}

Export

  • SVG — Aperçu web, scalable
  • PNG — Téléchargement rapide
  • PDF — Impression avec crop marks, format A6/A5/A4 au choix
  • Bulk PDF — Tous les QR codes d'un établissement en un seul PDF

Opérations

  • Générer — Auto à la création d'un WC
  • Personnaliser — establishment_admin (preview live avant sauvegarde)
  • Régénérer le token — Manuellement ou par cron (anti-spam)
  • Télécharger — Export PNG/SVG/PDF

Module 5 — Signalement

Page publique (/report/{toiletId}/{token})

Aucune authentification requise. L'utilisateur arrive via le QR code.

Champs du signalement

ChampTypeRequisDescription
toiletIdFKAuto (depuis l'URL)
categoryenumCatégorie du problème
predefinedIssueIdFKProblème prédéfini sélectionné
freeTextstringTexte libre (max 200 chars)
statusenumnew, seen, in_progress, resolved, ignored
fingerprintstringHash du device (anti-spam)
ipHashstringHash de l'IP (anti-spam)
idempotencyKeystringfingerprint + toiletId + timestamp(30s)

UX de signalement

  1. L'utilisateur scanne → page avec le nom de l'établissement et du WC
  2. Grille de problèmes prédéfinis (icônes + texte, tap unique)
  3. Option "Autre" → champ texte libre (200 chars max)
  4. Bouton "Envoyer" → confirmation "Merci !"
  5. Pas de retour possible — une fois envoyé, c'est fini

Le parcours complet doit prendre moins de 10 secondes.

Edge cases signalement

CasComportement
Token expiréPage "QR code expiré — scannez à nouveau sur place"
previousToken (< 24h)Redirect automatique vers le nouveau token
Établissement désactivéPage "Ce service n'est plus disponible"
WC désactivéPage "Ce WC n'est plus actif"
Doublon réseau (retry)Rejet silencieux via idempotency key (même réponse 200)
Rate limit atteintPage "Trop de signalements — réessayez plus tard"
Grace period tokenLe POST accepte un token valide au moment du GET initial (1h)

Module 6 — Notifications

Canaux

CanalDéclencheurContenu
In-app (SSE)Chaque signalementBadge + toast temps réel
EmailChaque signalementTemplate HTML via Resend
WhatsAppAgrégé (fenêtre de 5 min)Message template Meta

Configuration par utilisateur

Chaque membre de l'établissement configure ses propres préférences :

ParamètreTypeDéfaut
whatsappEnabledbooleanfalse
emailEnabledbooleantrue
inAppEnabledbooleantrue
whatsappNumberstring

La config est par user, pas par établissement. Le chef veut le WhatsApp, l'agent de ménage veut juste l'in-app.

Agrégation WhatsApp

Si 3+ signalements pour le même WC en 5 minutes → un seul message groupé.

Fallback chain

WhatsApp → si échec (3 retries) → Email de fallback
Email → si échec → Log + alerte admin
In-app → toujours (pas de dépendance externe)

Module 7 — Dashboard établissement

Pages et fonctionnalités

PageFonctionnalités clés
/dashboardCompteur actifs, dernier signalement, graphique 7j, hot spots
/dashboard/reportsListe paginée (cursor), filtres, bulk actions, badge "X nouveaux" SSE
/dashboard/reports/[id]Détail + timeline audit trail + changement de statut
/dashboard/toiletsCRUD des WC
/dashboard/qr-codesPreview live personnalisation, export PDF/PNG/SVG, bulk export
/dashboard/settingsParamètres établissement, notifications, équipe
/dashboard/teamGestion membres + leurs préférences de notification

Vue d'ensemble (landing dashboard)

┌──────────────────────────────────────────────────┐
│  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
│  │ ACTIFS  │  │ AUJOURD' │  │ RÉSOLUS │          │
│  │   12    │  │   HUI    │  │ AUJOURD │          │
│  │         │  │    8     │  │    5    │          │
│  └─────────┘  └─────────┘  └─────────┘          │
│                                                   │
│  ┌─ Dernier signalement ────────────────────┐    │
│  │ 🚽 WC Lobby — Toilettes sales            │    │
│  │ ⏱️ Il y a 3 minutes                      │    │
│  └───────────────────────────────────────────┘    │
│                                                   │
│  ┌─ 7 derniers jours ───────────────────────┐    │
│  │ ▓▓▓▓▓▓▓▓░░░░ Lun                        │    │
│  │ ▓▓▓▓░░░░░░░░ Mar                        │    │
│  │ ...                                       │    │
│  └───────────────────────────────────────────┘    │
│                                                   │
│  ┌─ Hot spots ──────────────────────────────┐    │
│  │ 1. WC 1er étage — 15 signalements/sem    │    │
│  │ 2. WC Lobby — 8 signalements/sem         │    │
│  └───────────────────────────────────────────┘    │
└──────────────────────────────────────────────────┘

Liste des signalements

  • Pagination : Cursor-based (pas offset — performant sur gros volumes)
  • Filtres : WC, statut, catégorie, plage de dates
  • Tri : Par date (défaut), par statut, par WC
  • Bulk actions : Sélectionner plusieurs → "Marquer comme résolu" / "Ignorer"
  • Temps réel : Badge "X nouveaux signalements" via SSE, clic pour charger
  • Recherche : Par texte libre du signalement

Détail signalement + audit trail

Chaque changement de statut est tracé :

NOUVEAU        16 avr 14:30    Signalement reçu

VU             16 avr 14:32    Vu par Jean Dupont

EN_COURS       16 avr 14:45    Passé en cours par Marie Martin

RÉSOLU         16 avr 15:10    Résolu par Marie Martin

Changement de statut concurrent

Si 2 membres modifient le même signalement en même temps :

// Optimistic locking via updatedAt
const updated = await db
  .update(report)
  .set({ status: newStatus, updatedAt: new Date() })
  .where(
    and(
      eq(report.id, reportId),
      eq(report.updatedAt, expectedUpdatedAt), // Optimistic lock
    ),
  )
  .returning();

if (updated.length === 0) {
  throw new TRPCError({
    code: "CONFLICT",
    message: "Ce signalement a été modifié. Veuillez rafraîchir.",
  });
}

Mobile-first

Le gérant est sur son téléphone, pas devant un PC.

  • Layout responsive, touch-friendly (h-14 min pour les targets)
  • Swipe pour changer de statut sur la liste
  • Bottom navigation sur mobile
  • Notifications push (PWA) pour le badge

Module 8 — Dashboard admin Wcare

Pages

PageDescription
/adminVue globale : établissements actifs, signalements totaux, volume 30j
/admin/establishmentsCRUD des établissements
/admin/establishments/[id]Détail + WC + signalements + envoyer magic link
/admin/establishments/newCréation + envoi automatique du magic link

Module 9 — Anti-spam

Voir ADR-05 et Opérations anti-spam

5 couches : token rotatif, rate limit IP, fingerprint device, cooldown 10 min, détection patterns.


Module 10 — Temps réel (SSE)

Architecture

Server (Elysia) ──SSE──→ Dashboard (Next.js)

Events

EventPayloadDéclencheur
report:new{ reportId, toiletId, category, createdAt }Nouveau signalement
report:updated{ reportId, status, updatedBy }Changement de statut
notification:new{ count }Nouvelle notification

Implémentation

// Server — Elysia SSE endpoint
app.get("/events/:establishmentId", ({ params, set }) => {
  set.headers["content-type"] = "text/event-stream";
  set.headers["cache-control"] = "no-cache";
  set.headers["connection"] = "keep-alive";

  // Stream d'events scoped par establishmentId
  return eventBus.subscribe(params.establishmentId);
});
// Client — Hook React
function useReportEvents(establishmentId: string) {
  useEffect(() => {
    const source = new EventSource(`/events/${establishmentId}`);
    source.addEventListener("report:new", (e) => {
      const data = JSON.parse(e.data);
      queryClient.invalidateQueries({ queryKey: ["reports"] });
      toast.info(`Nouveau signalement — ${data.category}`);
    });
    return () => source.close();
  }, [establishmentId]);
}

Module 11 — Job queue (notifications async)

Pourquoi

  • L'API WhatsApp peut être lente (1-3s) → le signalement ne doit pas attendre
  • L'agrégation WhatsApp nécessite un timer/scheduler
  • Les retries en cas d'échec doivent être gérés proprement
  • L'email peut aussi échouer → retry

Architecture

POST /report → INSERT report → ENQUEUE notification job

                              Job Runner (async)
                              ├── In-app: INSERT + SSE push
                              ├── Email: Resend API (retry x3)
                              └── WhatsApp: check aggregation window
                                  ├── window open → send (retry x3)
                                  └── window closed → skip (agrégé)

Implémentation

Deux options à évaluer :

  1. BullMQ + Redis — Robuste, battle-tested, mais dépendance Redis
  2. DB-based queue — Table job en PostgreSQL, poll toutes les secondes. Simple, pas de dépendance

En V1, la DB-based queue suffit. Migration vers BullMQ si le volume l'exige.


Module 12 — Analytics (V2)

  • Signalements par WC, par jour/semaine/mois
  • Temps moyen de résolution
  • Catégories les plus fréquentes
  • Comparaison entre WC
  • Heures de pointe
  • Export CSV
  • Taux de résolution vs ignoration

On this page