feat: QueueMed v1.0.0 — Documentation et handoff

- README.md complet avec stack, routes, architecture DB
- MANUS_HANDOFF.md avec état du projet et roadmap
- todo.md avec toutes les fonctionnalités
- Schema Drizzle de référence dans docs/

Fonctionnalités v1.0.0 :
- File d'attente virtuelle temps réel (WebSocket/Socket.io)
- QR code anti-triche rotatif par cabinet
- Interface patient mobile (position live, alertes)
- Écran d'affichage tablette/moniteur
- Tableau de bord médecin complet
- Analytics + export CSV
- Tickets imprimables
- Système d'abonnement (essai 30j, blocage)
- Multi-cabinets

Auteur: William MERI
This commit is contained in:
Tarzzan 2026-02-27 10:49:35 -05:00
commit 526f68dad1
8 changed files with 409 additions and 0 deletions

0
.gitignore vendored Normal file
View file

0
AGENT_CONTEXT.md Normal file
View file

0
AUTHORS.md Normal file
View file

106
MANUS_HANDOFF.md Normal file
View file

@ -0,0 +1,106 @@
# MANUS_HANDOFF — QueueMed v1.0.0
> Fichier de passation pour la continuité du développement.
> Dernière mise à jour : 2026-02-27 — William MERI
---
## État actuel du projet
**Version** : 1.0.0
**Statut** : Production-ready (fonctionnalités core complètes, Stripe à activer)
**Tests** : 8/8 passent, 0 erreurs TypeScript
**Checkpoint Manus** : `a63c623c`
---
## Ce qui est implémenté
### Backend (server/)
- `routers.ts` — 20+ procédures tRPC : auth, clinics, queue, analytics
- `db.ts` — Helpers Drizzle pour toutes les tables
- `_core/index.ts` — Serveur Express + Socket.io intégré
- Middleware `subscriptionProcedure` — Bloque les procédures si abonnement expiré
- Rotation automatique du QR code (configurable par cabinet)
- Procédure `analytics.exportCsv` — Export CSV des événements
### Frontend (client/src/)
- `pages/Home.tsx` — Landing page cinématique (hero, features, how-it-works, témoignages, pricing, CTA)
- `pages/Dashboard.tsx` — Tableau de bord médecin (KPIs, liste cabinets, actions rapides)
- `pages/DoctorClinics.tsx` — Gestion des cabinets (CRUD, QR code, paramètres)
- `pages/QueueManagement.tsx` — Gestion file temps réel (appel suivant, absent, retirer, imprimer ticket)
- `pages/Analytics.tsx` — Graphiques affluence, export CSV, recommandations IA
- `pages/PatientQueue.tsx` — Interface patient (position live, temps estimé, alertes)
- `pages/DisplayScreen.tsx` — Écran d'affichage tablette (numéro animé, ticker, connexion status)
- `pages/SubscriptionPage.tsx` — Page abonnement (plans, essai gratuit, blocage)
- `pages/SubscriptionBlocked.tsx` — Page de blocage après expiration
- `pages/PrintTicket.tsx` — Ticket imprimable pour patients sans smartphone
### Base de données (drizzle/schema.ts)
```
users, subscriptions, clinics, queueEntries, analyticsEvents
```
---
## Ce qui reste à faire
### Priorité haute
- [ ] **Intégration Stripe** — L'utilisateur doit fournir ses clés Stripe (STRIPE_SECRET_KEY, VITE_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET). Utiliser `webdev_add_feature stripe` une fois les clés configurées dans Settings → Payment.
- [ ] **Notifications SMS** — Intégrer Twilio pour alerter les patients par SMS quand leur tour approche (alternative aux notifications push navigateur).
### Priorité moyenne
- [ ] **Page `/subscription/plans`** — Checkout Stripe réel avec redirection vers portail client.
- [ ] **Webhook Stripe** — Route `/api/stripe/webhook` pour gérer `checkout.session.completed`, `invoice.paid`, `customer.subscription.deleted`.
- [ ] **Tests supplémentaires** — Couvrir les procédures queue.callNext, queue.markAbsent, analytics.get.
### Priorité basse
- [ ] **Mode multi-praticiens** — Plusieurs médecins par cabinet (table `clinicMembers`).
- [ ] **Rapports PDF** — Export PDF hebdomadaire automatique.
- [ ] **Application mobile** — React Native avec les mêmes APIs tRPC.
---
## Informations techniques importantes
### Socket.io
Le serveur Socket.io est initialisé dans `server/_core/index.ts` et exposé globalement via `(global as any).__socketIo`. Les procédures tRPC l'utilisent via `getIo()` dans `routers.ts`.
Rooms Socket.io :
- `clinic:{clinicId}` — Médecin + écran d'affichage
- `patient:{patientToken}` — Patient individuel
- `display:{clinicId}` — Écran d'affichage uniquement
### QR Code anti-triche
Le token QR est stocké dans `clinics.qrToken`. La rotation est déclenchée par `rotateQrToken()` dans `db.ts`. La fréquence est configurable via `clinics.qrRotationMinutes` (0 = pas de rotation).
### Abonnement
- Essai gratuit : 30 jours à partir de la première connexion
- Statuts : `trial``active``expired` / `canceled`
- Le middleware `subscriptionProcedure` vérifie `isSubscriptionActive()` avant chaque opération sensible
### Design system
- Thème : dark, palette teal (#0d9488) + orange (#f97316)
- Classes utilitaires custom : `.gradient-text`, `.glass-card`, `.glow-teal`, `.text-glow-teal`, `.queue-number`, `.animate-ticker`
- Police display : Inter (Google Fonts)
---
## Commandes utiles
```bash
pnpm dev # Démarrer le serveur de développement
pnpm test # Exécuter les tests Vitest
pnpm db:push # Pousser les migrations Drizzle
pnpm build # Build de production
npx tsc --noEmit # Vérifier les types TypeScript
```
---
## Contacts et ressources
- **Auteur** : William MERI
- **Dépôt GitHub** : https://github.com/Tarzzan/queue-med
- **Documentation Stripe** : https://stripe.com/docs/billing/subscriptions
- **Documentation Socket.io** : https://socket.io/docs/v4/

118
README.md Normal file
View file

@ -0,0 +1,118 @@
# QueueMed — Salle d'attente virtuelle pour cabinets médicaux
> **Plateforme SaaS** de gestion de file d'attente numérique pour les médecins qui reçoivent sans rendez-vous.
> Conçue et développée par **William MERI** — v1.0.0
---
## Présentation
QueueMed transforme la salle d'attente physique en une file d'attente virtuelle accessible depuis n'importe quel smartphone. Les patients scannent un QR code anti-triche à l'accueil, suivent leur position en temps réel et reçoivent une alerte quand leur tour approche — leur permettant d'attendre où ils le souhaitent.
Les médecins bénéficient d'un tableau de bord complet pour gérer la file, appeler les patients, consulter des analytics d'affluence et gérer plusieurs cabinets depuis une interface unifiée.
---
## Fonctionnalités principales
| Fonctionnalité | Description |
|---|---|
| **QR Code anti-triche** | Token unique rotatif par cabinet (configurable : 5 min à 24h). Impossible de partager ou falsifier sa position. |
| **File d'attente temps réel** | Mise à jour instantanée via WebSocket (Socket.io). Position, temps estimé, numéro de ticket. |
| **Écran d'affichage** | Interface tablette/moniteur avec numéro appelé animé, ticker défilant et indicateur de connexion. |
| **Interface patient** | Aucun compte requis. Scan QR → suivi live → alerte au bon moment. |
| **Tableau de bord médecin** | Gestion de file, appel du suivant, marquage absent, réinitialisation, statistiques du jour. |
| **Multi-cabinets** | Un médecin peut gérer plusieurs salles d'attente depuis un seul compte. |
| **Tickets imprimables** | Numéro unique pour les patients sans smartphone. Inclusion totale garantie. |
| **Analytics avancés** | Affluence par heure, par jour, temps d'attente moyen, recommandations IA, export CSV. |
| **Abonnement mensuel** | Essai gratuit 30 jours, puis blocage si non abonné. Prêt pour intégration Stripe. |
| **Notifications push** | Alertes navigateur quand le tour approche (Web Push API). |
---
## Stack technique
| Couche | Technologie |
|---|---|
| **Frontend** | React 19, Tailwind CSS 4, shadcn/ui, Recharts, Framer Motion |
| **Backend** | Express 4, tRPC 11, Socket.io 4 |
| **Base de données** | MySQL/TiDB via Drizzle ORM |
| **Auth** | Manus OAuth (JWT, session cookie) |
| **QR Code** | `qrcode` npm package |
| **Tests** | Vitest (8 tests, 0 erreurs TS) |
| **Déploiement** | Manus Hosting (CDN, HTTPS, domaine personnalisé) |
---
## Architecture de la base de données
```
users → Comptes médecins (OAuth)
subscriptions → Abonnements (trial/active/expired/canceled)
clinics → Cabinets médicaux (1 médecin → N cabinets)
queueEntries → Entrées dans la file (token anti-triche, position, statut)
analyticsEvents → Événements pour les graphiques d'affluence
```
---
## Routes de l'application
| Route | Description | Accès |
|---|---|---|
| `/` | Landing page (hero, features, pricing, témoignages) | Public |
| `/dashboard` | Tableau de bord médecin | Authentifié |
| `/dashboard/clinics` | Gestion des cabinets | Authentifié |
| `/dashboard/queue/:clinicId` | Gestion de la file en temps réel | Authentifié |
| `/dashboard/analytics` | Analytics et export CSV | Authentifié |
| `/display/:clinicId` | Écran d'affichage tablette/moniteur | Public |
| `/queue/:token` | Interface patient (suivi temps réel) | Public |
| `/ticket/:entryId` | Page ticket imprimable | Public |
---
## Variables d'environnement
Toutes les variables sont injectées automatiquement par la plateforme Manus. Aucune configuration manuelle requise.
Pour l'intégration Stripe (à activer) :
- `STRIPE_SECRET_KEY` — Clé secrète Stripe
- `VITE_STRIPE_PUBLISHABLE_KEY` — Clé publique Stripe
- `STRIPE_WEBHOOK_SECRET` — Secret webhook Stripe
---
## Lancer le projet en local
```bash
pnpm install
pnpm db:push
pnpm dev
```
---
## Tests
```bash
pnpm test
# 8 tests, 0 erreurs TypeScript
```
---
## Roadmap (prochaines versions)
- [ ] Intégration Stripe complète (checkout, webhooks, portail client)
- [ ] Notifications SMS (Twilio)
- [ ] Application mobile React Native
- [ ] Intégration agenda médecin (Doctolib API)
- [ ] Mode multi-praticiens par cabinet
- [ ] Rapports PDF automatiques hebdomadaires
---
## Auteur
**William MERI** — Conçu avec QueueMed v1.0.0
© 2026 QueueMed. Tous droits réservés.

0
ROADMAP.html Normal file
View file

127
docs/schema.ts Normal file
View file

@ -0,0 +1,127 @@
import {
int,
mysqlEnum,
mysqlTable,
text,
timestamp,
varchar,
boolean,
bigint,
float,
json,
} from "drizzle-orm/mysql-core";
// ─── Users (médecins) ────────────────────────────────────────────────────────
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
// ─── Subscriptions ───────────────────────────────────────────────────────────
export const subscriptions = mysqlTable("subscriptions", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
stripeCustomerId: varchar("stripeCustomerId", { length: 128 }),
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 128 }),
stripePriceId: varchar("stripePriceId", { length: 128 }),
plan: mysqlEnum("plan", ["trial", "basic", "pro"]).default("trial").notNull(),
status: mysqlEnum("status", ["trialing", "active", "past_due", "canceled", "expired"]).default("trialing").notNull(),
trialStartedAt: timestamp("trialStartedAt").defaultNow().notNull(),
trialEndsAt: timestamp("trialEndsAt").notNull(),
currentPeriodStart: timestamp("currentPeriodStart"),
currentPeriodEnd: timestamp("currentPeriodEnd"),
canceledAt: timestamp("canceledAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Subscription = typeof subscriptions.$inferSelect;
export type InsertSubscription = typeof subscriptions.$inferInsert;
// ─── Clinics (cabinets médicaux) ─────────────────────────────────────────────
export const clinics = mysqlTable("clinics", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
name: varchar("name", { length: 255 }).notNull(),
address: text("address"),
phone: varchar("phone", { length: 32 }),
color: varchar("color", { length: 16 }).default("#0d9488"),
isActive: boolean("isActive").default(true).notNull(),
// QR code token rotatif anti-triche
qrToken: varchar("qrToken", { length: 64 }).notNull(),
qrTokenExpiresAt: timestamp("qrTokenExpiresAt"),
qrRotationMinutes: int("qrRotationMinutes").default(30),
// Paramètres file d'attente
avgConsultationMinutes: int("avgConsultationMinutes").default(15),
maxQueueSize: int("maxQueueSize").default(50),
isQueueOpen: boolean("isQueueOpen").default(false).notNull(),
currentTicketNumber: int("currentTicketNumber").default(0).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Clinic = typeof clinics.$inferSelect;
export type InsertClinic = typeof clinics.$inferInsert;
// ─── Queue Entries (patients en file) ────────────────────────────────────────
export const queueEntries = mysqlTable("queue_entries", {
id: int("id").autoincrement().primaryKey(),
clinicId: int("clinicId").notNull(),
ticketNumber: int("ticketNumber").notNull(),
// Identifiant de session anonyme du patient
patientToken: varchar("patientToken", { length: 64 }).notNull(),
patientName: varchar("patientName", { length: 128 }),
patientPhone: varchar("patientPhone", { length: 32 }),
status: mysqlEnum("status", ["waiting", "called", "in_consultation", "done", "absent", "canceled"])
.default("waiting")
.notNull(),
position: int("position").notNull(),
joinedAt: timestamp("joinedAt").defaultNow().notNull(),
calledAt: timestamp("calledAt"),
consultationStartAt: timestamp("consultationStartAt"),
consultationEndAt: timestamp("consultationEndAt"),
estimatedWaitMinutes: int("estimatedWaitMinutes"),
notificationSent: boolean("notificationSent").default(false).notNull(),
// Pour l'impression de ticket
isPrinted: boolean("isPrinted").default(false).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type QueueEntry = typeof queueEntries.$inferSelect;
export type InsertQueueEntry = typeof queueEntries.$inferInsert;
// ─── Analytics Events ─────────────────────────────────────────────────────────
export const analyticsEvents = mysqlTable("analytics_events", {
id: int("id").autoincrement().primaryKey(),
clinicId: int("clinicId").notNull(),
eventType: mysqlEnum("eventType", [
"patient_joined",
"patient_called",
"patient_done",
"patient_absent",
"queue_opened",
"queue_closed",
]).notNull(),
ticketNumber: int("ticketNumber"),
waitMinutes: int("waitMinutes"),
consultationMinutes: int("consultationMinutes"),
queueSizeAtEvent: int("queueSizeAtEvent"),
hourOfDay: int("hourOfDay"),
dayOfWeek: int("dayOfWeek"),
metadata: json("metadata"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type AnalyticsEvent = typeof analyticsEvents.$inferSelect;
export type InsertAnalyticsEvent = typeof analyticsEvents.$inferInsert;

58
todo.md Normal file
View file

@ -0,0 +1,58 @@
# QueueMed Project TODO
## Phase 3 : Schéma DB & Design System
- [x] Schéma Drizzle : tables users, clinics, queue_entries, subscriptions, analytics_events
- [x] Migration DB (pnpm db:push)
- [x] Design system : palette teal/orange cinématique dans index.css
- [x] Polices Google Fonts (Inter + Space Grotesk)
## Phase 4 : Landing Page & Auth Médecin
- [x] Landing page cinématique avec hero, features, pricing
- [x] Page d'inscription / connexion médecin (OAuth Manus)
- [x] Middleware de vérification d'abonnement (trial/active/blocked)
- [x] Page de blocage abonnement expiré
## Phase 5 : Dashboard Médecin & QR Code
- [x] Dashboard médecin principal avec stats
- [x] Gestion multi-cabinets (CRUD clinics)
- [x] Génération QR code unique/aléatoire par cabinet (rotation anti-triche)
- [x] Interface gestion file d'attente (appel prochain, skip, fermer)
- [x] Affichage numéro en cours et temps estimé
## Phase 6 : Interface Patient & Écran d'Affichage
- [x] Page patient après scan QR code (sans compte requis)
- [x] Affichage position en temps réel dans la file
- [x] Estimation du temps d'attente en live
- [x] Écran d'affichage tablette/moniteur (route /display/:clinicId)
- [x] WebSocket server (Socket.io) pour mises à jour temps réel
- [x] Connexion WebSocket côté patient et écran d'affichage
## Phase 7 : Stripe & Abonnement
- [x] Plans d'abonnement mensuel (Basic 29€, Pro 59€)
- [x] Gestion essai gratuit 1 mois (auto-création à la première connexion)
- [x] Page de paiement et gestion abonnement dans le dashboard
- [x] Blocage automatique après expiration (subscriptionProcedure middleware)
- [ ] Intégration Stripe réelle (webdev_add_feature stripe) à activer
- [ ] Webhook Stripe pour renouvellement/expiration automatique
## Phase 10 : Améliorations UX & Notifications
- [ ] Page patient enrichie (progression animée, alertes)
- [ ] Écran d'affichage avec animation de numéro appelé
- [ ] Landing page : section "Comment ça marche" complète
- [ ] Notifications push navigateur (Web Push API)
- [ ] Export CSV des analytics
- [ ] README.md et MANUS_HANDOFF.md
- [ ] Push GitHub final
## Phase 8 : Analytics, Notifications & Tickets
- [x] Analytics : temps d'attente moyen, pics d'affluence, taux de présence
- [x] Graphiques recharts dans le dashboard médecin (barres, camembert)
- [x] Prédictions et recommandations IA basées sur l'historique
- [x] Impression de ticket numérique (page imprimable)
- [x] Attribution numéro unique pour patients sans téléphone (printTicket)
- [ ] Notifications push/SMS (Twilio) à intégrer
## Phase 9 : Tests, Audit & Documentation
- [x] Tests Vitest pour les procédures tRPC critiques (8 tests, 2 fichiers)
- [x] 0 erreur TypeScript
- [ ] Checkpoint final et commit GitHub