PharmScan is a role-based pharmacy safety and dispensing workflow app with:
- Pharmacist-side prescription verification and quantity validation
- AI-assisted counselling recommendations at handover
- Manager-side analytics, anomaly detection, and action briefs
The project has a React frontend, Node.js/Express backend, and MySQL database.
- API.md - endpoint reference, auth, request/response examples
- ARCHITECTURE.md - system architecture, data flow, and key design decisions
- TESTING.md - unit and E2E test strategy and run commands
- Secure login with JWT
- Separate flows for pharmacist and manager
- Session expiry handling (token invalid/expired)
- Prescription queue view
- Drug verification using:
- Search
- Barcode input
- Camera simulation mode
- Real-time Safety Verification (3-layer check):
- Generic Match Check (BLOCKS if mismatch)
- Scanned drug generic name + strength must match prescription
- Mismatch shows RED ALERT modal: "WRONG MEDICINE - GENERIC MISMATCH"
- NO override allowed—pharmacist must re-scan correct drug
- Critical patient safety control
- Brand Match Check (ASKS for reason if mismatch)
- Scanned brand vs. prescribed brand comparison
- Mismatch (but same generic) shows YELLOW modal asking for reason
- Reason options: "Patient asked for this brand" / "Prescribed brand out of stock" / "Other"
- Reason is captured and logged for audit trail
- Quantity Check (SILENT processing)
- Under/over quantities proceed without showing warning modal
- Mismatch status tracked in logs for analytics
- Unit conversion support (drops, sprays, puffs, mL, tablets, capsules, etc.)
- Generic Match Check (BLOCKS if mismatch)
- Match classification:
- Perfect match (generic + strength + brand)
- Brand difference (same generic, different brand requires reason)
- Extra drug (detected and blocked if generic mismatch)
- Missing drug
- Partial completion support when all prescribed medicines cannot be added
- Mandatory reason selection before partial completion:
- Patient asked
- Out of stock
- Other
- In counselling review, partial items are shown as Not Dispensed (not as mismatch/missing)
- Partial completion path does not require generic mismatch override (already prevented at scan stage)
- Backend endpoint generates counselling recommendations per drug
- Dynamic token budgeting to reduce truncation issues
- Timeout handling and safe fallback recommendations
- Key normalization for robust matching (generic + strength + brand)
- Frontend status badges:
- AI Live
- Fallback
- AI Offline
- Every verification/handover is logged to MySQL
- Stores scanned drugs, prescribed drugs, and match summary
- Captures override reasons for mismatch handling
- KPI summary (dispenses, mismatches, quantity anomalies, accuracy)
- Trend and watchlist analytics from real dispense logs
- Drug-level quantity mismatch watchlists
- Pharmacist performance analytics
- AI-generated operations brief with targeted actions
- Safety and anomaly sections powered by live backend data
- Frontend: React, Vite, React Router, Recharts
- Backend: Node.js, Express, JWT, bcrypt
- Database: MySQL (mysql2)
- AI Provider: OpenRouter-compatible chat completion API
- frontend: React application
- backend: Express API and database scripts
- demo-data: sample prescription JSON
- pharmscan.sql: exported MySQL dump for quick local setup
- Node.js 18+ (recommended)
- npm 9+
- MySQL 8+ (or compatible)
Backend:
cd backend
npm installFrontend:
cd ../frontend
npm installCreate a database named pharmscan in MySQL.
Example SQL:
CREATE DATABASE pharmscan;Create backend/.env with your local values:
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=
DB_NAME=pharmscan
PORT=5000
LLM_BASE_URL=https://openrouter.ai/api/v1
LLM_API_KEY=your_api_key_here
LLM_MODEL=openai/gpt-4o-mini
LLM_TIMEOUT_MS=7000
JWT_SECRET=put_a_long_random_secret_hereConfiguration Details:
-
LLM_API_KEY: Get your OpenRouter API key:
- Visit https://openrouter.ai
- Sign up or log in
- Navigate to API Keys section
- Generate a new key and copy it here
-
JWT_SECRET: Use a long random string (at least 32 characters)
- Example:
openssl rand -base64 32(Linux/Mac) or[System.Convert]::ToBase64String((Get-Random -InputObject (0..255) -Count 32))(PowerShell)
- Example:
Preferred: import the root SQL dump first.
From the project root:
mysql -u root -p pharmscan < pharmscan.sql- Open phpMyAdmin
- Select database pharmscan
- Go to Import
- Choose pharmscan.sql
- Run import
Use this only when you are not importing pharmscan.sql.
cd backend
node init_db.jsThis creates tables and inserts default users:
- Pharmacist: username pharmacist, password password123
- Manager: username manager, password admin123
Use this only when you are not importing pharmscan.sql, or if your dump does not include drug rows.
cd backend
npm run seed:drugscd backend
npm run devBackend runs at http://localhost:5000
cd frontend
npm run devFrontend runs at the Vite URL shown in terminal (usually http://localhost:5173).
This repository already includes pharmscan.sql.
After importing pharmscan.sql:
- Skip backend initialization and seed scripts unless you intentionally want a reset
- Start backend and frontend directly
Backend:
- npm run dev
- npm run start
- npm run seed:drugs
Frontend:
- npm run dev
- npm run build
- npm run preview
- npm test
- npm run test:e2e:ci
- npm run test:e2e:open
- POST /api/login
- GET /api/drugs
- POST /api/dispense-logs
- POST /api/counselling/recommendations
- GET /api/manager/stats
- GET /api/manager/dispensing-logs
- GET /api/manager/pharmacist-performance
- GET /api/manager/analytics-summary
- GET /api/manager/ai-brief
- GET /api/health
- Backend exits immediately:
- Verify MySQL is running
- Verify DB credentials in backend/.env
- Ensure database pharmscan exists
- AI counselling not visible:
- Check backend logs for counselling fallback reason
- Verify LLM_BASE_URL, LLM_API_KEY, and LLM_MODEL
- Hard-refresh frontend after code changes
- 401/403 API responses:
- Re-login to refresh JWT token