- Overview
- Key Features
- Tech Stack
- Architecture
- Project Structure
- Database Schema
- Real-time Events
- API Reference
- User Roles & Panels
- Keyboard Shortcuts
- PWA Support
- Getting Started
- Environment Variables
- Running Tests
- Admin Panel
- Contributing
CQMP is a full-stack, real-time clinic queue management system that eliminates the chaos of paper-based patient queuing. It connects receptionists, doctors, and waiting patients through a live-updating dashboard β ensuring smooth patient flow, minimal wait-time confusion, and a professional clinic experience.
Built on Laravel 13 + Laravel Reverb (WebSockets) on the backend, and React 19 + Zustand + TypeScript on the frontend, the system delivers sub-second queue updates to all connected screens simultaneously.
Authentication uses a custom JWT implementation (JwtService + JwtMiddleware) for stateless, cookie-free token auth β ideal for shared-hosting deployments where Sanctum sessions are impractical.
| Feature | Description |
|---|---|
| π’ Live Queue Board | Patient queue updates in real-time via WebSockets β no page refresh needed |
| π¨ Emergency Insert | Instantly push an emergency patient to the front, with automatic serial renumbering |
| π’ Serial Reordering | Move any patient to any position by entering a target serial number |
| π¨οΈ Thermal Slip Printing | One-click print of a formatted receipt with serial and estimated wait |
| βΈοΈ Queue Freeze / Resume | Pause walk-in registrations while keeping the existing queue intact |
| β±οΈ Estimated Wait Times | Auto-calculated from average consultation time + doctor delay |
| πΊ TV Display Mode | Full-screen patient-facing display β accessible at /tv without login |
| π Role-Based Access | Doctor, Receptionist, and Admin roles via Spatie Permission |
| π Audit Trail | Every action (call, complete, skip, emergency, reinsert, delete) is logged |
| π Dark / Light Mode | System-aware theme with instant toggle, persisted to localStorage |
| β¨οΈ Full Keyboard Control | Every core action has a single-key hotkey β no mouse required |
| π± PWA Installable | Install as a native-like app on Android, iOS, or desktop |
| π Doctor Delay Tracking | Doctors can log delays; wait times auto-update for all waiting patients |
| π Visitor Self-Booking | Public booking form (no login) β name only, phone optional |
| π‘οΈ Admin Panel | Filament-powered admin for managing clinics, doctors, and patients |
| Technology | Version | Purpose |
|---|---|---|
| PHP | 8.3+ | Runtime |
| Laravel | 13.x | Application framework |
| Laravel Reverb | 1.x | Native WebSocket server |
| Custom JWT | β | Stateless API token auth (JwtService + JwtMiddleware) |
| Laravel Filament | 5.x | Admin panel UI |
| Spatie Permission | 8.x | Role & permission management |
| SQLite / MySQL | β | Database (SQLite by default in dev) |
| Technology | Version | Purpose |
|---|---|---|
| React | 19.x | UI framework |
| TypeScript | 6.x | Type safety |
| Vite | 8.x | Build tool & dev server |
| Zustand | 5.x | Global state management |
| Laravel Echo | 2.x | WebSocket client |
| Pusher JS | 8.x | WebSocket transport adapter |
| Tailwind CSS | 3.x | Utility-first styling |
| Lucide React | β | Icon library |
| vite-plugin-pwa | β | Service Worker & PWA manifest |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CQMP Architecture β
ββββββββββββββββββ¬βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ€
β Frontend β Backend API β WebSocket Layer β
β React 19 + TS β Laravel 13 REST API β Laravel Reverb β
ββββββββββββββββββ€ β β
β LoginForm β POST /queue/open β ws://host:8080/app/.. β
β Receptionist β GET /queue/today β β
β Dashboard β POST /queue/create β Channels: β
β Doctor β POST /queue/call-next β ββ queue.{queueDayId} β
β Dashboard β POST /queue/complete β ββ doctor-queue.{docId} β
β TV Display β POST /queue/skip β β
β Visitor β POST /queue/reinsert β Events Fired: β
β Booking β POST /queue/emergency β ββ QueueCreated β
ββββββββββββββββββ€ POST /queue/freeze β ββ QueueUpdated β
β State (Zustandβ DELETE /queue/:id β ββ QueueCompleted β
β stores) β POST /doctor/delay β ββ QueueDeleted β
β ββ useQueue β β ββ EmergencyInserted β
β ββ useAuth β Public (no auth): β ββ QueueFrozen β
β ββ useTheme β GET /public/doctors β ββ QueueResumed β
β β GET /public/queue β ββ EstimatedTimeUpdated β
β β POST /public/book β β
β β β β
β β Admin Panel: β β
β β /admin (Filament) β β
ββββββββββββββββββ΄ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ
Staff Login: POST /api/v1/login { email, password }
β
βΌ
AuthController validates credentials
β
βΌ
JwtService::generate() β signed JWT (HS256, 8-hour TTL)
β
βΌ
Token returned in JSON β stored in localStorage ('cqmp_token')
β
βΌ
All protected requests: Authorization: Bearer {token}
β
βΌ
JwtMiddleware::handle() β validates signature + expiry
Unauthenticated visitor navigates to /tv
β
βΌ
TvDisplay detects isPublicView = true (no cqmp_token in localStorage)
β
βΌ
Uses publicApi β GET /api/v1/public/queue?doctor_id=N (no auth)
β Polls every 10 seconds (WebSocket requires auth)
βΌ
Queue displayed: currently called serial + waiting list
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Authenticated TV staff opens /tv
β
βΌ
TvDisplay detects isPublicView = false (token present)
β
βΌ
Uses authenticated api β GET /api/v1/queue/today?doctor_id=N
β + WebSocket (Echo/Reverb) for real-time push
βΌ
Queue displayed with instant WebSocket updates
Receptionist types name β clicks "Add Queue" [R]
β
βΌ
React β POST /api/v1/patients (create / find patient)
β
βΌ
React β POST /api/v1/queue/create
β
βΌ
QueueController β QueueEngine::createWalkIn()
β (DB transaction with row-level lock)
βΌ
QueueItem saved β broadcast(QueueCreated)
β
βΌ
Laravel Reverb β WebSocket β All subscribers on queue.{id}
β
βΌ
useQueueStore listener β state.items updated
β
βΌ
React re-renders: Doctor Dashboard + TV Display (instantly)
Clinic Queue Management Platform (CQMP)/
β
βββ backend/ # Laravel 13 API + Admin
β βββ app/
β β βββ Events/ # 9 WebSocket broadcast events
β β β βββ QueueCreated.php
β β β βββ QueueUpdated.php
β β β βββ QueueCompleted.php
β β β βββ QueueDeleted.php
β β β βββ EmergencyInserted.php
β β β βββ QueueFrozen.php
β β β βββ QueueResumed.php
β β β βββ QueueOpened.php
β β β βββ EstimatedTimeUpdated.php
β β β
β β βββ Filament/Resources/ # Admin panel CRUD resources
β β β βββ Clinics/
β β β βββ Doctors/
β β β βββ Patients/
β β β βββ Appointments/
β β β
β β βββ Http/
β β β βββ Controllers/Api/
β β β β βββ AuthController.php # Login / logout / me
β β β β βββ PatientController.php # Patient CRUD (phone optional)
β β β β βββ QueueController.php # All queue ops + public endpoints
β β β β
β β β βββ Middleware/
β β β βββ JwtMiddleware.php # Custom JWT auth guard
β β β
β β βββ Models/ # 11 Eloquent models
β β β βββ Clinic.php
β β β βββ Doctor.php
β β β βββ DoctorDelay.php
β β β βββ Patient.php
β β β βββ QueueDay.php
β β β βββ QueueItem.php
β β β βββ Appointment.php
β β β βββ Announcement.php
β β β βββ AuditLog.php
β β β βββ Receptionist.php
β β β βββ User.php
β β β
β β βββ Services/
β β βββ QueueEngine.php # Core queue business logic
β β βββ JwtService.php # JWT sign / verify (HS256)
β β βββ AuditService.php # Action audit logging
β β
β βββ database/migrations/ # 16 migrations
β βββ routes/api.php # All API routes under /api/v1
β βββ tests/Feature/
β βββ QueueApiTest.php # Feature tests for queue operations
β
βββ frontend/ # React 19 + TypeScript SPA
βββ public/
β βββ favicon.svg # Browser tab icon (queue + cross mark)
β βββ logo.svg # Full horizontal wordmark
β βββ icon-192.svg # PWA home screen icon
β βββ icon-512.svg # PWA splash / store icon
β βββ manifest.json # Web App Manifest
β
βββ src/
βββ components/
β βββ LoginForm.tsx # Booking portal + staff login modal
β βββ ReceptionistDashboard.tsx # Patient registration + queue management
β βββ DoctorDashboard.tsx # Doctor's call / complete / skip panel
β βββ TvDisplay.tsx # Public-facing live TV board
β
βββ hooks/
β βββ useKeyboardShortcut.ts # Global hotkey system (auto-disabled in inputs)
β
βββ store/
β βββ useQueueStore.ts # Queue state + WebSocket subscriptions
β βββ useAuthStore.ts # JWT token + user state
β βββ useThemeStore.ts # Dark / light theme
β
βββ utils/
βββ api.ts # Axios: auth instance + public instance
βββ echo.ts # Laravel Echo + Reverb WebSocket config
clinics
id | name | address | phone | timestamps
doctors
id | user_id | clinic_id | name | specialization
| average_consultation_time | is_available | timestamps
patients
id | name | phone (nullable) | notes | is_blocked
| blocked_reason | qr_identifier | timestamps
queue_days
id | doctor_id | clinic_id | date | status (opened|paused|closed)
| opened_by | opened_at | closed_at | timestamps
queue_items
id | queue_day_id | patient_id | serial_no | appointment_type
| status (Waiting|Called|Completed|Skipped)
| priority (Normal|Emergency) | estimated_wait
| called_at | completed_at | timestamps
appointments
id | patient_id | doctor_id | scheduled_at | status | notes | timestamps
doctor_delays
id | doctor_id | delay_minutes | reason | start_time | end_time | timestamps
audit_logs
id | user_id | action | target_patient_id | details
| ip_address | user_agent | created_at
announcements
id | clinic_id | message | is_active | timestamps
All events are broadcast over Laravel Reverb and received by Laravel Echo on the frontend. The TV display uses 10-second polling when accessed without a staff login (WebSocket channels require authentication).
| Event | Channel | Key Payload | Trigger |
|---|---|---|---|
QueueCreated |
queue.{id} |
queue_item |
New patient registered |
QueueUpdated |
queue.{id} |
queue_item |
Patient reordered / called |
QueueCompleted |
queue.{id} |
queue_item_id |
Doctor marks done |
QueueDeleted |
queue.{id} |
queue_item_id, doctor_id |
Entry removed |
EmergencyInserted |
queue.{id} |
queue_item |
Emergency patient added |
QueueFrozen |
queue.{id} |
queue_day |
Queue paused |
QueueResumed |
queue.{id} |
queue_day |
Queue unpaused |
QueueOpened |
doctor-queue.{docId} |
queue_day |
New queue day opened |
EstimatedTimeUpdated |
queue.{id} |
wait_times: {id: minutes} |
Any status change |
All routes are prefixed /api/v1. Protected routes require Authorization: Bearer {token}.
POST /api/v1/login Body: { email, password } β { token, user }
POST /api/v1/logout Invalidates current JWT
GET /api/v1/me Returns current user profileGET /api/v1/patients ?search=name_or_phone β paginated list
POST /api/v1/patients { name, phone? } β phone is optional
GET /api/v1/patients/{id}
PUT /api/v1/patients/{id}
DELETE /api/v1/patients/{id}GET /api/v1/queue/today ?doctor_id=1 β { queue_day, items[] }
POST /api/v1/queue/open { doctor_id, date? }
POST /api/v1/queue/create { queue_day_id, patient_id, serial_no? }
POST /api/v1/queue/call-next { queue_day_id }
POST /api/v1/queue/complete { queue_item_id }
POST /api/v1/queue/skip { queue_item_id }
POST /api/v1/queue/reinsert { queue_item_id, position }
POST /api/v1/queue/emergency { queue_day_id, patient_id }
POST /api/v1/queue/freeze { queue_day_id }
POST /api/v1/queue/resume { queue_day_id }
DELETE /api/v1/queue/{queueItem}POST /api/v1/doctor/delay { doctor_id, delay_minutes, reason }GET /api/v1/public/doctors List of available doctors (cached 60 s)
GET /api/v1/public/queue ?doctor_id=1 β read-only queue for TV display
POST /api/v1/public/book { name, phone?, doctor_id } β visitor self-booking
GET /api/v1/settings/public Clinic display settings (title, logo, etc.)TV Display uses
GET /public/queuewhen accessed without login. This endpoint returns only display-safe fields:serial_no,status,priority,estimated_wait, and patientname. Phone numbers and internal IDs are not exposed.
- Type patient name (phone is optional) and register instantly
- Look up existing patients by phone to auto-fill name
- Set a custom serial position or let the system assign one
- Insert emergency patients β all other serials shift automatically
- View live waiting list, current patient in chamber, skipped, and completed
- Reorder any patient: enter a target serial number to move them
- Print thermal receipt slips with serial number and wait estimate
- Delete queue entries
- Open a new queue day for the current session
- Call next patient β highest priority first, lowest serial within priority
- Mark the current patient as Completed or Skipped
- Freeze / Resume the queue (pauses new walk-in registrations)
- Log a delay in minutes β all waiting patient wait times update immediately
- View the full waiting list with live estimated times
- Accessible at
/tvβ no login required - Navigate to it directly in the browser; no button on the booking portal
- Full-screen animated board showing the current serial number being called
- Single-doctor mode: select a doctor to watch one queue
- Lobby mode: see all doctors' queues side-by-side
- Bilingual voice announcement (Bangla + English) when a serial is called
- When accessed without login: polls
GET /public/queueevery 10 seconds - When accessed with a staff token: uses WebSocket (Reverb) for instant updates
- No login required β main screen of the portal (
/) - Patient enters their name only (phone is optional)
- Selects a doctor and receives a serial number on screen
- Can download a token image for reference
- Accessible at
/adminwith admin credentials - Full CRUD for Clinics, Doctors, Patients, and Appointments
- Search, filter, and manage all records
- Role and permission assignment per user
All shortcuts are automatically disabled when the cursor is inside any
<input>or<textarea>.
| Key | Action |
|---|---|
V |
Open Visitor Booking form |
| Key | Action |
|---|---|
1 2 3 |
Select doctor by position in list |
B / Esc |
Go back to portal |
Q |
Sign out |
T |
Toggle dark / light theme |
| Key | Action |
|---|---|
N |
Focus Name field (primary input) |
F |
Focus Phone field (lookup) |
S |
Focus Custom Serial field |
R |
Register patient (normal queue) |
E |
Register patient (emergency β front of queue) |
B / Esc |
Back to doctor selection |
Q |
Sign out |
T |
Toggle theme |
| Key | Action |
|---|---|
1 2 3 |
Select doctor (on selection screen) |
O |
Open queue (new day) |
N |
Call next patient |
C |
Complete current patient |
S |
Skip current patient |
P |
Pause / Resume queue |
D |
Focus delay input |
B / Esc |
Back to portal |
Q |
Sign out |
T |
Toggle theme |
| Key | Action |
|---|---|
1 2 3 |
Select doctor |
N |
Focus name field |
F |
Focus phone field |
S |
Submit booking |
B / Esc |
Back to login |
CQMP is a fully installable Progressive Web App.
| Feature | Detail |
|---|---|
| Service Worker | Workbox-generated via vite-plugin-pwa |
| Offline Caching | All static assets cached; API calls are NetworkOnly |
| Auto-update | New SW activates immediately (skipWaiting + clientsClaim) |
| App Shortcuts | Long-press icon β jump to Receptionist Desk or Doctor Dashboard |
| Theme Color | Indigo #6366F1 (light) / Navy #0F172A (dark) |
Install:
- Android β Chrome menu β Add to Home Screen
- iOS β Safari Share β Add to Home Screen
- Desktop β Chrome/Edge address bar β install icon
- PHP 8.4.1+
- Composer 2.x
- Node.js 18+ with npm 9+
- SQLite (bundled) or MySQL / PostgreSQL
git clone https://github.com/beingmushfiq/cqmp.git
cd cqmp/backend
# Install deps, generate key, run migrations
composer setup
# Start all services in one terminal
composer devcomposer dev starts four processes concurrently with color-coded output:
| Process | Port | Description |
|---|---|---|
php artisan serve |
8000 | Laravel API |
php artisan reverb:start |
8080 | WebSocket server |
php artisan queue:listen |
β | Background job worker |
npm run dev (Vite) |
5173 | React frontend |
# ββ Backend βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
cd backend
composer install
php -r "file_exists('.env') || copy('.env.example', '.env');"
php artisan key:generate
# SQLite (default)
php -r "file_exists('database/database.sqlite') || touch('database/database.sqlite');"
php artisan migrate --seed
# Start each in a separate terminal
php artisan serve # Terminal 1
php artisan reverb:start # Terminal 2
php artisan queue:listen # Terminal 3
# ββ Frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
cd ../frontend
npm install
npm run dev| URL | Description |
|---|---|
http://localhost:5173 |
React SPA β visitor booking portal |
http://localhost:5173/tv |
TV Display (public, no login required) |
http://localhost:5173/login |
Staff login page |
http://localhost:8000/api/v1 |
REST API base |
ws://localhost:8080 |
WebSocket server (Reverb) |
http://localhost:8000/admin |
Filament Admin Panel |
For local development only. Change all passwords before deploying.
| Role | Password | |
|---|---|---|
| Receptionist | receptionist@cqmp.local |
password |
| Doctor | doctor@cqmp.local |
password |
| Admin | admin@cqmp.local |
password |
APP_NAME="Clinic Queue Management Platform"
APP_ENV=local
APP_URL=http://localhost:8000
# CORS β allow the frontend origin
FRONTEND_URL=http://localhost:5173
# Database β SQLite (default) or switch to MySQL
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=cqmp
# DB_USERNAME=root
# DB_PASSWORD=secret
# JWT authentication
JWT_SECRET=your-256-bit-secret-here
JWT_TTL=480 # Token lifetime in minutes (default: 8 hours)
# WebSocket via Laravel Reverb
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=cqmp-local
REVERB_APP_KEY=cqmp-reverb-key
REVERB_APP_SECRET=cqmp-reverb-secret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
# Passed to Vite (frontend reads these)
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"VITE_API_URL=http://localhost:8000/api/v1
VITE_REVERB_APP_KEY=cqmp-reverb-key
VITE_REVERB_HOST=127.0.0.1
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=httpCQMP uses a custom HS256 JWT implementation instead of Laravel Sanctum. This was chosen for shared-hosting compatibility β no session cookies, no database token table queries on every request.
| Step | Detail |
|---|---|
| Login | POST /api/v1/login returns a signed JWT |
| Storage | Token stored in localStorage as cqmp_token |
| Transport | Sent as Authorization: Bearer {token} on every protected request |
| Validation | JwtMiddleware verifies signature and expiry on each request |
| Expiry | 8 hours by default (configurable via JWT_TTL) |
| Logout | Token is discarded client-side; no server-side revocation needed |
The frontend maintains two separate Axios instances:
api β includes Bearer token, used for all staff/authenticated routes
publicApi β no token, used for /public/* endpoints and TV display
The api instance has a 401 interceptor that clears the token and redirects to /. The redirect is suppressed when on the /tv route, so the TV display remains visible even if a session expires.
cd backend
# Run all tests
php artisan test
# Run only queue API tests
php artisan test --filter=QueueApiTest
# Verbose output
php artisan test --filter=QueueApiTest --verboseThe QueueApiTest suite covers:
| Test Case | What It Verifies |
|---|---|
| Open queue | Queue day created for a doctor |
| Walk-in registration | Serial assigned, broadcast fired |
| Custom serial | Patient placed at specified position |
| Emergency insert | Patient jumps to front, others shift |
| Call next | Correct priority + serial ordering |
| Skip patient | Status changes, wait times recalc |
| Reinsert patient | Moved to target position, all serials corrected |
| Queue freeze | New walk-ins rejected with 422 |
| Queue resume | Walk-ins accepted again |
| Delete entry | Removed from DB, recalc triggered |
Visit http://localhost:8000/admin β log in with admin credentials.
| Section | Features |
|---|---|
| Clinics | Create, edit, view clinic profiles |
| Doctors | Assign to clinics, set consultation time, toggle availability |
| Patients | Search, block / unblock, view history |
| Appointments | Overview of all bookings and their current status |
The QueueEngine service is the heart of the system. All write operations run inside database transactions with row-level locking to prevent race conditions.
QueueEngine::openQueue() // Start a new queue day (idempotent)
QueueEngine::createWalkIn() // Atomic serial assignment with DB lock
QueueEngine::callNext() // Highest priority β lowest serial
QueueEngine::complete() // Mark done, recalculate wait times
QueueEngine::skip() // Move to Skipped, recalculate
QueueEngine::reinsert() // Move to target position, bump others
QueueEngine::insertEmergency() // Front of queue, all others shift +1
QueueEngine::freeze() // Block new walk-ins
QueueEngine::resume() // Re-enable walk-ins
QueueEngine::recalculateWaitTimes() // EWT = avg_time Γ position + delay
QueueEngine::deleteItem() // Remove and recalculateContributions are welcome! Please follow these steps:
# 1. Fork and create a feature branch
git checkout -b feature/your-feature-name
# 2. Make changes β backend and frontend are separate directories
# 3. Run backend tests
cd backend && php artisan test
# 4. Type-check the frontend
cd frontend && npx tsc -b --noEmit
# 5. Format PHP code
./vendor/bin/pint
# 6. Open a pull request with a clear description| Language | Tool | Command |
|---|---|---|
| PHP | Laravel Pint (PSR-12) | ./vendor/bin/pint |
| TypeScript | OxLint | npm run lint |
| Commits | Conventional Commits | feat:, fix:, docs:, refactor: |
This project is licensed under the MIT License. See LICENSE for details.
Built with β€οΈ for efficient, humane healthcare