🏗️ Architecture Technique
Stack complète, modèle de données, flux WebSocket et procédures tRPC.
Stack Technologique
| Couche |
Technologie |
Version |
Rôle |
| Frontend |
React |
19 |
Interface utilisateur réactive |
| Styling |
Tailwind CSS |
4 |
Utility-first CSS |
| Composants |
shadcn/ui |
latest |
Design system cohérent |
| Graphiques |
Recharts |
latest |
Visualisation analytics |
| Animations |
Framer Motion |
latest |
Transitions fluides |
| Routing |
Wouter |
latest |
Navigation SPA légère |
| Backend |
Express |
4 |
Serveur HTTP |
| API |
tRPC |
11 |
RPC typé end-to-end |
| Temps réel |
Socket.io |
4 |
WebSocket bidirectionnel |
| ORM |
Drizzle |
latest |
Query builder typé |
| Base de données |
MySQL / TiDB |
8+ |
Stockage relationnel |
| Auth |
Manus OAuth |
— |
SSO avec JWT |
| Tests |
Vitest |
latest |
Tests unitaires rapides |
| QR Code |
qrcode (npm) |
— |
Génération côté serveur |
Modèle de Données
Diagramme Entité-Relation
erDiagram
users ||--o{ subscriptions : "possède"
users ||--o{ clinics : "gère"
clinics ||--o{ queueEntries : "contient"
clinics ||--o{ analyticsEvents : "génère"
users {
int id PK
varchar openId UK
varchar name
varchar email
varchar avatarUrl
enum role
timestamp createdAt
}
subscriptions {
int id PK
int userId FK
varchar stripeCustomerId
varchar stripeSubscriptionId
enum plan
enum status
timestamp trialStartedAt
timestamp trialEndsAt
timestamp currentPeriodStart
timestamp currentPeriodEnd
}
clinics {
int id PK
int userId FK
varchar name
text address
varchar phone
varchar color
boolean isActive
varchar qrToken
timestamp qrTokenExpiresAt
int qrRotationMinutes
int avgConsultationMinutes
int maxQueueSize
boolean isQueueOpen
int currentTicketNumber
}
queueEntries {
int id PK
int clinicId FK
int ticketNumber
varchar patientToken
varchar patientName
enum status
int position
timestamp joinedAt
timestamp calledAt
int estimatedWaitMinutes
boolean notificationSent
boolean isPrinted
}
analyticsEvents {
int id PK
int clinicId FK
enum eventType
int ticketNumber
int waitMinutes
int consultationMinutes
int queueSizeAtEvent
int hourOfDay
int dayOfWeek
json metadata
}
Détail des Tables
| Table |
Lignes estimées |
Description |
users |
~100 |
Comptes médecins (OAuth Manus) |
subscriptions |
~100 |
Un abonnement par utilisateur |
clinics |
~200 |
Cabinets médicaux (multi par médecin) |
queue_entries |
~10 000/mois |
Entrées dans la file (historique) |
analytics_events |
~50 000/mois |
Événements pour graphiques |
Statuts des Entrées de File
┌──────────┐
│ waiting │ ← Patient rejoint la file
└────┬─────┘
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ called │ │ absent │ │ canceled │
└────┬─────┘ └──────────┘ └──────────┘
│
▼
┌───────────────────┐
│ in_consultation │
└────────┬──────────┘
│
▼
┌──────────┐
│ done │
└──────────┘
Procédures tRPC
Authentification
| Procédure |
Type |
Accès |
Description |
auth.me |
Query |
Public |
Utilisateur courant (ou null) |
auth.logout |
Mutation |
Protected |
Déconnexion (supprime cookie) |
Abonnement
| Procédure |
Type |
Accès |
Description |
subscription.get |
Query |
Protected |
Statut abonnement courant |
subscription.check |
Query |
Protected |
Vérifie si actif/trial |
Cabinets
| Procédure |
Type |
Accès |
Description |
clinic.list |
Query |
Protected |
Liste des cabinets du médecin |
clinic.get |
Query |
Protected |
Détails d'un cabinet |
clinic.create |
Mutation |
Subscription |
Créer un cabinet |
clinic.update |
Mutation |
Subscription |
Modifier un cabinet |
clinic.delete |
Mutation |
Subscription |
Supprimer un cabinet |
clinic.rotateQr |
Mutation |
Subscription |
Forcer rotation QR |
clinic.getQrData |
Query |
Protected |
Données pour QR code |
File d'Attente
| Procédure |
Type |
Accès |
Description |
queue.getEntries |
Query |
Protected |
Entrées de la file |
queue.open |
Mutation |
Subscription |
Ouvrir la file |
queue.close |
Mutation |
Subscription |
Fermer la file |
queue.callNext |
Mutation |
Subscription |
Appeler le suivant |
queue.markAbsent |
Mutation |
Subscription |
Marquer absent |
queue.remove |
Mutation |
Subscription |
Retirer de la file |
queue.reset |
Mutation |
Subscription |
Réinitialiser la file |
queue.join |
Mutation |
Public |
Patient rejoint (via QR) |
queue.getPatientStatus |
Query |
Public |
Position du patient |
queue.printTicket |
Mutation |
Subscription |
Générer ticket |
Analytics
| Procédure |
Type |
Accès |
Description |
analytics.get |
Query |
Protected |
Stats par cabinet |
analytics.getAll |
Query |
Protected |
Stats globales |
analytics.exportCsv |
Query |
Protected |
Export CSV |
Middleware d'Abonnement
Le middleware subscriptionProcedure intercepte chaque requête protégée et vérifie l'état de l'abonnement :
Requête tRPC → protectedProcedure → subscriptionProcedure → Procédure
│ │
│ ├── trial actif ? → ✅ Continuer
│ ├── abonnement actif ? → ✅ Continuer
│ └── expiré/annulé ? → ❌ FORBIDDEN
│
└── Pas authentifié ? → ❌ UNAUTHORIZED
Routes Frontend
| Route |
Composant |
Accès |
Description |
/ |
Home.tsx |
Public |
Landing page cinématique |
/dashboard |
Dashboard.tsx |
Auth |
Tableau de bord principal |
/dashboard/clinics |
DoctorClinics.tsx |
Auth |
Gestion cabinets |
/dashboard/queue/:id |
QueueManagement.tsx |
Auth |
Gestion file temps réel |
/dashboard/analytics |
Analytics.tsx |
Auth |
Graphiques et export |
/dashboard/subscription |
SubscriptionPage.tsx |
Auth |
Plans et paiement |
/dashboard/help |
Help.tsx |
Auth |
Centre d'aide (FAQ) |
/dashboard/onboarding |
Onboarding.tsx |
Auth |
Wizard 3 étapes |
/display/:clinicId |
DisplayScreen.tsx |
Public |
Écran salle d'attente |
/queue/:token |
PatientQueue.tsx |
Public |
Suivi patient |
/ticket/:entryId |
PrintTicket.tsx |
Public |
Ticket imprimable |
/qr-poster/:clinicId |
QrPoster.tsx |
Auth |
Affiche QR A4 |
/subscription/blocked |
SubscriptionBlocked.tsx |
Auth |
Page blocage |
Sécurité
Couches de Protection
| Couche |
Mécanisme |
Description |
| Authentification |
JWT + Cookie HttpOnly |
Session signée, Secure, SameSite=Lax |
| Autorisation |
Middleware tRPC |
protectedProcedure, subscriptionProcedure |
| Anti-triche |
Token QR rotatif |
Impossible de partager/falsifier sa position |
| Rate limiting |
Express middleware |
Protection contre les abus |
| Validation |
Zod schemas |
Validation côté serveur de toutes les entrées |
| CORS |
Express CORS |
Origines autorisées uniquement |
Token QR Anti-Triche
Rotation automatique (configurable) :
T=0min T=30min T=60min T=90min
│ │ │ │
▼ ▼ ▼ ▼
[Token A] [Token B] [Token C] [Token D]
│ │ │ │
└── Valide ────┘ │ │
└── Valide ────┘ │
└── Valide ────┘
Patient scan Token B à T=25min → ✅ Rejoint la file
Partage lien Token B à T=35min → ❌ Token expiré, nouveau QR requis
Structure du Projet
queue-med/
├── client/
│ ├── public/ ← Favicon, manifest PWA
│ └── src/
│ ├── pages/ ← 12 pages (Landing, Dashboard, Queue...)
│ ├── components/ ← shadcn/ui + composants métier
│ ├── hooks/ ← useAuth, useMobile
│ ├── lib/ ← trpc client, utils
│ ├── contexts/ ← ThemeContext
│ ├── App.tsx ← Routes + layouts
│ ├── main.tsx ← Providers
│ └── index.css ← Design system (glass, glow, gradients)
├── server/
│ ├── _core/ ← Framework (OAuth, Express, Socket.io)
│ ├── routers.ts ← 20+ procédures tRPC
│ ├── db.ts ← Helpers Drizzle
│ └── storage.ts ← S3 helpers
├── drizzle/
│ └── schema.ts ← 5 tables
├── docs/
│ └── schema.ts ← Copie documentée du schéma
├── shared/ ← Types partagés
└── tests/ ← 13 tests Vitest