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:
commit
526f68dad1
8 changed files with 409 additions and 0 deletions
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
0
AGENT_CONTEXT.md
Normal file
0
AGENT_CONTEXT.md
Normal file
0
AUTHORS.md
Normal file
0
AUTHORS.md
Normal file
106
MANUS_HANDOFF.md
Normal file
106
MANUS_HANDOFF.md
Normal 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
118
README.md
Normal 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
0
ROADMAP.html
Normal file
127
docs/schema.ts
Normal file
127
docs/schema.ts
Normal 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
58
todo.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue