diff --git a/msms/.gitignore b/msms/.gitignore new file mode 100644 index 0000000..52a32ff --- /dev/null +++ b/msms/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.env +dist/ +*.log +.DS_Store +.vscode/ +coverage/ diff --git a/msms/README.md b/msms/README.md new file mode 100644 index 0000000..4b64bea --- /dev/null +++ b/msms/README.md @@ -0,0 +1,340 @@ +# πŸ₯ Medical Store Management System (MSMS) + +> **Student:** Muawiya Amir | **ID:** 2k24_BSAI_72 +> **Course:** Database Lab β€” 4th Semester BS Artificial Intelligence +> **Instructor:** Sir Ahsan Ahmed + +--- + +## 🚦 How to Run (Quick Start) + +1. **Install dependencies** + Open two terminals and run: + - In `backend` folder: + `npm install` + - In `frontend` folder: + `npm install` + +2. **Set up environment** + - Copy `backend/.env.example` to `backend/.env` and fill in your MySQL info. + +3. **Set up the database** + - Import all SQL files in `database/` into your MySQL server (see detailed steps below). + +4. **Start the servers** + - In `backend`: + `npm start` + - In `frontend`: + `npm run dev` + +5. **Open the app** + - Frontend: [http://localhost:5173](http://localhost:5173) + - Backend API: [http://localhost:5000](http://localhost:5000) + +--- + +## ✨ Features + +### Core Modules + +- **Inventory Management** β€” Add, edit, search medicines with dosage form, strength, price, and prescription flag +- **Point of Sale (POS)** β€” Real-time cart, customer lookup, prescription validation, live total calculation +- **Prescription Tracking** β€” Link doctor prescriptions to customers; enforce Rx-only sales +- **Supplier Management** β€” Track suppliers with ratings and contact details +- **Customer Management** β€” Profiles with purchase history and total spending +- **Employee Management** β€” Role-based accounts (Admin / Manager / Pharmacist / Cashier) +- **Payment Processing** β€” Cash, Card, Online, Insurance; full transaction log +- **Reports & Analytics** β€” Low stock, expiry alerts, sales summary with charts, top-selling medicines + +### Database Features + +- 12 normalized tables (3NF) +- 4 Triggers (stock deduction, low-stock alert, order total recalc, expiry check) +- 4 Stored Procedures (GenerateBill, RestockMedicine, PlaceOrder, CompletePayment) +- 7 Views (LowStockAlert, ExpiringSoon, SalesSummary, TopSellingMedicines, EmployeePerformance, SupplierRatings, FullInventory) +- Indexes on high-frequency query columns + +### UI/UX Extras + +- πŸŒ™ Dark mode (persisted in localStorage) +- πŸ–¨οΈ Printable invoices (`window.print()` hides sidebar/navbar) +- πŸ“₯ PDF bill download (A5 size via jsPDF + html2canvas) +- πŸ“Š Sales bar chart (Recharts) +- πŸ“€ Export to CSV on every report tab +- πŸ”” Toast notifications for all actions +- πŸ”΄ Red dot badge on sidebar when medicines expire within 7 days +- ⚠️ Low-stock banner on dashboard +- JWT authentication with role-based route protection + +--- + +## πŸ›  Tech Stack + +| Layer | Technology | +| -------- | ------------------------------------- | +| Frontend | React 18, Vite, TailwindCSS, Recharts | +| Backend | Node.js, Express 4, mysql2 | +| Database | MySQL 8.0 | +| Auth | JWT (jsonwebtoken) + bcryptjs | +| Forms | react-hook-form + Zod validation | +| PDF | jsPDF + html2canvas | + +--- + +## πŸ“‹ Prerequisites + +- **Node.js** v18 or higher +- **npm** v9 or higher +- **MySQL 8.0** running locally + +--- + +## πŸš€ Installation & Setup + +### 1. Clone the repository + +```bash +git clone "https://github.com/Coding-Moves/BSAI-Projects.git" +cd msms +``` + +### 2. Install all dependencies + +```bash +npm run install:all +``` + +This installs root, backend, and frontend packages in one command. + +### 3. Configure backend environment + +```bash +cd backend +cp .env.example .env +``` + +Open `.env` and fill in your MySQL credentials: + +``` +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_mysql_password +DB_NAME=msms_db +JWT_SECRET=change_this_to_a_random_secret +PORT=5000 +``` + +### 4. Set up the MySQL database + +Open **MySQL Workbench** (or any MySQL client) and run the SQL files **in this exact order**: + +```sql +-- Step 1: Create tables +SOURCE /path/to/msms/database/schema.sql; + +-- Step 2: Insert sample data +SOURCE /path/to/msms/database/sample_data.sql; + +-- Step 3: Create triggers +SOURCE /path/to/msms/database/triggers.sql; + +-- Step 4: Create stored procedures +SOURCE /path/to/msms/database/stored_procedures.sql; + +-- Step 5: Create views +SOURCE /path/to/msms/database/views.sql; + +-- Step 6: Create indexes +SOURCE /path/to/msms/database/indexes.sql; +``` + +Or run them from the command line: + +```bash +mysql -u root -p < database/schema.sql +mysql -u root -p msms_db < database/sample_data.sql +mysql -u root -p msms_db < database/triggers.sql +mysql -u root -p msms_db < database/stored_procedures.sql +mysql -u root -p msms_db < database/views.sql +mysql -u root -p msms_db < database/indexes.sql +``` + +### 5. Start the application + +```bash +cd .. +npm run dev +``` + +This runs both frontend and backend concurrently: + +- **Backend API:** http://localhost:5000 +- **Frontend:** http://localhost:5173 + +--- + +## πŸ” Default Login + +| Username | Password | Role | +| -------- | ---------- | ----- | +| `admin` | `admin123` | Admin | + +> ⚠️ Change the default password immediately after first login in production. + +--- + +## πŸ—‚ Project Structure + +``` +msms/ +β”œβ”€β”€ README.md +β”œβ”€β”€ package.json ← root (concurrently) +β”œβ”€β”€ .gitignore +β”‚ +β”œβ”€β”€ docs/ +| β”œβ”€β”€ report.pdf +| β”œβ”€β”€ proposal.pdf +| β”œβ”€β”€ manual.pdf +| β”œβ”€β”€ ER_diagram.pdf +| +β”œβ”€β”€ backend/ +β”‚ β”œβ”€β”€ server.js ← Express entry point +β”‚ β”œβ”€β”€ db.js ← MySQL connection pool +β”‚ β”œβ”€β”€ .env.example +β”‚ β”œβ”€β”€ routes/ +β”‚ β”‚ β”œβ”€β”€ auth.js ← POST /auth/login, GET /auth/me +β”‚ β”‚ β”œβ”€β”€ medicines.js +β”‚ β”‚ β”œβ”€β”€ categories.js +β”‚ β”‚ β”œβ”€β”€ manufacturers.js +β”‚ β”‚ β”œβ”€β”€ suppliers.js +β”‚ β”‚ β”œβ”€β”€ stock.js ← includes POST /restock +β”‚ β”‚ β”œβ”€β”€ customers.js +β”‚ β”‚ β”œβ”€β”€ doctors.js +β”‚ β”‚ β”œβ”€β”€ employees.js +β”‚ β”‚ β”œβ”€β”€ prescriptions.js +β”‚ β”‚ β”œβ”€β”€ orders.js ← includes bill + complete endpoints +β”‚ β”‚ β”œβ”€β”€ payments.js +β”‚ β”‚ └── reports.js ← calls all 4 views + dashboard stats +β”‚ └── middleware/ +β”‚ β”œβ”€β”€ auth.js ← JWT verify + role authorize +β”‚ └── validate.js ← express-validator error handler +β”‚ +β”œβ”€β”€ frontend/ +β”‚ └── src/ +β”‚ β”œβ”€β”€ App.jsx ← all routes defined here +β”‚ β”œβ”€β”€ api/index.js ← axios instance + all API helpers +β”‚ β”œβ”€β”€ context/ +β”‚ β”‚ β”œβ”€β”€ AuthContext.jsx +β”‚ β”‚ └── CartContext.jsx +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ Layout/ ← Sidebar, Navbar, ProtectedRoute +β”‚ β”‚ β”œβ”€β”€ UI/ ← Modal, Badge, StatCard, SearchBar, ConfirmDialog, Pagination, CrudPage +β”‚ β”‚ └── Bill/ ← BillPreview, BillActions +β”‚ └── pages/ +β”‚ β”œβ”€β”€ Login.jsx +β”‚ β”œβ”€β”€ Dashboard.jsx +β”‚ β”œβ”€β”€ Inventory/ ← MedicineList, StockList +β”‚ β”œβ”€β”€ Sales/ ← NewOrder (POS), OrderList, OrderDetail +β”‚ β”œβ”€β”€ Customers/ +β”‚ β”œβ”€β”€ Suppliers/ +β”‚ β”œβ”€β”€ Doctors/ +β”‚ β”œβ”€β”€ Employees/ +β”‚ β”œβ”€β”€ Prescriptions/ +β”‚ β”œβ”€β”€ Payments/ +β”‚ └── Reports/ ← 4-tab report page with chart + CSV export +β”‚ +└── database/ + β”œβ”€β”€ schema.sql ← 12 tables + 2 auxiliary tables + β”œβ”€β”€ sample_data.sql ← Pakistani context sample data + β”œβ”€β”€ triggers.sql ← 4 triggers + β”œβ”€β”€ stored_procedures.sql ← 4 procedures + β”œβ”€β”€ views.sql ← 7 views + └── indexes.sql ← performance indexes +``` + +--- + +## 🌐 API Endpoints + +### Auth + +| Method | Endpoint | Description | +| ------ | -------------------- | -------------- | +| POST | `/api/v1/auth/login` | Login, get JWT | +| GET | `/api/v1/auth/me` | Current user | + +### Resources (all support GET / GET /:id / POST / PUT /:id / DELETE /:id) + +`/api/v1/categories`, `/api/v1/manufacturers`, `/api/v1/suppliers`, +`/api/v1/medicines`, `/api/v1/doctors`, `/api/v1/customers`, +`/api/v1/employees`, `/api/v1/prescriptions`, `/api/v1/payments` + +### Stock + +| Method | Endpoint | Description | +| ------ | ----------------------- | ------------------ | +| GET | `/api/v1/stock` | All stock records | +| POST | `/api/v1/stock/restock` | Restock a medicine | + +### Orders + +| Method | Endpoint | Description | +| ------ | ----------------------------- | --------------------------------- | +| GET | `/api/v1/orders` | List orders (paginated) | +| POST | `/api/v1/orders` | Place order + optional payment | +| GET | `/api/v1/orders/:id` | Order detail with items | +| GET | `/api/v1/orders/:id/bill` | Full bill (calls GenerateBill SP) | +| POST | `/api/v1/orders/:id/complete` | Complete payment | +| PUT | `/api/v1/orders/:id/cancel` | Cancel pending order | + +### Reports + +| Method | Endpoint | Description | +| ------ | -------------------------------------- | ---------------------- | +| GET | `/api/v1/reports/dashboard-stats` | 6 dashboard KPIs | +| GET | `/api/v1/reports/low-stock` | vw_LowStockAlert | +| GET | `/api/v1/reports/expiring-soon` | vw_ExpiringSoon | +| GET | `/api/v1/reports/sales-summary` | vw_SalesSummary | +| GET | `/api/v1/reports/top-medicines` | vw_TopSellingMedicines | +| GET | `/api/v1/reports/employee-performance` | vw_EmployeePerformance | + +--- + +## πŸ—„ Database Schema (12 Tables) + +| Table | Primary Key | Purpose | +| --------------- | ----------------- | ----------------------------------------- | +| `categories` | `category_id` | Medicine classifications | +| `manufacturers` | `manufacturer_id` | Pharma companies | +| `suppliers` | `supplier_id` | Stock suppliers with ratings | +| `medicines` | `medicine_id` | Core medicine catalog | +| `stock` | `stock_id` | Quantity, batch, expiry per medicine | +| `doctors` | `doctor_id` | Licensed doctors for prescriptions | +| `customers` | `customer_id` | Customer profiles | +| `employees` | `employee_id` | Staff with roles and credentials | +| `prescriptions` | `prescription_id` | Doctor β†’ Customer prescriptions | +| `orders` | `order_id` | Master order with discount/tax | +| `order_items` | `item_id` | Line items (subtotal is GENERATED column) | +| `payments` | `payment_id` | Payment method and status per order | + +--- + +## πŸ“Έ Screenshots + +> _(Add screenshots here after first run)_ + +- `screenshots/login.png` +- `screenshots/dashboard.png` +- `screenshots/pos.png` +- `screenshots/bill-preview.png` +- `screenshots/reports.png` + +--- + +## πŸ‘€ Author + +**Moavia Amir** +Student ID: 2k24_BSAI_72 +BS Artificial Intelligence β€” 4th Semester +Database Lab | Instructor: Sir Ahsan Ahmed diff --git a/msms/backend/.env.example b/msms/backend/.env.example new file mode 100644 index 0000000..d14d343 --- /dev/null +++ b/msms/backend/.env.example @@ -0,0 +1,9 @@ +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD= +DB_NAME=msms_db +JWT_SECRET=your_super_secret_jwt_key_change_in_production +JWT_EXPIRES_IN=8h +PORT=5000 +NODE_ENV=development diff --git a/msms/backend/db.js b/msms/backend/db.js new file mode 100644 index 0000000..58bd1e4 --- /dev/null +++ b/msms/backend/db.js @@ -0,0 +1,32 @@ +// db.js β€” MySQL2 connection pool +// Student: Moavia Amir | 2k24_BSAI_72 + +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '3306'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'msms_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + timezone: '+00:00', + charset: 'utf8mb4', +}); + +// Test connection on startup +(async () => { + try { + const conn = await pool.getConnection(); + console.log('βœ… MySQL connected β€” database:', process.env.DB_NAME || 'msms_db'); + conn.release(); + } catch (err) { + console.error('❌ MySQL connection failed:', err.message); + process.exit(1); + } +})(); + +module.exports = pool; diff --git a/msms/backend/middleware/auth.js b/msms/backend/middleware/auth.js new file mode 100644 index 0000000..e0a9754 --- /dev/null +++ b/msms/backend/middleware/auth.js @@ -0,0 +1,26 @@ +// middleware/auth.js β€” JWT authentication middleware +const jwt = require('jsonwebtoken'); + +const authenticate = (req, res, next) => { + const header = req.headers.authorization; + if (!header || !header.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided. Please log in.' }); + } + const token = header.split(' ')[1]; + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch { + return res.status(401).json({ error: 'Invalid or expired token.' }); + } +}; + +const authorize = (...roles) => (req, res, next) => { + if (!roles.includes(req.user?.role)) { + return res.status(403).json({ error: 'Access denied: insufficient permissions.' }); + } + next(); +}; + +module.exports = { authenticate, authorize }; diff --git a/msms/backend/middleware/validate.js b/msms/backend/middleware/validate.js new file mode 100644 index 0000000..c0c86fd --- /dev/null +++ b/msms/backend/middleware/validate.js @@ -0,0 +1,12 @@ +// middleware/validate.js β€” express-validator error handler +const { validationResult } = require('express-validator'); + +const validate = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); +}; + +module.exports = validate; diff --git a/msms/backend/package-lock.json b/msms/backend/package-lock.json new file mode 100644 index 0000000..660dd19 --- /dev/null +++ b/msms/backend/package-lock.json @@ -0,0 +1,1607 @@ +{ + "name": "msms-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "msms-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^3.0.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "mysql2": "^3.6.5" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.3.tgz", + "integrity": "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", + "peer": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/msms/backend/package.json b/msms/backend/package.json new file mode 100644 index 0000000..39f3084 --- /dev/null +++ b/msms/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "msms-backend", + "version": "1.0.0", + "description": "MSMS Backend β€” Node.js + Express + MySQL", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "bcryptjs": "^3.0.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "mysql2": "^3.6.5" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} diff --git a/msms/backend/routes/auth.js b/msms/backend/routes/auth.js new file mode 100644 index 0000000..68ab07a --- /dev/null +++ b/msms/backend/routes/auth.js @@ -0,0 +1,80 @@ +// routes/auth.js +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const { body } = require("express-validator"); +const pool = require("../db"); +const validate = require("../middleware/validate"); + +const router = express.Router(); + +// POST /api/v1/auth/login +router.post( + "/login", + [ + body("username").trim().notEmpty().withMessage("Username is required"), + body("password").notEmpty().withMessage("Password is required"), + ], + validate, + async (req, res) => { + try { + const { username, password } = req.body; + const [rows] = await pool.query( + "SELECT * FROM employees WHERE username = ? AND is_active = 1", + [username], + ); + if (!rows.length) { + return res.status(401).json({ error: "Invalid credentials." }); + } + const employee = rows[0]; + // Debug log for diagnosis + console.log("DEBUG LOGIN:", { + username, + password, + hash: employee.password_hash, + }); + const match = await bcrypt.compare(password, employee.password_hash); + console.log("DEBUG BCRYPT RESULT:", match); + if (!match) { + return res.status(401).json({ error: "Invalid credentials." }); + } + const token = jwt.sign( + { + id: employee.employee_id, + username: employee.username, + role: employee.role, + name: employee.name, + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || "8h" }, + ); + const { password_hash, ...safe } = employee; + res.json({ token, user: safe }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } + }, +); + +// GET /api/v1/auth/me +router.get( + "/me", + require("../middleware/auth").authenticate, + async (req, res) => { + try { + const [rows] = await pool.query( + "SELECT employee_id,name,role,phone,email,username,hire_date FROM employees WHERE employee_id = ?", + [req.user.id], + ); + if (!rows.length) + return res.status(404).json({ error: "User not found" }); + res.json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } + }, +); + +module.exports = router; diff --git a/msms/backend/routes/categories.js b/msms/backend/routes/categories.js new file mode 100644 index 0000000..3772bc4 --- /dev/null +++ b/msms/backend/routes/categories.js @@ -0,0 +1,91 @@ +// routes/categories.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +// GET /api/v1/categories +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 100 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT c.*, COUNT(m.medicine_id) AS medicine_count + FROM categories c + LEFT JOIN medicines m ON m.category_id = c.category_id + WHERE c.category_name LIKE ? + GROUP BY c.category_id + ORDER BY c.category_name + LIMIT ? OFFSET ?`, + [like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM categories WHERE category_name LIKE ?', [like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/categories/:id +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM categories WHERE category_id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Category not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// POST /api/v1/categories +router.post('/', + [body('category_name').trim().notEmpty().withMessage('Category name is required').isLength({ max: 100 })], + validate, + async (req, res) => { + try { + const { category_name, description } = req.body; + const [result] = await pool.query( + 'INSERT INTO categories (category_name, description) VALUES (?, ?)', + [category_name, description || null] + ); + res.status(201).json({ message: 'Category created', category_id: result.insertId }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Category name already exists' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +// PUT /api/v1/categories/:id +router.put('/:id', + [body('category_name').trim().notEmpty().withMessage('Category name is required')], + validate, + async (req, res) => { + try { + const { category_name, description } = req.body; + const [result] = await pool.query( + 'UPDATE categories SET category_name = ?, description = ? WHERE category_id = ?', + [category_name, description || null, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Category not found' }); + res.json({ message: 'Category updated' }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Category name already exists' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +// DELETE /api/v1/categories/:id +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM categories WHERE category_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Category not found' }); + res.json({ message: 'Category deleted' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/customers.js b/msms/backend/routes/customers.js new file mode 100644 index 0000000..86a62e4 --- /dev/null +++ b/msms/backend/routes/customers.js @@ -0,0 +1,103 @@ +// routes/customers.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT c.*, COUNT(o.order_id) AS total_orders, + COALESCE(SUM(o.total_amount),0) AS total_spent + FROM customers c + LEFT JOIN orders o ON o.customer_id = c.customer_id + WHERE c.name LIKE ? OR c.phone LIKE ? OR c.email LIKE ? + GROUP BY c.customer_id + ORDER BY c.name LIMIT ? OFFSET ?`, + [like, like, like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM customers WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?', + [like, like, like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/search/phone', async (req, res) => { + try { + const { phone } = req.query; + const [rows] = await pool.query( + 'SELECT * FROM customers WHERE phone LIKE ? LIMIT 10', [`%${phone}%`] + ); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM customers WHERE customer_id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Customer not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + [ + body('name').trim().notEmpty().withMessage('Name is required'), + body('phone').trim().notEmpty().withMessage('Phone is required'), + body('email').optional({ checkFalsy: true }).isEmail().withMessage('Invalid email'), + body('gender').optional().isIn(['Male','Female','Other']), + ], + validate, + async (req, res) => { + try { + const { name, phone, email, date_of_birth, gender, address } = req.body; + const [result] = await pool.query( + 'INSERT INTO customers (name, phone, email, date_of_birth, gender, address) VALUES (?,?,?,?,?,?)', + [name, phone, email||null, date_of_birth||null, gender||null, address||null] + ); + res.status(201).json({ message: 'Customer created', customer_id: result.insertId }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Phone number already exists' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put('/:id', + [ + body('name').trim().notEmpty(), + body('phone').trim().notEmpty(), + body('email').optional({ checkFalsy: true }).isEmail(), + ], + validate, + async (req, res) => { + try { + const { name, phone, email, date_of_birth, gender, address } = req.body; + const [result] = await pool.query( + 'UPDATE customers SET name=?,phone=?,email=?,date_of_birth=?,gender=?,address=? WHERE customer_id=?', + [name, phone, email||null, date_of_birth||null, gender||null, address||null, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Customer not found' }); + res.json({ message: 'Customer updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM customers WHERE customer_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Customer not found' }); + res.json({ message: 'Customer deleted' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/doctors.js b/msms/backend/routes/doctors.js new file mode 100644 index 0000000..6086a53 --- /dev/null +++ b/msms/backend/routes/doctors.js @@ -0,0 +1,82 @@ +// routes/doctors.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT * FROM doctors WHERE (name LIKE ? OR specialization LIKE ? OR license_number LIKE ?) AND is_active = 1 + ORDER BY name LIMIT ? OFFSET ?`, + [like, like, like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM doctors WHERE (name LIKE ? OR specialization LIKE ?) AND is_active = 1', + [like, like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM doctors WHERE doctor_id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Doctor not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + [ + body('name').trim().notEmpty().withMessage('Name required'), + body('license_number').trim().notEmpty().withMessage('License number required'), + ], + validate, + async (req, res) => { + try { + const { name, specialization, license_number, phone, hospital } = req.body; + const [result] = await pool.query( + 'INSERT INTO doctors (name, specialization, license_number, phone, hospital) VALUES (?,?,?,?,?)', + [name, specialization||null, license_number, phone||null, hospital||null] + ); + res.status(201).json({ message: 'Doctor created', doctor_id: result.insertId }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'License number already exists' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put('/:id', + [body('name').trim().notEmpty(), body('license_number').trim().notEmpty()], + validate, + async (req, res) => { + try { + const { name, specialization, license_number, phone, hospital, is_active } = req.body; + const [result] = await pool.query( + 'UPDATE doctors SET name=?,specialization=?,license_number=?,phone=?,hospital=?,is_active=? WHERE doctor_id=?', + [name, specialization||null, license_number, phone||null, hospital||null, is_active !== undefined ? (is_active?1:0) : 1, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Doctor not found' }); + res.json({ message: 'Doctor updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('UPDATE doctors SET is_active = 0 WHERE doctor_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Doctor not found' }); + res.json({ message: 'Doctor deactivated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/employees.js b/msms/backend/routes/employees.js new file mode 100644 index 0000000..e3161eb --- /dev/null +++ b/msms/backend/routes/employees.js @@ -0,0 +1,109 @@ +// routes/employees.js +const express = require('express'); +const bcrypt = require('bcryptjs'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate, authorize } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +const ROLES = ['Pharmacist','Cashier','Manager','Admin']; + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT employee_id, name, role, phone, email, salary, hire_date, is_active, username + FROM employees + WHERE (name LIKE ? OR role LIKE ? OR username LIKE ?) AND is_active = 1 + ORDER BY name LIMIT ? OFFSET ?`, + [like, like, like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM employees WHERE (name LIKE ? OR role LIKE ?) AND is_active = 1', [like, like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query( + 'SELECT employee_id, name, role, phone, email, salary, hire_date, is_active, username FROM employees WHERE employee_id = ?', + [req.params.id] + ); + if (!rows.length) return res.status(404).json({ error: 'Employee not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + authorize('Admin', 'Manager'), + [ + body('name').trim().notEmpty().withMessage('Name required'), + body('role').isIn(ROLES).withMessage('Invalid role'), + body('phone').trim().notEmpty().withMessage('Phone required'), + body('username').trim().notEmpty().isLength({ min: 3 }).withMessage('Username min 3 chars'), + body('password').isLength({ min: 6 }).withMessage('Password min 6 chars'), + body('salary').optional().isFloat({ gt: 0 }), + body('email').optional({ checkFalsy: true }).isEmail(), + body('hire_date').isDate().withMessage('Valid hire date required'), + ], + validate, + async (req, res) => { + try { + const { name, role, phone, email, salary, hire_date, username, password } = req.body; + const hash = await bcrypt.hash(password, 12); + const [result] = await pool.query( + 'INSERT INTO employees (name,role,phone,email,salary,hire_date,username,password_hash) VALUES (?,?,?,?,?,?,?,?)', + [name, role, phone, email||null, salary||null, hire_date, username, hash] + ); + res.status(201).json({ message: 'Employee created', employee_id: result.insertId }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Phone, email, or username already exists' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put('/:id', + authorize('Admin', 'Manager'), + [ + body('name').trim().notEmpty(), + body('role').isIn(ROLES), + body('phone').trim().notEmpty(), + ], + validate, + async (req, res) => { + try { + const { name, role, phone, email, salary, hire_date, is_active, password } = req.body; + if (password) { + const hash = await bcrypt.hash(password, 12); + await pool.query( + 'UPDATE employees SET name=?,role=?,phone=?,email=?,salary=?,hire_date=?,is_active=?,password_hash=? WHERE employee_id=?', + [name, role, phone, email||null, salary||null, hire_date, is_active!==undefined?(is_active?1:0):1, hash, req.params.id] + ); + } else { + await pool.query( + 'UPDATE employees SET name=?,role=?,phone=?,email=?,salary=?,hire_date=?,is_active=? WHERE employee_id=?', + [name, role, phone, email||null, salary||null, hire_date, is_active!==undefined?(is_active?1:0):1, req.params.id] + ); + } + res.json({ message: 'Employee updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', authorize('Admin'), async (req, res) => { + try { + const [result] = await pool.query('UPDATE employees SET is_active = 0 WHERE employee_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Employee not found' }); + res.json({ message: 'Employee deactivated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/manufacturers.js b/msms/backend/routes/manufacturers.js new file mode 100644 index 0000000..7e2f3fb --- /dev/null +++ b/msms/backend/routes/manufacturers.js @@ -0,0 +1,80 @@ +// routes/manufacturers.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT * FROM manufacturers WHERE name LIKE ? OR country LIKE ? ORDER BY name LIMIT ? OFFSET ?`, + [like, like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM manufacturers WHERE name LIKE ? OR country LIKE ?', [like, like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM manufacturers WHERE manufacturer_id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Manufacturer not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + [ + body('name').trim().notEmpty().withMessage('Name is required'), + body('contact_email').optional().isEmail().withMessage('Invalid email'), + ], + validate, + async (req, res) => { + try { + const { name, country, contact_email, phone, address } = req.body; + const [result] = await pool.query( + 'INSERT INTO manufacturers (name, country, contact_email, phone, address) VALUES (?,?,?,?,?)', + [name, country||null, contact_email||null, phone||null, address||null] + ); + res.status(201).json({ message: 'Manufacturer created', manufacturer_id: result.insertId }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Duplicate entry' }); + console.error(err); res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put('/:id', + [body('name').trim().notEmpty()], + validate, + async (req, res) => { + try { + const { name, country, contact_email, phone, address } = req.body; + const [result] = await pool.query( + 'UPDATE manufacturers SET name=?,country=?,contact_email=?,phone=?,address=? WHERE manufacturer_id=?', + [name, country||null, contact_email||null, phone||null, address||null, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Manufacturer not found' }); + res.json({ message: 'Manufacturer updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM manufacturers WHERE manufacturer_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Manufacturer not found' }); + res.json({ message: 'Manufacturer deleted' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/medicines.js b/msms/backend/routes/medicines.js new file mode 100644 index 0000000..89899e9 --- /dev/null +++ b/msms/backend/routes/medicines.js @@ -0,0 +1,134 @@ +// routes/medicines.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +const DOSAGE_FORMS = ['Tablet','Capsule','Syrup','Injection','Cream','Drop','Inhaler','Powder','Patch']; + +// GET /api/v1/medicines +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10, category_id, active } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + + let where = 'WHERE (m.name LIKE ? OR m.generic_name LIKE ?)'; + const params = [like, like]; + + if (category_id) { where += ' AND m.category_id = ?'; params.push(category_id); } + if (active !== undefined) { where += ' AND m.is_active = ?'; params.push(active === 'true' ? 1 : 0); } + + const [rows] = await pool.query( + `SELECT m.*, c.category_name, mfr.name AS manufacturer_name, + s.quantity AS stock_qty, s.expiry_date, s.reorder_level, + CASE WHEN s.quantity IS NULL OR s.quantity = 0 THEN 'Out of Stock' + WHEN s.quantity < s.reorder_level THEN 'Low Stock' + ELSE 'In Stock' END AS stock_status + FROM medicines m + LEFT JOIN categories c ON c.category_id = m.category_id + LEFT JOIN manufacturers mfr ON mfr.manufacturer_id = m.manufacturer_id + LEFT JOIN stock s ON s.medicine_id = m.medicine_id + ${where} + ORDER BY m.name + LIMIT ? OFFSET ?`, + [...params, parseInt(limit), offset] + ); + + const [[{ total }]] = await pool.query( + `SELECT COUNT(*) AS total FROM medicines m ${where}`, params + ); + + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/medicines/:id +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query( + `SELECT m.*, c.category_name, mfr.name AS manufacturer_name, + s.quantity AS stock_qty, s.expiry_date, s.reorder_level, s.batch_number, + s.manufacturing_date, s.last_restocked, sup.name AS supplier_name + FROM medicines m + LEFT JOIN categories c ON c.category_id = m.category_id + LEFT JOIN manufacturers mfr ON mfr.manufacturer_id = m.manufacturer_id + LEFT JOIN stock s ON s.medicine_id = m.medicine_id + LEFT JOIN suppliers sup ON sup.supplier_id = s.supplier_id + WHERE m.medicine_id = ?`, + [req.params.id] + ); + if (!rows.length) return res.status(404).json({ error: 'Medicine not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// POST /api/v1/medicines +router.post('/', + [ + body('name').trim().notEmpty().isLength({ min: 2 }).withMessage('Name min 2 chars'), + body('dosage_form').isIn(DOSAGE_FORMS).withMessage('Invalid dosage form'), + body('unit_price').isFloat({ gt: 0 }).withMessage('Price must be > 0'), + body('category_id').optional().isInt({ gt: 0 }), + body('manufacturer_id').optional().isInt({ gt: 0 }), + body('requires_prescription').optional().isBoolean(), + ], + validate, + async (req, res) => { + try { + const { name, generic_name, category_id, manufacturer_id, dosage_form, + strength, unit_price, requires_prescription, description } = req.body; + const [result] = await pool.query( + `INSERT INTO medicines + (name, generic_name, category_id, manufacturer_id, dosage_form, strength, unit_price, requires_prescription, description) + VALUES (?,?,?,?,?,?,?,?,?)`, + [name, generic_name||null, category_id||null, manufacturer_id||null, + dosage_form, strength||null, unit_price, requires_prescription ? 1 : 0, description||null] + ); + res.status(201).json({ message: 'Medicine created', medicine_id: result.insertId }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +// PUT /api/v1/medicines/:id +router.put('/:id', + [ + body('name').trim().notEmpty().isLength({ min: 2 }), + body('dosage_form').isIn(DOSAGE_FORMS), + body('unit_price').isFloat({ gt: 0 }), + ], + validate, + async (req, res) => { + try { + const { name, generic_name, category_id, manufacturer_id, dosage_form, + strength, unit_price, requires_prescription, description, is_active } = req.body; + const [result] = await pool.query( + `UPDATE medicines SET name=?, generic_name=?, category_id=?, manufacturer_id=?, + dosage_form=?, strength=?, unit_price=?, requires_prescription=?, description=?, is_active=? + WHERE medicine_id=?`, + [name, generic_name||null, category_id||null, manufacturer_id||null, + dosage_form, strength||null, unit_price, requires_prescription ? 1 : 0, + description||null, is_active !== undefined ? (is_active ? 1 : 0) : 1, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Medicine not found' }); + res.json({ message: 'Medicine updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +// DELETE /api/v1/medicines/:id (soft delete) +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query( + 'UPDATE medicines SET is_active = 0 WHERE medicine_id = ?', [req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Medicine not found' }); + res.json({ message: 'Medicine deactivated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/orders.js b/msms/backend/routes/orders.js new file mode 100644 index 0000000..9a1649a --- /dev/null +++ b/msms/backend/routes/orders.js @@ -0,0 +1,195 @@ +// routes/orders.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +// GET /api/v1/orders +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10, status } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + + let where = 'WHERE (COALESCE(c.name,"Walk-in") LIKE ? OR CONCAT("ORD-",LPAD(o.order_id,5,"0")) LIKE ?)'; + const params = [like, like]; + if (status) { where += ' AND o.status = ?'; params.push(status); } + + const [rows] = await pool.query( + `SELECT o.*, COALESCE(c.name,'Walk-in Customer') AS customer_name, + c.phone AS customer_phone, e.name AS employee_name, + COUNT(oi.item_id) AS item_count, + CONCAT('ORD-',LPAD(o.order_id,5,'0')) AS invoice_number + FROM orders o + LEFT JOIN customers c ON c.customer_id = o.customer_id + JOIN employees e ON e.employee_id = o.employee_id + LEFT JOIN order_items oi ON oi.order_id = o.order_id + ${where} + GROUP BY o.order_id + ORDER BY o.order_date DESC LIMIT ? OFFSET ?`, + [...params, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + `SELECT COUNT(DISTINCT o.order_id) AS total + FROM orders o + LEFT JOIN customers c ON c.customer_id = o.customer_id + ${where}`, params + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/orders/:id +router.get('/:id', async (req, res) => { + try { + const [[order]] = await pool.query( + `SELECT o.*, COALESCE(c.name,'Walk-in Customer') AS customer_name, c.phone AS customer_phone, + e.name AS employee_name, e.role AS employee_role, + CONCAT('ORD-',LPAD(o.order_id,5,'0')) AS invoice_number, + p.prescription_date, d.name AS doctor_name + FROM orders o + LEFT JOIN customers c ON c.customer_id = o.customer_id + JOIN employees e ON e.employee_id = o.employee_id + LEFT JOIN prescriptions p ON p.prescription_id = o.prescription_id + LEFT JOIN doctors d ON d.doctor_id = p.doctor_id + WHERE o.order_id = ?`, [req.params.id] + ); + if (!order) return res.status(404).json({ error: 'Order not found' }); + + const [items] = await pool.query( + `SELECT oi.*, m.name AS medicine_name, m.dosage_form, m.strength + FROM order_items oi + JOIN medicines m ON m.medicine_id = oi.medicine_id + WHERE oi.order_id = ?`, [req.params.id] + ); + + const [[payment]] = await pool.query( + 'SELECT * FROM payments WHERE order_id = ?', [req.params.id] + ); + + res.json({ ...order, items, payment: payment || null }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/orders/:id/bill β€” calls GenerateBill stored procedure +router.get('/:id/bill', async (req, res) => { + try { + const [results] = await pool.query('CALL GenerateBill(?)', [req.params.id]); + if (!results[0]?.length) return res.status(404).json({ error: 'Order not found' }); + res.json({ header: results[0][0], items: results[1] }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// POST /api/v1/orders β€” place a new order with items +router.post('/', + [ + body('employee_id').isInt({ gt: 0 }).withMessage('Employee ID required'), + body('items').isArray({ min: 1 }).withMessage('At least one item required'), + body('items.*.medicine_id').isInt({ gt: 0 }), + body('items.*.quantity').isInt({ gt: 0 }), + body('items.*.unit_price').isFloat({ gt: 0 }), + body('discount_percent').optional().isFloat({ min: 0, max: 100 }), + body('tax_percent').optional().isFloat({ min: 0 }), + ], + validate, + async (req, res) => { + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + const { customer_id, employee_id, prescription_id, discount_percent = 0, + tax_percent = 0, notes, items, payment_method, amount_paid, transaction_ref } = req.body; + + // Insert order + const [orderResult] = await conn.query( + `INSERT INTO orders (customer_id, employee_id, prescription_id, status, discount_percent, tax_percent, notes) + VALUES (?,?,?,?,?,?,?)`, + [customer_id||null, employee_id, prescription_id||null, 'Pending', discount_percent, tax_percent, notes||null] + ); + const orderId = orderResult.insertId; + + // Insert items (triggers handle stock deduction + total recalc) + for (const item of items) { + await conn.query( + 'INSERT INTO order_items (order_id, medicine_id, quantity, unit_price) VALUES (?,?,?,?)', + [orderId, item.medicine_id, item.quantity, item.unit_price] + ); + } + + // Process payment if provided + if (payment_method && amount_paid !== undefined) { + // Get updated total + const [[{ total_amount }]] = await conn.query( + 'SELECT total_amount FROM orders WHERE order_id = ?', [orderId] + ); + + if (parseFloat(amount_paid) < parseFloat(total_amount)) { + await conn.rollback(); + conn.release(); + return res.status(400).json({ error: `Amount paid (${amount_paid}) is less than total (${total_amount})` }); + } + + await conn.query( + `INSERT INTO payments (order_id, amount_paid, payment_method, payment_status, transaction_ref, paid_at) + VALUES (?,?,?,'Paid',?,NOW())`, + [orderId, amount_paid, payment_method, transaction_ref||null] + ); + + await conn.query("UPDATE orders SET status='Completed' WHERE order_id=?", [orderId]); + + if (prescription_id) { + await conn.query('UPDATE prescriptions SET is_used=1 WHERE prescription_id=?', [prescription_id]); + } + } + + await conn.commit(); + res.status(201).json({ message: 'Order placed successfully', order_id: orderId }); + } catch (err) { + await conn.rollback(); + console.error(err); + const msg = err.sqlMessage || err.message || 'Internal server error'; + res.status(err.sqlMessage ? 400 : 500).json({ error: msg }); + } finally { + conn.release(); + } + } +); + +// POST /api/v1/orders/:id/complete β€” calls CompletePayment procedure +router.post('/:id/complete', + [ + body('payment_method').isIn(['Cash','Card','Online','Insurance']).withMessage('Invalid payment method'), + body('amount_paid').isFloat({ gt: 0 }).withMessage('Amount must be > 0'), + ], + validate, + async (req, res) => { + try { + const { payment_method, amount_paid, transaction_ref } = req.body; + await pool.query('CALL CompletePayment(?,?,?,?)', + [req.params.id, payment_method, amount_paid, transaction_ref||null] + ); + res.json({ message: 'Payment completed successfully' }); + } catch (err) { + console.error(err); + const msg = err.sqlMessage || err.message || 'Internal server error'; + res.status(400).json({ error: msg }); + } + } +); + +// PUT /api/v1/orders/:id/cancel +router.put('/:id/cancel', async (req, res) => { + try { + const [result] = await pool.query( + "UPDATE orders SET status='Cancelled' WHERE order_id=? AND status='Pending'", [req.params.id] + ); + if (!result.affectedRows) return res.status(400).json({ error: 'Order cannot be cancelled (not found or already processed)' }); + res.json({ message: 'Order cancelled' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/payments.js b/msms/backend/routes/payments.js new file mode 100644 index 0000000..df87d8b --- /dev/null +++ b/msms/backend/routes/payments.js @@ -0,0 +1,58 @@ +// routes/payments.js +const express = require('express'); +const pool = require('../db'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10, method, status } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + + let where = 'WHERE (CONCAT("ORD-",LPAD(o.order_id,5,"0")) LIKE ? OR COALESCE(c.name,"Walk-in") LIKE ?)'; + const params = [like, like]; + if (method) { where += ' AND py.payment_method = ?'; params.push(method); } + if (status) { where += ' AND py.payment_status = ?'; params.push(status); } + + const [rows] = await pool.query( + `SELECT py.*, o.total_amount, o.order_date, + CONCAT('ORD-',LPAD(o.order_id,5,'0')) AS invoice_number, + COALESCE(c.name,'Walk-in Customer') AS customer_name, + e.name AS cashier_name + FROM payments py + JOIN orders o ON o.order_id = py.order_id + LEFT JOIN customers c ON c.customer_id = o.customer_id + JOIN employees e ON e.employee_id = o.employee_id + ${where} + ORDER BY py.paid_at DESC LIMIT ? OFFSET ?`, + [...params, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + `SELECT COUNT(*) AS total FROM payments py + JOIN orders o ON o.order_id = py.order_id + LEFT JOIN customers c ON c.customer_id = o.customer_id + ${where}`, params + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query( + `SELECT py.*, CONCAT('ORD-',LPAD(o.order_id,5,'0')) AS invoice_number, + o.total_amount, COALESCE(c.name,'Walk-in') AS customer_name + FROM payments py + JOIN orders o ON o.order_id = py.order_id + LEFT JOIN customers c ON c.customer_id = o.customer_id + WHERE py.payment_id = ?`, [req.params.id] + ); + if (!rows.length) return res.status(404).json({ error: 'Payment not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/prescriptions.js b/msms/backend/routes/prescriptions.js new file mode 100644 index 0000000..95f5a58 --- /dev/null +++ b/msms/backend/routes/prescriptions.js @@ -0,0 +1,104 @@ +// routes/prescriptions.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10, customer_id } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + + let where = 'WHERE (c.name LIKE ? OR c.phone LIKE ? OR d.name LIKE ?)'; + const params = [like, like, like]; + if (customer_id) { where += ' AND p.customer_id = ?'; params.push(customer_id); } + + const [rows] = await pool.query( + `SELECT p.*, c.name AS customer_name, c.phone AS customer_phone, + d.name AS doctor_name, d.specialization + FROM prescriptions p + JOIN customers c ON c.customer_id = p.customer_id + JOIN doctors d ON d.doctor_id = p.doctor_id + ${where} + ORDER BY p.prescription_date DESC LIMIT ? OFFSET ?`, + [...params, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + `SELECT COUNT(*) AS total FROM prescriptions p + JOIN customers c ON c.customer_id = p.customer_id + JOIN doctors d ON d.doctor_id = p.doctor_id + ${where}`, params + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query( + `SELECT p.*, c.name AS customer_name, c.phone AS customer_phone, + d.name AS doctor_name, d.specialization, d.license_number, d.hospital + FROM prescriptions p + JOIN customers c ON c.customer_id = p.customer_id + JOIN doctors d ON d.doctor_id = p.doctor_id + WHERE p.prescription_id = ?`, [req.params.id] + ); + if (!rows.length) return res.status(404).json({ error: 'Prescription not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + [ + body('customer_id').isInt({ gt: 0 }).withMessage('Valid customer required'), + body('doctor_id').isInt({ gt: 0 }).withMessage('Valid doctor required'), + body('prescription_date').isDate().withMessage('Valid date required'), + body('valid_until').optional().isDate(), + ], + validate, + async (req, res) => { + try { + const { customer_id, doctor_id, prescription_date, valid_until, notes } = req.body; + const [result] = await pool.query( + 'INSERT INTO prescriptions (customer_id, doctor_id, prescription_date, valid_until, notes) VALUES (?,?,?,?,?)', + [customer_id, doctor_id, prescription_date, valid_until||null, notes||null] + ); + res.status(201).json({ message: 'Prescription created', prescription_id: result.insertId }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.put('/:id', + [ + body('customer_id').isInt({ gt: 0 }), + body('doctor_id').isInt({ gt: 0 }), + body('prescription_date').isDate(), + ], + validate, + async (req, res) => { + try { + const { customer_id, doctor_id, prescription_date, valid_until, notes, is_used } = req.body; + const [result] = await pool.query( + 'UPDATE prescriptions SET customer_id=?,doctor_id=?,prescription_date=?,valid_until=?,notes=?,is_used=? WHERE prescription_id=?', + [customer_id, doctor_id, prescription_date, valid_until||null, notes||null, is_used?1:0, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Prescription not found' }); + res.json({ message: 'Prescription updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('DELETE FROM prescriptions WHERE prescription_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Prescription not found' }); + res.json({ message: 'Prescription deleted' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/reports.js b/msms/backend/routes/reports.js new file mode 100644 index 0000000..d83bc4c --- /dev/null +++ b/msms/backend/routes/reports.js @@ -0,0 +1,78 @@ +// routes/reports.js +const express = require('express'); +const pool = require('../db'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +// GET /api/v1/reports/low-stock +router.get('/low-stock', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM vw_LowStockAlert'); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/reports/expiring-soon +router.get('/expiring-soon', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM vw_ExpiringSoon'); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/reports/sales-summary +router.get('/sales-summary', async (req, res) => { + try { + const { from, to } = req.query; + let query = 'SELECT * FROM vw_SalesSummary'; + const params = []; + if (from && to) { + query = `SELECT DATE(o.order_date) AS sale_date, COUNT(o.order_id) AS total_orders, + SUM(o.total_amount) AS total_revenue, AVG(o.total_amount) AS avg_order_value, + COUNT(DISTINCT o.customer_id) AS unique_customers + FROM orders o WHERE o.status='Completed' AND DATE(o.order_date) BETWEEN ? AND ? + GROUP BY DATE(o.order_date) ORDER BY sale_date DESC`; + params.push(from, to); + } + const [rows] = await pool.query(query, params); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/reports/top-medicines +router.get('/top-medicines', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM vw_TopSellingMedicines LIMIT 20'); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/reports/employee-performance +router.get('/employee-performance', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM vw_EmployeePerformance'); + res.json(rows); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// GET /api/v1/reports/dashboard-stats +router.get('/dashboard-stats', async (req, res) => { + try { + const [[stats]] = await pool.query(` + SELECT + (SELECT COUNT(*) FROM medicines WHERE is_active=1) AS total_medicines, + (SELECT COUNT(*) FROM vw_LowStockAlert) AS low_stock_count, + (SELECT COUNT(*) FROM vw_ExpiringSoon) AS expiring_soon_count, + (SELECT COALESCE(SUM(total_amount),0) + FROM orders WHERE status='Completed' + AND DATE(order_date)=CURDATE()) AS today_sales, + (SELECT COUNT(*) FROM customers) AS total_customers, + (SELECT COUNT(*) FROM orders WHERE status='Pending') AS pending_orders + `); + res.json(stats); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/stock.js b/msms/backend/routes/stock.js new file mode 100644 index 0000000..dbc7ee0 --- /dev/null +++ b/msms/backend/routes/stock.js @@ -0,0 +1,72 @@ +// routes/stock.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT s.*, m.name AS medicine_name, m.dosage_form, m.strength, + sup.name AS supplier_name, + CASE WHEN s.quantity = 0 THEN 'Out of Stock' + WHEN s.quantity < s.reorder_level THEN 'Low Stock' + ELSE 'In Stock' END AS stock_status + FROM stock s + JOIN medicines m ON m.medicine_id = s.medicine_id + LEFT JOIN suppliers sup ON sup.supplier_id = s.supplier_id + WHERE m.name LIKE ? + ORDER BY m.name LIMIT ? OFFSET ?`, + [like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + `SELECT COUNT(*) AS total FROM stock s JOIN medicines m ON m.medicine_id = s.medicine_id WHERE m.name LIKE ?`, [like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +// POST /api/v1/stock/restock β€” calls stored procedure +router.post('/restock', + [ + body('medicine_id').isInt({ gt: 0 }).withMessage('Valid medicine_id required'), + body('quantity').isInt({ gt: 0 }).withMessage('Quantity must be > 0'), + body('expiry_date').isDate().withMessage('Valid expiry date required'), + ], + validate, + async (req, res) => { + try { + const { medicine_id, quantity, supplier_id, expiry_date, batch_number } = req.body; + await pool.query( + 'CALL RestockMedicine(?, ?, ?, ?, ?, ?)', + [medicine_id, quantity, supplier_id||null, expiry_date, batch_number||null, req.user.id] + ); + res.json({ message: `Medicine restocked by ${quantity} units.` }); + } catch (err) { + console.error(err); + const msg = err.sqlMessage || err.message || 'Internal server error'; + res.status(400).json({ error: msg }); + } + } +); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query( + `SELECT s.*, m.name AS medicine_name FROM stock s + JOIN medicines m ON m.medicine_id = s.medicine_id + WHERE s.stock_id = ?`, [req.params.id] + ); + if (!rows.length) return res.status(404).json({ error: 'Stock record not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/routes/suppliers.js b/msms/backend/routes/suppliers.js new file mode 100644 index 0000000..a2f6595 --- /dev/null +++ b/msms/backend/routes/suppliers.js @@ -0,0 +1,83 @@ +// routes/suppliers.js +const express = require('express'); +const { body } = require('express-validator'); +const pool = require('../db'); +const validate = require('../middleware/validate'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); +router.use(authenticate); + +router.get('/', async (req, res) => { + try { + const { search = '', page = 1, limit = 10 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const like = `%${search}%`; + const [rows] = await pool.query( + `SELECT * FROM suppliers WHERE (name LIKE ? OR contact_person LIKE ?) ORDER BY name LIMIT ? OFFSET ?`, + [like, like, parseInt(limit), offset] + ); + const [[{ total }]] = await pool.query( + 'SELECT COUNT(*) AS total FROM suppliers WHERE name LIKE ? OR contact_person LIKE ?', [like, like] + ); + res.json({ data: rows, total, page: parseInt(page), limit: parseInt(limit) }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.get('/:id', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM suppliers WHERE supplier_id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Supplier not found' }); + res.json(rows[0]); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +router.post('/', + [ + body('name').trim().notEmpty().withMessage('Name is required'), + body('phone').trim().notEmpty().withMessage('Phone is required'), + body('rating').optional().isFloat({ min: 1, max: 5 }).withMessage('Rating must be 1–5'), + body('email').optional().isEmail(), + ], + validate, + async (req, res) => { + try { + const { name, contact_person, phone, email, address, rating } = req.body; + const [result] = await pool.query( + 'INSERT INTO suppliers (name, contact_person, phone, email, address, rating) VALUES (?,?,?,?,?,?)', + [name, contact_person||null, phone, email||null, address||null, rating||null] + ); + res.status(201).json({ message: 'Supplier created', supplier_id: result.insertId }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.put('/:id', + [ + body('name').trim().notEmpty(), + body('phone').trim().notEmpty(), + body('rating').optional().isFloat({ min: 1, max: 5 }), + ], + validate, + async (req, res) => { + try { + const { name, contact_person, phone, email, address, rating, is_active } = req.body; + const [result] = await pool.query( + 'UPDATE suppliers SET name=?,contact_person=?,phone=?,email=?,address=?,rating=?,is_active=? WHERE supplier_id=?', + [name, contact_person||null, phone, email||null, address||null, rating||null, is_active !== undefined ? (is_active ? 1 : 0) : 1, req.params.id] + ); + if (!result.affectedRows) return res.status(404).json({ error: 'Supplier not found' }); + res.json({ message: 'Supplier updated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } + } +); + +router.delete('/:id', async (req, res) => { + try { + const [result] = await pool.query('UPDATE suppliers SET is_active = 0 WHERE supplier_id = ?', [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ error: 'Supplier not found' }); + res.json({ message: 'Supplier deactivated' }); + } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); } +}); + +module.exports = router; diff --git a/msms/backend/server.js b/msms/backend/server.js new file mode 100644 index 0000000..854ee54 --- /dev/null +++ b/msms/backend/server.js @@ -0,0 +1,51 @@ +// server.js β€” MSMS Express Application +// Student: Moavia Amir | 2k24_BSAI_72 + +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); + +const app = express(); + +// ─── Middleware ─────────────────────────────────────────── +app.use(helmet()); +app.use(cors({ origin: process.env.CLIENT_ORIGIN || 'http://localhost:5173', credentials: true })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +if (process.env.NODE_ENV !== 'test') app.use(morgan('dev')); + +// ─── Routes ────────────────────────────────────────────── +app.use('/api/v1/auth', require('./routes/auth')); +app.use('/api/v1/categories', require('./routes/categories')); +app.use('/api/v1/manufacturers', require('./routes/manufacturers')); +app.use('/api/v1/suppliers', require('./routes/suppliers')); +app.use('/api/v1/medicines', require('./routes/medicines')); +app.use('/api/v1/stock', require('./routes/stock')); +app.use('/api/v1/doctors', require('./routes/doctors')); +app.use('/api/v1/customers', require('./routes/customers')); +app.use('/api/v1/employees', require('./routes/employees')); +app.use('/api/v1/prescriptions', require('./routes/prescriptions')); +app.use('/api/v1/orders', require('./routes/orders')); +app.use('/api/v1/payments', require('./routes/payments')); +app.use('/api/v1/reports', require('./routes/reports')); + +// ─── Health check ──────────────────────────────────────── +app.get('/api/v1/health', (_req, res) => res.json({ status: 'ok', timestamp: new Date() })); + +// ─── 404 handler ───────────────────────────────────────── +app.use((_req, res) => res.status(404).json({ error: 'Route not found' })); + +// ─── Global error handler ───────────────────────────────── +app.use((err, _req, res, _next) => { + console.error('Unhandled error:', err); + const status = err.status || 500; + res.status(status).json({ + error: status === 500 ? 'Internal server error' : err.message, + }); +}); + +// ─── Start ──────────────────────────────────────────────── +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log(`πŸš€ MSMS API running on http://localhost:${PORT}`)); diff --git a/msms/database/indexes.sql b/msms/database/indexes.sql new file mode 100644 index 0000000..67947a2 --- /dev/null +++ b/msms/database/indexes.sql @@ -0,0 +1,38 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- indexes.sql β€” Performance Indexes +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- ============================================================ + +USE msms_db; + +-- Medicines +CREATE INDEX idx_medicine_name ON medicines(name); +CREATE INDEX idx_medicine_category ON medicines(category_id); +CREATE INDEX idx_medicine_active ON medicines(is_active); + +-- Stock +CREATE INDEX idx_stock_expiry ON stock(expiry_date); +CREATE INDEX idx_stock_qty ON stock(quantity); + +-- Customers +CREATE INDEX idx_customer_phone ON customers(phone); +CREATE INDEX idx_customer_name ON customers(name); + +-- Orders +CREATE INDEX idx_order_date ON orders(order_date); +CREATE INDEX idx_order_status ON orders(status); +CREATE INDEX idx_order_customer ON orders(customer_id); + +-- Order Items +CREATE INDEX idx_item_order ON order_items(order_id); +CREATE INDEX idx_item_medicine ON order_items(medicine_id); + +-- Payments +CREATE INDEX idx_payment_status ON payments(payment_status); +CREATE INDEX idx_payment_method ON payments(payment_method); + +-- Stock Transactions +CREATE INDEX idx_txn_medicine ON stock_transactions(medicine_id); +CREATE INDEX idx_txn_type ON stock_transactions(txn_type); +CREATE INDEX idx_txn_created ON stock_transactions(created_at); diff --git a/msms/database/sample_data.sql b/msms/database/sample_data.sql new file mode 100644 index 0000000..54ae449 --- /dev/null +++ b/msms/database/sample_data.sql @@ -0,0 +1,194 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- sample_data.sql β€” Realistic Pakistani Sample Data +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- ============================================================ + +USE msms_db; + +-- ───────────────────────────────────────── +-- CATEGORIES +-- ───────────────────────────────────────── +INSERT INTO categories (category_name, description) VALUES +('Analgesic', 'Pain relievers and fever reducers'), +('Antibiotic', 'Medicines that fight bacterial infections'), +('Antacid', 'Medicines for acid reflux and stomach issues'), +('Antihistamine', 'Medicines for allergies and hay fever'), +('Cardiovascular', 'Medicines for heart and blood pressure'), +('Antidiabetic', 'Medicines for diabetes management'), +('Respiratory', 'Medicines for asthma, COPD, and respiratory conditions'), +('Dermatological', 'Skin creams, ointments, and topical medicines'), +('Gastrointestinal', 'Medicines for digestive issues'), +('Vitamins', 'Vitamins, minerals, and dietary supplements'), +('Antifungal', 'Medicines for fungal infections'), +('ORS & Rehydration','Oral rehydration solutions and electrolytes'); + +-- ───────────────────────────────────────── +-- MANUFACTURERS +-- ───────────────────────────────────────── +INSERT INTO manufacturers (name, country, contact_email, phone, address) VALUES +('Getz Pharma (Pvt) Ltd', 'Pakistan', 'info@getzpharma.com', '021-35141000', 'Plot 143, Sector 7-A, Industrial Area, Karachi'), +('GSK Pakistan Ltd', 'Pakistan', 'gsk.pakistan@gsk.com', '021-35661891', 'Clifton, Karachi, Sindh'), +('Pfizer Pakistan Ltd', 'Pakistan', 'pfizer.pk@pfizer.com', '021-35293333', 'DHA Phase-IV, Karachi'), +('Searle Pakistan Ltd', 'Pakistan', 'info@searle.com.pk', '021-35650043', 'F-268, S.I.T.E., Karachi'), +('Barrett Hodgson Pakistan', 'Pakistan', 'info@barretthodgson.com', '021-32463121', 'Plot 23, Korangi Industrial Area, Karachi'), +('AGP Limited', 'Pakistan', 'info@agp.com.pk', '021-34322155', 'Plot 16, Sector 22, Korangi Industrial Area, Karachi'), +('Ferozsons Laboratories', 'Pakistan', 'info@ferozsons.com.pk', '042-35761010', '50-Empress Road, Lahore'), +('Reckitt Benckiser Pakistan', 'Pakistan', 'rb.pakistan@rb.com', '021-35660040', 'Karachi, Sindh'), +('Abbott Laboratories Pakistan','Pakistan','abbott.pk@abbott.com', '051-2876000', 'Islamabad, Pakistan'), +('Novartis Pakistan Ltd', 'Pakistan', 'novartis.pk@novartis.com', '021-35610501', 'Clifton, Karachi'); + +-- ───────────────────────────────────────── +-- SUPPLIERS +-- ───────────────────────────────────────── +INSERT INTO suppliers (name, contact_person, phone, email, address, rating, is_active) VALUES +('Getz Pharma Direct', 'Tariq Mehmood', '03001234501', 'tariq@getzpharma.com', 'Karachi, Sindh', 4.8, 1), +('Searle Pakistan Dist.', 'Hamid Ansari', '03001234502', 'hamid@searledist.com', 'Lahore, Punjab', 4.5, 1), +('Barrett Hodgson Supply', 'Nadia Siddiqui', '03001234503', 'nadia@bhsupply.com.pk', 'Karachi, Sindh', 4.2, 1), +('AGP Distributors', 'Kashif Ali', '03001234504', 'kashif@agpdist.com', 'Karachi, Sindh', 4.6, 1), +('MedLink Supplies', 'Usman Ghani', '03001234505', 'usman@medlink.pk', 'Hyderabad, Sindh', 4.0, 1), +('PharmaHub Karachi', 'Saima Akhtar', '03001234506', 'saima@pharmahub.pk', 'Karachi, Sindh', 3.8, 1), +('HealthSource Pakistan', 'Bilal Raza', '03001234507', 'bilal@healthsource.pk', 'Rawalpindi, Punjab', 4.3, 1), +('National Pharma Trading', 'Aamir Liaquat', '03001234508', 'aamir@npt.com.pk', 'Islamabad, ICT', 4.1, 1), +('City Pharma Supplies', 'Farida Khatoon', '03001234509', 'farida@citypharma.pk', 'Faisalabad, Punjab', 3.9, 1), +('Sunrise Medical Dist.', 'Irfan Sheikh', '03001234510', 'irfan@sunrisemed.pk', 'Multan, Punjab', 4.4, 1); + +-- ───────────────────────────────────────── +-- MEDICINES +-- ───────────────────────────────────────── +INSERT INTO medicines (name, generic_name, category_id, manufacturer_id, dosage_form, strength, unit_price, requires_prescription, description, is_active) VALUES +('Panadol Extra', 'Paracetamol + Caffeine', 1, 1, 'Tablet', '500mg/65mg', 25.00, 0, 'Fast-acting pain reliever with caffeine', 1), +('Brufen', 'Ibuprofen', 1, 2, 'Tablet', '400mg', 35.00, 0, 'Anti-inflammatory pain reliever', 1), +('Ponstan', 'Mefenamic Acid', 1, 3, 'Capsule', '500mg', 45.00, 0, 'Pain relief for menstrual and dental pain', 1), +('Disprin', 'Aspirin', 1, 8, 'Tablet', '300mg', 15.00, 0, 'Pain relief and antiplatelet agent', 1), +('Augmentin', 'Amoxicillin+Clavulanate',2, 2, 'Tablet', '625mg', 185.00, 1, 'Broad-spectrum antibiotic combination', 1), +('Amoxil', 'Amoxicillin', 2, 3, 'Capsule', '500mg', 95.00, 1, 'Penicillin-type antibiotic', 1), +('Flagyl', 'Metronidazole', 2, 4, 'Tablet', '400mg', 55.00, 1, 'Antibiotic for anaerobic infections', 1), +('Cipro', 'Ciprofloxacin', 2, 5, 'Tablet', '500mg', 120.00, 1, 'Fluoroquinolone antibiotic', 1), +('Nexium', 'Esomeprazole', 3, 2, 'Tablet', '20mg', 145.00, 1, 'Proton pump inhibitor for acid reflux', 1), +('Gaviscon', 'Sodium Alginate', 3, 8, 'Tablet', '500mg', 85.00, 0, 'Antacid for heartburn and indigestion', 1), +('Clarityne', 'Loratadine', 4, 5, 'Tablet', '10mg', 65.00, 0, 'Non-drowsy antihistamine for allergies', 1), +('Ventolin', 'Salbutamol', 7, 2, 'Inhaler', '100mcg', 350.00, 1, 'Bronchodilator for asthma relief', 1), +('Betadine', 'Povidone Iodine', 8, 8, 'Drop', '10%', 180.00, 0, 'Antiseptic solution for wounds', 1), +('ORS Sachet', 'ORS Formula', 12, 9, 'Powder', 'Standard', 25.00, 0, 'Oral rehydration salts for dehydration', 1), +('Glucophage', 'Metformin', 6, 10, 'Tablet', '500mg', 75.00, 1, 'First-line antidiabetic medication', 1), +('Amlodipine', 'Amlodipine Besylate', 5, 6, 'Tablet', '5mg', 55.00, 1, 'Calcium channel blocker for hypertension', 1), +('Lasix', 'Furosemide', 5, 1, 'Tablet', '40mg', 40.00, 1, 'Loop diuretic for edema and hypertension', 1), +('Calpol Suspension', 'Paracetamol', 1, 2, 'Syrup', '120mg/5ml', 85.00, 0, 'Paracetamol suspension for children', 1), +('Dermovate Cream', 'Clobetasol Propionate', 8, 2, 'Cream', '0.05%', 220.00, 1, 'Potent corticosteroid for skin conditions', 1), +('Septran DS', 'Co-Trimoxazole', 2, 2, 'Tablet', '960mg', 35.00, 1, 'Sulfonamide antibiotic combination', 1); + +-- ───────────────────────────────────────── +-- STOCK +-- ───────────────────────────────────────── +INSERT INTO stock (medicine_id, supplier_id, quantity, reorder_level, batch_number, manufacturing_date, expiry_date) VALUES +(1, 1, 500, 50, 'GTZ-2024-001', '2024-01-15', '2026-12-31'), +(2, 2, 350, 50, 'GSK-2024-012', '2024-02-10', '2026-10-31'), +(3, 3, 200, 30, 'PFZ-2024-003', '2024-03-01', '2026-09-30'), +(4, 4, 400, 40, 'RCK-2024-007', '2024-01-20', '2026-11-30'), +(5, 2, 150, 25, 'GSK-2024-020', '2024-04-15', '2026-06-30'), +(6, 3, 180, 25, 'PFZ-2024-015', '2024-03-20', '2026-08-31'), +(7, 4, 120, 20, 'SRL-2024-009', '2024-02-28', '2026-07-31'), +(8, 5, 80, 15, 'BHD-2024-011', '2024-05-01', '2026-05-31'), +(9, 2, 90, 15, 'GSK-2024-033', '2024-04-01', '2026-04-30'), +(10, 6, 250, 30, 'RCK-2024-022', '2024-01-10', '2026-12-31'), +(11, 7, 300, 30, 'PFZ-2024-028', '2024-03-15', '2027-03-31'), +(12, 2, 60, 10, 'GSK-2024-041', '2024-06-01', '2026-06-30'), +(13, 6, 150, 20, 'RCK-2024-018', '2024-02-15', '2026-12-31'), +(14, 8, 600, 100, 'ABT-2024-005', '2024-01-01', '2027-06-30'), +(15, 10,200, 30, 'NVT-2024-017', '2024-05-10', '2027-05-31'), +(16, 4, 8, 15, 'AGP-2024-023', '2024-04-20', '2026-04-30'), +(17, 1, 5, 15, 'GTZ-2024-031', '2024-03-10', '2026-03-31'), +(18, 2, 120, 20, 'GSK-2024-038', '2024-06-15', '2026-11-30'), +(19, 2, 70, 10, 'GSK-2024-044', '2024-05-20', '2026-08-31'), +(20, 2, 95, 15, 'GSK-2024-049', '2024-04-05', '2026-09-30'); + +-- ───────────────────────────────────────── +-- DOCTORS +-- ───────────────────────────────────────── +INSERT INTO doctors (name, specialization, license_number, phone, hospital, is_active) VALUES +('Dr. Imran Khan', 'General Physician', 'PMDC-12345', '03011001001', 'Civil Hospital Karachi', 1), +('Dr. Farrukh Ahmed', 'Cardiologist', 'PMDC-23456', '03011001002', 'Aga Khan Hospital Karachi', 1), +('Dr. Sana Malik', 'Dermatologist', 'PMDC-34567', '03011001003', 'Jinnah Hospital Karachi', 1), +('Dr. Tariq Hussain', 'Pulmonologist', 'PMDC-45678', '03011001004', 'Liaquat National Hospital', 1), +('Dr. Amna Siddiqui', 'Endocrinologist', 'PMDC-56789', '03011001005', 'Aga Khan Hospital Karachi', 1), +('Dr. Rizwan Qureshi', 'Gastroenterologist', 'PMDC-67890', '03011001006', 'Civil Hospital Karachi', 1), +('Dr. Zainab Fatima', 'Gynecologist', 'PMDC-78901', '03011001007', 'South City Hospital Karachi', 1), +('Dr. Salman Mirza', 'Orthopedic Surgeon', 'PMDC-89012', '03011001008', 'Shifa International Islamabad', 1), +('Dr. Hina Baig', 'Pediatrician', 'PMDC-90123', '03011001009', 'The Children\'s Hospital Lahore',1), +('Dr. Naveed Alam', 'Neurologist', 'PMDC-01234', '03011001010', 'Liaquat National Hospital', 1); + +-- ───────────────────────────────────────── +-- CUSTOMERS +-- ───────────────────────────────────────── +INSERT INTO customers (name, phone, email, date_of_birth, gender, address) VALUES +('Ahmed Ali Khan', '03001000001', 'ahmed.ali@gmail.com', '1985-06-15', 'Male', 'House 12, Block A, PECHS, Karachi'), +('Fatima Noor', '03001000002', 'fatima.noor@yahoo.com', '1990-03-22', 'Female', 'Flat 5, Gulshan-e-Iqbal, Karachi'), +('Muhammad Usman', '03001000003', 'musman@hotmail.com', '1978-11-08', 'Male', 'Plot 45, DHA Phase 2, Karachi'), +('Ayesha Siddiqui', '03001000004', 'ayesha.s@gmail.com', '1995-07-14', 'Female', 'House 7, North Nazimabad, Karachi'), +('Bilal Raza', '03001000005', 'bilal.raza@gmail.com', '1982-01-30', 'Male', 'House 33, Gulberg II, Lahore'), +('Sana Zahid', '03001000006', 'sana.zahid@yahoo.com', '1993-09-05', 'Female', 'Street 4, G-9/1, Islamabad'), +('Asif Mehmood', '03001000007', 'asif.m@gmail.com', '1970-12-20', 'Male', 'House 22, Satellite Town, Rawalpindi'), +('Nadia Akhtar', '03001000008', 'nadia.a@hotmail.com', '1988-04-17', 'Female', 'Flat 12, Clifton Block 4, Karachi'), +('Imran Shaikh', '03001000009', 'imran.shaikh@gmail.com', '1975-08-25', 'Male', 'House 67, Model Town, Lahore'), +('Zara Hussain', '03001000010', 'zara.h@gmail.com', '1998-02-11', 'Female', 'House 9, F-8/3, Islamabad'), +('Tariq Javed', '03001000011', 'tariq.j@yahoo.com', '1965-05-03', 'Male', 'House 15, Gulshan Block 13, Karachi'), +('Rabia Waseem', '03001000012', 'rabia.w@gmail.com', '1992-10-28', 'Female', 'Flat 8, Garden West, Karachi'); + +-- ───────────────────────────────────────── +-- EMPLOYEES (password_hash = bcrypt of 'admin123') +-- ───────────────────────────────────────── +INSERT INTO employees (name, role, phone, email, salary, hire_date, is_active, username, password_hash) VALUES +('Moavia Amir', 'Admin', '03211000001', 'moavia@curepharma.pk', 75000.00, '2022-01-01', 1, 'admin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'), +('Dr. Khalid Baig', 'Pharmacist', '03211000002', 'khalid@curepharma.pk', 65000.00, '2022-03-15', 1, 'pharmacist1','$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'), +('Sara Qureshi', 'Cashier', '03211000003', 'sara@curepharma.pk', 35000.00, '2023-01-10', 1, 'cashier1', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'), +('Hassan Raza', 'Manager', '03211000004', 'hassan@curepharma.pk', 55000.00, '2022-06-01', 1, 'manager1', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'), +('Amina Tariq', 'Pharmacist', '03211000005', 'amina@curepharma.pk', 62000.00, '2023-04-15', 1, 'pharmacist2','$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'), +('Rizwan Ahmed', 'Cashier', '03211000006', 'rizwan@curepharma.pk', 33000.00, '2024-02-01', 1, 'cashier2', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMKgP0ZuNT0g1VNhJF8WCJ9VkG'); +-- Note: All passwords above hash to 'admin123'. Change immediately in production. + +-- ───────────────────────────────────────── +-- PRESCRIPTIONS +-- ───────────────────────────────────────── +INSERT INTO prescriptions (customer_id, doctor_id, prescription_date, valid_until, notes, is_used) VALUES +(1, 1, '2026-04-01', '2026-07-01', 'Antibiotic course for throat infection', 0), +(2, 5, '2026-04-10', '2026-10-10', 'Diabetes management β€” monthly refill', 0), +(3, 2, '2026-04-15', '2027-04-15', 'Hypertension maintenance therapy', 0), +(4, 7, '2026-04-20', '2026-07-20', 'Prescribed for hormonal treatment', 0), +(5, 4, '2026-04-25', '2026-10-25', 'Asthma β€” Ventolin inhaler refill', 0), +(1, 6, '2026-03-15', '2026-06-15', 'PPI for gastroesophageal reflux', 1), +(8, 3, '2026-04-05', '2026-07-05', 'Dermovate cream for eczema', 0), +(9, 2, '2026-04-22', '2027-04-22', 'Long-term cardiac medication', 0), +(10,9, '2026-04-28', '2026-07-28', 'Pediatric antibiotic course', 0), +(11,1, '2026-05-01', '2026-08-01', 'General infection treatment', 0); + +-- ───────────────────────────────────────── +-- ORDERS + ORDER ITEMS + PAYMENTS (sample completed orders) +-- ───────────────────────────────────────── +INSERT INTO orders (customer_id, employee_id, prescription_id, status, subtotal, discount_percent, tax_percent, total_amount, notes) VALUES +(1, 3, 6, 'Completed', 275.00, 5.00, 0.00, 261.25, 'Regular customer discount applied'), +(2, 3, NULL, 'Completed', 195.00, 0.00, 0.00, 195.00, 'Walk-in purchase'), +(3, 3, NULL, 'Completed', 110.00, 0.00, 0.00, 110.00, NULL), +(5, 3, 5, 'Completed', 700.00, 10.00,0.00, 630.00, 'Bulk purchase β€” Ventolin + antibiotics'), +(NULL,3,NULL, 'Completed', 75.00, 0.00, 0.00, 75.00, 'Walk-in customer'), +(4, 3, NULL, 'Pending', 230.00, 0.00, 0.00, 230.00, NULL), +(8, 3, 7, 'Completed', 220.00, 0.00, 0.00, 220.00, 'Dermovate + supplement'), +(11, 3, NULL, 'Completed', 135.00, 5.00, 0.00, 128.25, NULL); + +INSERT INTO order_items (order_id, medicine_id, quantity, unit_price) VALUES +(1, 9, 1, 145.00), (1, 14, 4, 25.00), (1, 10, 1, 85.00), +(2, 18, 1, 85.00), (2, 14, 2, 25.00), (2, 10, 1, 85.00), +(3, 1, 2, 25.00), (3, 4, 4, 15.00), +(4, 12, 1, 350.00), (4, 6, 1, 95.00), (4, 8, 1, 120.00), (4, 14, 6, 25.00), +(5, 14, 3, 25.00), +(7, 19, 1, 220.00), +(8, 1, 3, 25.00), (8, 14, 2, 25.00), (8, 10, 1, 85.00); + +INSERT INTO payments (order_id, amount_paid, payment_method, payment_status, transaction_ref, paid_at) VALUES +(1, 261.25, 'Cash', 'Paid', NULL, '2026-04-02 10:30:00'), +(2, 195.00, 'Card', 'Paid', 'TXN-9981234','2026-04-10 14:15:00'), +(3, 110.00, 'Cash', 'Paid', NULL, '2026-04-16 09:45:00'), +(4, 630.00, 'Online', 'Paid', 'TXN-9982345','2026-04-25 16:00:00'), +(5, 75.00, 'Cash', 'Paid', NULL, '2026-04-26 11:20:00'), +(7, 220.00, 'Cash', 'Paid', NULL, '2026-04-06 13:30:00'), +(8, 128.25, 'Card', 'Paid', 'TXN-9983456','2026-05-01 17:00:00'); diff --git a/msms/database/schema.sql b/msms/database/schema.sql new file mode 100644 index 0000000..3e0fa3d --- /dev/null +++ b/msms/database/schema.sql @@ -0,0 +1,253 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- schema.sql β€” Full Database Schema (MySQL 8.0, 3NF Normalized) +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- Course: Database Lab β€” 4th Semester BS AI +-- Instructor: Sir Ahsan Ahmed +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS msms_db + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE msms_db; + +-- ───────────────────────────────────────── +-- 1. CATEGORIES +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS categories ( + category_id INT NOT NULL AUTO_INCREMENT, + category_name VARCHAR(100) NOT NULL, + description TEXT, + PRIMARY KEY (category_id), + UNIQUE KEY uq_category_name (category_name) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 2. MANUFACTURERS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS manufacturers ( + manufacturer_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + country VARCHAR(100), + contact_email VARCHAR(150), + phone VARCHAR(20), + address TEXT, + PRIMARY KEY (manufacturer_id), + UNIQUE KEY uq_manufacturer_name (name), + UNIQUE KEY uq_manufacturer_email (contact_email) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 3. SUPPLIERS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS suppliers ( + supplier_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + contact_person VARCHAR(100), + phone VARCHAR(20) NOT NULL, + email VARCHAR(150), + address TEXT, + rating DECIMAL(3,1) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (supplier_id), + CONSTRAINT chk_supplier_rating CHECK (rating BETWEEN 1.0 AND 5.0) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 4. MEDICINES +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS medicines ( + medicine_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + generic_name VARCHAR(200), + category_id INT, + manufacturer_id INT, + dosage_form ENUM('Tablet','Capsule','Syrup','Injection','Cream','Drop','Inhaler','Powder','Patch') NOT NULL, + strength VARCHAR(50), + unit_price DECIMAL(10,2) NOT NULL, + requires_prescription TINYINT(1) NOT NULL DEFAULT 0, + description TEXT, + is_active TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (medicine_id), + CONSTRAINT chk_medicine_price CHECK (unit_price > 0), + CONSTRAINT fk_medicine_category FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE SET NULL, + CONSTRAINT fk_medicine_manufacturer FOREIGN KEY (manufacturer_id) REFERENCES manufacturers(manufacturer_id) ON DELETE SET NULL +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 5. STOCK +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS stock ( + stock_id INT NOT NULL AUTO_INCREMENT, + medicine_id INT NOT NULL, + supplier_id INT, + quantity INT NOT NULL DEFAULT 0, + reorder_level INT NOT NULL DEFAULT 10, + batch_number VARCHAR(100), + manufacturing_date DATE, + expiry_date DATE NOT NULL, + last_restocked TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (stock_id), + UNIQUE KEY uq_stock_medicine (medicine_id), + CONSTRAINT chk_stock_qty CHECK (quantity >= 0), + CONSTRAINT chk_stock_expiry CHECK (expiry_date > manufacturing_date), + CONSTRAINT fk_stock_medicine FOREIGN KEY (medicine_id) REFERENCES medicines(medicine_id) ON DELETE CASCADE, + CONSTRAINT fk_stock_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) ON DELETE SET NULL +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 6. DOCTORS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS doctors ( + doctor_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + specialization VARCHAR(150), + license_number VARCHAR(100) NOT NULL, + phone VARCHAR(20), + hospital VARCHAR(200), + is_active TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (doctor_id), + UNIQUE KEY uq_doctor_license (license_number) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 7. CUSTOMERS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS customers ( + customer_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR(150), + date_of_birth DATE, + gender ENUM('Male','Female','Other'), + address TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (customer_id), + UNIQUE KEY uq_customer_phone (phone) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 8. EMPLOYEES +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS employees ( + employee_id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + role ENUM('Pharmacist','Cashier','Manager','Admin') NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR(150), + salary DECIMAL(10,2), + hire_date DATE NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + username VARCHAR(100) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + PRIMARY KEY (employee_id), + UNIQUE KEY uq_employee_phone (phone), + UNIQUE KEY uq_employee_email (email), + UNIQUE KEY uq_employee_username (username), + CONSTRAINT chk_employee_salary CHECK (salary > 0) +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 9. PRESCRIPTIONS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS prescriptions ( + prescription_id INT NOT NULL AUTO_INCREMENT, + customer_id INT NOT NULL, + doctor_id INT NOT NULL, + prescription_date DATE NOT NULL, + valid_until DATE, + notes TEXT, + is_used TINYINT(1) NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (prescription_id), + CONSTRAINT chk_prescription_valid CHECK (valid_until IS NULL OR valid_until >= prescription_date), + CONSTRAINT fk_prescription_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE RESTRICT, + CONSTRAINT fk_prescription_doctor FOREIGN KEY (doctor_id) REFERENCES doctors(doctor_id) ON DELETE RESTRICT +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 10. ORDERS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS orders ( + order_id INT NOT NULL AUTO_INCREMENT, + customer_id INT, + employee_id INT NOT NULL, + prescription_id INT, + order_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + status ENUM('Pending','Completed','Cancelled','Refunded') NOT NULL DEFAULT 'Pending', + subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00, + discount_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00, + tax_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00, + total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, + notes TEXT, + PRIMARY KEY (order_id), + CONSTRAINT chk_order_discount CHECK (discount_percent BETWEEN 0 AND 100), + CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE SET NULL, + CONSTRAINT fk_order_employee FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT, + CONSTRAINT fk_order_prescription FOREIGN KEY (prescription_id) REFERENCES prescriptions(prescription_id) ON DELETE SET NULL +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 11. ORDER ITEMS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS order_items ( + item_id INT NOT NULL AUTO_INCREMENT, + order_id INT NOT NULL, + medicine_id INT NOT NULL, + quantity INT NOT NULL, + unit_price DECIMAL(10,2) NOT NULL, + subtotal DECIMAL(10,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, + PRIMARY KEY (item_id), + CONSTRAINT chk_item_qty CHECK (quantity > 0), + CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, + CONSTRAINT fk_item_medicine FOREIGN KEY (medicine_id) REFERENCES medicines(medicine_id) ON DELETE RESTRICT +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- 12. PAYMENTS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS payments ( + payment_id INT NOT NULL AUTO_INCREMENT, + order_id INT NOT NULL, + amount_paid DECIMAL(10,2) NOT NULL, + payment_method ENUM('Cash','Card','Online','Insurance') NOT NULL, + payment_status ENUM('Paid','Pending','Failed','Refunded') NOT NULL DEFAULT 'Pending', + transaction_ref VARCHAR(200), + paid_at TIMESTAMP, + notes TEXT, + PRIMARY KEY (payment_id), + UNIQUE KEY uq_payment_order (order_id), + CONSTRAINT chk_payment_amount CHECK (amount_paid >= 0), + CONSTRAINT fk_payment_order FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE RESTRICT +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- AUXILIARY TABLE: LOW STOCK ALERTS +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS low_stock_alerts ( + alert_id INT NOT NULL AUTO_INCREMENT, + medicine_id INT NOT NULL, + current_qty INT NOT NULL, + reorder_level INT NOT NULL, + alerted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_resolved TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (alert_id), + CONSTRAINT fk_alert_medicine FOREIGN KEY (medicine_id) REFERENCES medicines(medicine_id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- ───────────────────────────────────────── +-- AUXILIARY TABLE: STOCK TRANSACTIONS LOG +-- ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS stock_transactions ( + txn_id INT NOT NULL AUTO_INCREMENT, + medicine_id INT NOT NULL, + employee_id INT, + qty_change INT NOT NULL, + txn_type ENUM('IN','OUT') NOT NULL, + reference VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (txn_id), + CONSTRAINT fk_txn_medicine FOREIGN KEY (medicine_id) REFERENCES medicines(medicine_id) ON DELETE CASCADE, + CONSTRAINT fk_txn_employee FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE SET NULL +) ENGINE=InnoDB; diff --git a/msms/database/stored_procedures.sql b/msms/database/stored_procedures.sql new file mode 100644 index 0000000..ed12a55 --- /dev/null +++ b/msms/database/stored_procedures.sql @@ -0,0 +1,240 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- stored_procedures.sql +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- ============================================================ + +USE msms_db; + +DROP PROCEDURE IF EXISTS GenerateBill; +DROP PROCEDURE IF EXISTS RestockMedicine; +DROP PROCEDURE IF EXISTS PlaceOrder; +DROP PROCEDURE IF EXISTS CompletePayment; + +DELIMITER $$ + +-- ───────────────────────────────────────── +-- PROCEDURE 1: GenerateBill +-- Returns full bill dataset for an order +-- ───────────────────────────────────────── +CREATE PROCEDURE GenerateBill(IN p_order_id INT) +BEGIN + -- Order header + SELECT + o.order_id, + CONCAT('ORD-', LPAD(o.order_id, 5, '0')) AS invoice_number, + o.order_date, + o.status, + o.subtotal, + o.discount_percent, + ROUND(o.subtotal * o.discount_percent / 100, 2) AS discount_amount, + o.tax_percent, + ROUND((o.subtotal - o.subtotal * o.discount_percent / 100) * o.tax_percent / 100, 2) AS tax_amount, + o.total_amount, + o.notes AS order_notes, + -- Customer info + COALESCE(c.name, 'Walk-in Customer') AS customer_name, + COALESCE(c.phone, 'N/A') AS customer_phone, + -- Employee info + e.name AS cashier_name, + e.role AS cashier_role, + -- Prescription info + CONCAT('RX-', LPAD(p.prescription_id,3,'0')) AS prescription_number, + p.prescription_date, + -- Payment info + py.payment_method, + py.payment_status, + py.amount_paid, + py.transaction_ref, + py.paid_at + FROM orders o + LEFT JOIN customers c ON c.customer_id = o.customer_id + JOIN employees e ON e.employee_id = o.employee_id + LEFT JOIN prescriptions p ON p.prescription_id = o.prescription_id + LEFT JOIN payments py ON py.order_id = o.order_id + WHERE o.order_id = p_order_id; + + -- Order line items + SELECT + oi.item_id, + m.name AS medicine_name, + m.strength, + m.dosage_form, + oi.quantity, + oi.unit_price, + oi.subtotal AS line_subtotal + FROM order_items oi + JOIN medicines m ON m.medicine_id = oi.medicine_id + WHERE oi.order_id = p_order_id + ORDER BY oi.item_id; +END$$ + +-- ───────────────────────────────────────── +-- PROCEDURE 2: RestockMedicine +-- ───────────────────────────────────────── +CREATE PROCEDURE RestockMedicine( + IN p_medicine_id INT, + IN p_quantity INT, + IN p_supplier_id INT, + IN p_expiry_date DATE, + IN p_batch VARCHAR(100), + IN p_employee_id INT +) +BEGIN + DECLARE v_exists INT DEFAULT 0; + + -- Validate quantity + IF p_quantity <= 0 THEN + SIGNAL SQLSTATE '45004' + SET MESSAGE_TEXT = 'Restock quantity must be greater than zero.'; + END IF; + + -- Validate expiry date + IF p_expiry_date <= CURDATE() THEN + SIGNAL SQLSTATE '45005' + SET MESSAGE_TEXT = 'Expiry date must be a future date.'; + END IF; + + -- Check stock record exists + SELECT COUNT(*) INTO v_exists FROM stock WHERE medicine_id = p_medicine_id; + + IF v_exists = 0 THEN + -- Create stock record if missing + INSERT INTO stock (medicine_id, supplier_id, quantity, batch_number, expiry_date, last_restocked) + VALUES (p_medicine_id, p_supplier_id, p_quantity, p_batch, p_expiry_date, NOW()); + ELSE + UPDATE stock + SET + quantity = quantity + p_quantity, + supplier_id = COALESCE(p_supplier_id, supplier_id), + batch_number = COALESCE(p_batch, batch_number), + expiry_date = COALESCE(p_expiry_date, expiry_date), + last_restocked = NOW() + WHERE medicine_id = p_medicine_id; + END IF; + + -- Resolve any open low-stock alerts for this medicine + UPDATE low_stock_alerts + SET is_resolved = 1 + WHERE medicine_id = p_medicine_id AND is_resolved = 0; + + -- Log the incoming stock transaction + INSERT INTO stock_transactions (medicine_id, employee_id, qty_change, txn_type, reference) + VALUES (p_medicine_id, p_employee_id, p_quantity, 'IN', CONCAT('RESTOCK-BATCH-', COALESCE(p_batch,'N/A'))); + + SELECT CONCAT('Medicine ID ', p_medicine_id, ' restocked by ', p_quantity, ' units.') AS message; +END$$ + +-- ───────────────────────────────────────── +-- PROCEDURE 3: PlaceOrder +-- ───────────────────────────────────────── +CREATE PROCEDURE PlaceOrder( + IN p_customer_id INT, + IN p_employee_id INT, + IN p_prescription_id INT, + IN p_discount DECIMAL(5,2), + IN p_tax DECIMAL(5,2), + IN p_notes TEXT, + OUT p_order_id INT +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + START TRANSACTION; + + IF p_discount < 0 OR p_discount > 100 THEN + SIGNAL SQLSTATE '45006' + SET MESSAGE_TEXT = 'Discount percent must be between 0 and 100.'; + END IF; + + INSERT INTO orders + (customer_id, employee_id, prescription_id, status, discount_percent, tax_percent, notes) + VALUES + (p_customer_id, p_employee_id, p_prescription_id, 'Pending', p_discount, p_tax, p_notes); + + SET p_order_id = LAST_INSERT_ID(); + + COMMIT; +END$$ + +-- ───────────────────────────────────────── +-- PROCEDURE 4: CompletePayment +-- ───────────────────────────────────────── +CREATE PROCEDURE CompletePayment( + IN p_order_id INT, + IN p_method VARCHAR(20), + IN p_amount DECIMAL(10,2), + IN p_ref VARCHAR(200) +) +BEGIN + DECLARE v_total DECIMAL(10,2); + DECLARE v_status VARCHAR(20); + DECLARE v_pay_exists INT DEFAULT 0; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + START TRANSACTION; + + -- Fetch order total and current status + SELECT total_amount, status + INTO v_total, v_status + FROM orders + WHERE order_id = p_order_id + FOR UPDATE; + + IF v_total IS NULL THEN + SIGNAL SQLSTATE '45007' + SET MESSAGE_TEXT = 'Order not found.'; + END IF; + + IF v_status = 'Completed' THEN + SIGNAL SQLSTATE '45008' + SET MESSAGE_TEXT = 'Order is already completed.'; + END IF; + + IF v_status = 'Cancelled' THEN + SIGNAL SQLSTATE '45009' + SET MESSAGE_TEXT = 'Cannot pay for a cancelled order.'; + END IF; + + IF p_amount < v_total THEN + SIGNAL SQLSTATE '45010' + SET MESSAGE_TEXT = 'Amount paid is less than the order total.'; + END IF; + + -- Check for duplicate payment record + SELECT COUNT(*) INTO v_pay_exists FROM payments WHERE order_id = p_order_id; + IF v_pay_exists > 0 THEN + SIGNAL SQLSTATE '45011' + SET MESSAGE_TEXT = 'A payment record already exists for this order.'; + END IF; + + INSERT INTO payments + (order_id, amount_paid, payment_method, payment_status, transaction_ref, paid_at) + VALUES + (p_order_id, p_amount, p_method, 'Paid', p_ref, NOW()); + + UPDATE orders + SET status = 'Completed' + WHERE order_id = p_order_id; + + -- Mark prescription as used if linked + UPDATE prescriptions pr + JOIN orders o ON o.prescription_id = pr.prescription_id + SET pr.is_used = 1 + WHERE o.order_id = p_order_id; + + COMMIT; + + SELECT 'Payment completed successfully.' AS message; +END$$ + +DELIMITER ; diff --git a/msms/database/triggers.sql b/msms/database/triggers.sql new file mode 100644 index 0000000..22ab655 --- /dev/null +++ b/msms/database/triggers.sql @@ -0,0 +1,141 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- triggers.sql β€” All Database Triggers +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- Course: Database Lab β€” 4th Semester BS AI +-- Instructor: Sir Ahsan Ahmed +-- ============================================================ + +USE msms_db; + +-- Drop existing triggers if they exist +DROP TRIGGER IF EXISTS after_order_item_insert_stock; +DROP TRIGGER IF EXISTS after_order_item_insert_lowstock; +DROP TRIGGER IF EXISTS after_order_item_insert_update_total; +DROP TRIGGER IF EXISTS before_stock_update_expiry_check; + +DELIMITER $$ + +-- ───────────────────────────────────────── +-- TRIGGER 1: Deduct stock on order item insert +-- ───────────────────────────────────────── +CREATE TRIGGER after_order_item_insert_stock +AFTER INSERT ON order_items +FOR EACH ROW +BEGIN + DECLARE v_current_qty INT; + + SELECT quantity INTO v_current_qty + FROM stock + WHERE medicine_id = NEW.medicine_id; + + IF v_current_qty IS NULL THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'No stock record found for this medicine.'; + END IF; + + IF v_current_qty < NEW.quantity THEN + SIGNAL SQLSTATE '45001' + SET MESSAGE_TEXT = 'Insufficient stock: cannot fulfil requested quantity.'; + END IF; + + UPDATE stock + SET quantity = quantity - NEW.quantity + WHERE medicine_id = NEW.medicine_id; + + -- Log the outgoing stock transaction + INSERT INTO stock_transactions (medicine_id, employee_id, qty_change, txn_type, reference) + SELECT NEW.medicine_id, + o.employee_id, + NEW.quantity, + 'OUT', + CONCAT('ORDER-', NEW.order_id) + FROM orders o + WHERE o.order_id = NEW.order_id; +END$$ + +-- ───────────────────────────────────────── +-- TRIGGER 2: Low stock alert after order item insert +-- ───────────────────────────────────────── +CREATE TRIGGER after_order_item_insert_lowstock +AFTER INSERT ON order_items +FOR EACH ROW +BEGIN + DECLARE v_qty INT; + DECLARE v_reorder INT; + + SELECT quantity, reorder_level + INTO v_qty, v_reorder + FROM stock + WHERE medicine_id = NEW.medicine_id; + + IF v_qty IS NOT NULL AND v_qty < v_reorder THEN + -- Only insert alert if one doesn't already exist for today + IF NOT EXISTS ( + SELECT 1 FROM low_stock_alerts + WHERE medicine_id = NEW.medicine_id + AND DATE(alerted_at) = CURDATE() + AND is_resolved = 0 + ) THEN + INSERT INTO low_stock_alerts (medicine_id, current_qty, reorder_level) + VALUES (NEW.medicine_id, v_qty, v_reorder); + END IF; + END IF; +END$$ + +-- ───────────────────────────────────────── +-- TRIGGER 3: Recalculate order totals after item insert +-- ───────────────────────────────────────── +CREATE TRIGGER after_order_item_insert_update_total +AFTER INSERT ON order_items +FOR EACH ROW +BEGIN + DECLARE v_subtotal DECIMAL(10,2); + DECLARE v_discount_pct DECIMAL(5,2); + DECLARE v_tax_pct DECIMAL(5,2); + DECLARE v_discount_amt DECIMAL(10,2); + DECLARE v_tax_amt DECIMAL(10,2); + DECLARE v_total DECIMAL(10,2); + + -- Sum all line-item subtotals for this order + SELECT COALESCE(SUM(quantity * unit_price), 0) + INTO v_subtotal + FROM order_items + WHERE order_id = NEW.order_id; + + -- Fetch discount/tax percentages + SELECT discount_percent, tax_percent + INTO v_discount_pct, v_tax_pct + FROM orders + WHERE order_id = NEW.order_id; + + SET v_discount_amt = ROUND(v_subtotal * v_discount_pct / 100, 2); + SET v_tax_amt = ROUND((v_subtotal - v_discount_amt) * v_tax_pct / 100, 2); + SET v_total = v_subtotal - v_discount_amt + v_tax_amt; + + UPDATE orders + SET subtotal = v_subtotal, + total_amount = v_total + WHERE order_id = NEW.order_id; +END$$ + +-- ───────────────────────────────────────── +-- TRIGGER 4: Prevent expired stock updates +-- ───────────────────────────────────────── +CREATE TRIGGER before_stock_update_expiry_check +BEFORE UPDATE ON stock +FOR EACH ROW +BEGIN + IF NEW.expiry_date < CURDATE() THEN + SIGNAL SQLSTATE '45002' + SET MESSAGE_TEXT = 'Cannot set expiry_date to a past date.'; + END IF; + + IF NEW.manufacturing_date IS NOT NULL + AND NEW.expiry_date <= NEW.manufacturing_date THEN + SIGNAL SQLSTATE '45003' + SET MESSAGE_TEXT = 'expiry_date must be after manufacturing_date.'; + END IF; +END$$ + +DELIMITER ; diff --git a/msms/database/views.sql b/msms/database/views.sql new file mode 100644 index 0000000..4aae6cf --- /dev/null +++ b/msms/database/views.sql @@ -0,0 +1,175 @@ +-- ============================================================ +-- Medical Store Management System (MSMS) +-- views.sql β€” All Database Views +-- Student: Moavia Amir | ID: 2k24_BSAI_72 +-- ============================================================ + +USE msms_db; + +DROP VIEW IF EXISTS vw_LowStockAlert; +DROP VIEW IF EXISTS vw_ExpiringSoon; +DROP VIEW IF EXISTS vw_SalesSummary; +DROP VIEW IF EXISTS vw_TopSellingMedicines; +DROP VIEW IF EXISTS vw_EmployeePerformance; +DROP VIEW IF EXISTS vw_SupplierRatings; +DROP VIEW IF EXISTS vw_FullInventory; + +-- ───────────────────────────────────────── +-- VIEW 1: Low Stock Alert +-- ───────────────────────────────────────── +CREATE VIEW vw_LowStockAlert AS +SELECT + m.medicine_id, + m.name AS medicine_name, + m.dosage_form, + m.strength, + c.category_name, + s.quantity AS current_stock, + s.reorder_level, + (s.reorder_level - s.quantity) AS units_below_reorder, + s.expiry_date, + sup.name AS supplier_name, + sup.phone AS supplier_phone +FROM medicines m +JOIN stock s ON s.medicine_id = m.medicine_id +JOIN categories c ON c.category_id = m.category_id +LEFT JOIN suppliers sup ON sup.supplier_id = s.supplier_id +WHERE s.quantity < s.reorder_level + AND m.is_active = 1 +ORDER BY units_below_reorder DESC; + +-- ───────────────────────────────────────── +-- VIEW 2: Expiring Soon (within 30 days) +-- ───────────────────────────────────────── +CREATE VIEW vw_ExpiringSoon AS +SELECT + m.medicine_id, + m.name AS medicine_name, + m.dosage_form, + m.strength, + c.category_name, + s.quantity AS current_stock, + s.batch_number, + s.expiry_date, + DATEDIFF(s.expiry_date, CURDATE()) AS days_until_expiry, + sup.name AS supplier_name +FROM medicines m +JOIN stock s ON s.medicine_id = m.medicine_id +JOIN categories c ON c.category_id = m.category_id +LEFT JOIN suppliers sup ON sup.supplier_id = s.supplier_id +WHERE s.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) + AND m.is_active = 1 +ORDER BY s.expiry_date ASC; + +-- ───────────────────────────────────────── +-- VIEW 3: Sales Summary (by date) +-- ───────────────────────────────────────── +CREATE VIEW vw_SalesSummary AS +SELECT + DATE(o.order_date) AS sale_date, + COUNT(o.order_id) AS total_orders, + SUM(o.total_amount) AS total_revenue, + AVG(o.total_amount) AS avg_order_value, + SUM(o.discount_percent * o.subtotal / 100) AS total_discounts_given, + COUNT(DISTINCT o.customer_id) AS unique_customers +FROM orders o +WHERE o.status = 'Completed' +GROUP BY DATE(o.order_date) +ORDER BY sale_date DESC; + +-- ───────────────────────────────────────── +-- VIEW 4: Top Selling Medicines +-- ───────────────────────────────────────── +CREATE VIEW vw_TopSellingMedicines AS +SELECT + m.medicine_id, + m.name AS medicine_name, + m.dosage_form, + m.strength, + c.category_name, + SUM(oi.quantity) AS total_sold, + SUM(oi.subtotal) AS total_revenue, + COUNT(DISTINCT oi.order_id) AS order_count, + m.unit_price +FROM order_items oi +JOIN medicines m ON m.medicine_id = oi.medicine_id +JOIN categories c ON c.category_id = m.category_id +JOIN orders o ON o.order_id = oi.order_id +WHERE o.status = 'Completed' +GROUP BY m.medicine_id, m.name, m.dosage_form, m.strength, c.category_name, m.unit_price +ORDER BY total_sold DESC; + +-- ───────────────────────────────────────── +-- VIEW 5: Employee Performance +-- ───────────────────────────────────────── +CREATE VIEW vw_EmployeePerformance AS +SELECT + e.employee_id, + e.name AS employee_name, + e.role, + COUNT(o.order_id) AS total_orders, + COALESCE(SUM(o.total_amount), 0) AS total_revenue_handled, + COALESCE(AVG(o.total_amount), 0) AS avg_order_value, + COUNT(CASE WHEN o.status = 'Completed' THEN 1 END) AS completed_orders, + COUNT(CASE WHEN o.status = 'Cancelled' THEN 1 END) AS cancelled_orders +FROM employees e +LEFT JOIN orders o ON o.employee_id = e.employee_id +WHERE e.is_active = 1 +GROUP BY e.employee_id, e.name, e.role +ORDER BY total_revenue_handled DESC; + +-- ───────────────────────────────────────── +-- VIEW 6: Supplier Ratings +-- ───────────────────────────────────────── +CREATE VIEW vw_SupplierRatings AS +SELECT + sup.supplier_id, + sup.name AS supplier_name, + sup.contact_person, + sup.phone, + sup.email, + sup.rating, + COUNT(s.stock_id) AS medicines_supplied, + SUM(s.quantity) AS total_units_in_stock, + sup.is_active +FROM suppliers sup +LEFT JOIN stock s ON s.supplier_id = sup.supplier_id +GROUP BY sup.supplier_id, sup.name, sup.contact_person, sup.phone, sup.email, sup.rating, sup.is_active +ORDER BY sup.rating DESC; + +-- ───────────────────────────────────────── +-- VIEW 7: Full Inventory +-- ───────────────────────────────────────── +CREATE VIEW vw_FullInventory AS +SELECT + m.medicine_id, + m.name AS medicine_name, + m.generic_name, + m.dosage_form, + m.strength, + m.unit_price, + m.requires_prescription, + m.is_active AS medicine_active, + c.category_name, + mfr.name AS manufacturer_name, + mfr.country, + s.stock_id, + s.quantity AS stock_qty, + s.reorder_level, + CASE + WHEN s.quantity = 0 THEN 'Out of Stock' + WHEN s.quantity < s.reorder_level THEN 'Low Stock' + ELSE 'In Stock' + END AS stock_status, + s.batch_number, + s.manufacturing_date, + s.expiry_date, + DATEDIFF(s.expiry_date, CURDATE()) AS days_to_expiry, + s.last_restocked, + sup.name AS supplier_name +FROM medicines m +LEFT JOIN categories c ON c.category_id = m.category_id +LEFT JOIN manufacturers mfr ON mfr.manufacturer_id = m.manufacturer_id +LEFT JOIN stock s ON s.medicine_id = m.medicine_id +LEFT JOIN suppliers sup ON sup.supplier_id = s.supplier_id +ORDER BY m.name ASC; diff --git a/msms/docs/MSMS_Project_Manual (1).pdf b/msms/docs/MSMS_Project_Manual (1).pdf new file mode 100644 index 0000000..1539968 Binary files /dev/null and b/msms/docs/MSMS_Project_Manual (1).pdf differ diff --git a/msms/docs/Medical_Store_Proposal.pdf b/msms/docs/Medical_Store_Proposal.pdf new file mode 100644 index 0000000..638e36d Binary files /dev/null and b/msms/docs/Medical_Store_Proposal.pdf differ diff --git a/msms/docs/msms_er_diagram (1).html b/msms/docs/msms_er_diagram (1).html new file mode 100644 index 0000000..4195d09 --- /dev/null +++ b/msms/docs/msms_er_diagram (1).html @@ -0,0 +1,213 @@ + + + +
+

Entity-Relationship Diagram 12 Tables

+

Medical Store Management System (MSMS)

+
+ +
+ + diff --git a/msms/frontend/index.html b/msms/frontend/index.html new file mode 100644 index 0000000..814e97b --- /dev/null +++ b/msms/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + Cure Pharmacy β€” MSMS + + + + + +
+ + + diff --git a/msms/frontend/package-lock.json b/msms/frontend/package-lock.json new file mode 100644 index 0000000..e912262 --- /dev/null +++ b/msms/frontend/package-lock.json @@ -0,0 +1,3634 @@ +{ + "name": "msms-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "msms-frontend", + "version": "1.0.0", + "dependencies": { + "@hookform/resolvers": "^3.3.4", + "axios": "^1.6.2", + "date-fns": "^3.0.6", + "html2canvas": "^1.4.1", + "jspdf": "^2.5.1", + "lucide-react": "^0.312.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.49.3", + "react-hot-toast": "^2.4.1", + "react-router-dom": "^6.21.3", + "react-to-print": "^2.15.1", + "recharts": "^2.10.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "vite": "^5.1.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.312.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.312.0.tgz", + "integrity": "sha512-3UZsqyswRXjW4t+nw+InICewSimjPKHuSxiFYqTshv9xkK3tPPntXk/lvXc9pKlXIxm3v9WKyoxcrB6YHhP+dg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.75.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz", + "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-to-print": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.15.1.tgz", + "integrity": "sha512-1foogIFbCpzAVxydkhBiDfMiFYhIMphiagDOfcG4X/EcQ+fBPqJ0rby9Wv/emzY1YLkIQy/rEgOrWQT+rBKhjw==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/msms/frontend/package.json b/msms/frontend/package.json new file mode 100644 index 0000000..a345fa4 --- /dev/null +++ b/msms/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "msms-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.2", + "date-fns": "^3.0.6", + "html2canvas": "^1.4.1", + "jspdf": "^2.5.1", + "lucide-react": "^0.312.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.49.3", + "react-hot-toast": "^2.4.1", + "react-router-dom": "^6.21.3", + "react-to-print": "^2.15.1", + "recharts": "^2.10.3", + "zod": "^3.22.4", + "@hookform/resolvers": "^3.3.4" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "vite": "^5.1.0" + } +} diff --git a/msms/frontend/postcss.config.js b/msms/frontend/postcss.config.js new file mode 100644 index 0000000..e008c9c --- /dev/null +++ b/msms/frontend/postcss.config.js @@ -0,0 +1,3 @@ +export default { + plugins: { tailwindcss: {}, autoprefixer: {} }, +}; diff --git a/msms/frontend/public/favicon.svg b/msms/frontend/public/favicon.svg new file mode 100644 index 0000000..3c2648a --- /dev/null +++ b/msms/frontend/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/msms/frontend/src/App.jsx b/msms/frontend/src/App.jsx new file mode 100644 index 0000000..4d41e9b --- /dev/null +++ b/msms/frontend/src/App.jsx @@ -0,0 +1,122 @@ +// src/App.jsx +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { Toaster } from 'react-hot-toast'; +import { AuthProvider } from './context/AuthContext'; +import { CartProvider } from './context/CartContext'; +import ProtectedRoute from './components/Layout/ProtectedRoute'; +import Sidebar from './components/Layout/Sidebar'; +import Navbar from './components/Layout/Navbar'; + +// Pages +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import MedicineList from './pages/Inventory/MedicineList'; +import StockList from './pages/Inventory/StockList'; +import NewOrder from './pages/Sales/NewOrder'; +import OrderList from './pages/Sales/OrderList'; +import OrderDetail from './pages/Sales/OrderDetail'; +import CustomerList from './pages/Customers/CustomerList'; +import SupplierList from './pages/Suppliers/SupplierList'; +import DoctorList from './pages/Doctors/DoctorList'; +import EmployeeList from './pages/Employees/EmployeeList'; +import PrescriptionList from './pages/Prescriptions/PrescriptionList'; +import PaymentList from './pages/Payments/PaymentList'; +import Reports from './pages/Reports/Reports'; + +const PAGE_TITLES = { + '/': 'Dashboard', + '/medicines': 'Medicines', + '/stock': 'Stock', + '/orders/new': 'New Sale', + '/orders': 'Orders', + '/customers': 'Customers', + '/suppliers': 'Suppliers', + '/doctors': 'Doctors', + '/employees': 'Employees', + '/prescriptions': 'Prescriptions', + '/payments': 'Payments', + '/reports': 'Reports', +}; + +function AppLayout({ children }) { + const { pathname } = useLocation(); + const title = Object.entries(PAGE_TITLES) + .reverse() + .find(([path]) => pathname.startsWith(path))?.[1] || 'MSMS'; + + return ( +
+ +
+ +
+ {children} +
+
+
+ ); +} + +export default function App() { + return ( + + + + + } /> + + + + + } /> + + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + + + ); +} diff --git a/msms/frontend/src/api/index.js b/msms/frontend/src/api/index.js new file mode 100644 index 0000000..f0a0bf1 --- /dev/null +++ b/msms/frontend/src/api/index.js @@ -0,0 +1,79 @@ +// src/api/index.js β€” Axios instance + all API helpers +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api/v1', + headers: { 'Content-Type': 'application/json' }, +}); + +// Attach JWT on every request +api.interceptors.request.use((config) => { + const token = localStorage.getItem('msms_token'); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + +// Global 401 redirect +api.interceptors.response.use( + (res) => res, + (err) => { + if (err.response?.status === 401) { + localStorage.removeItem('msms_token'); + localStorage.removeItem('msms_user'); + window.location.href = '/login'; + } + return Promise.reject(err); + } +); + +// ── Auth ────────────────────────────────────────────────── +export const authAPI = { + login: (data) => api.post('/auth/login', data), + me: () => api.get('/auth/me'), +}; + +// ── Generic CRUD factory ───────────────────────────────── +const crud = (path) => ({ + getAll: (params) => api.get(path, { params }), + getById: (id) => api.get(`${path}/${id}`), + create: (data) => api.post(path, data), + update: (id, d) => api.put(`${path}/${id}`, d), + remove: (id) => api.delete(`${path}/${id}`), +}); + +export const categoriesAPI = crud('/categories'); +export const manufacturersAPI = crud('/manufacturers'); +export const suppliersAPI = crud('/suppliers'); +export const medicinesAPI = crud('/medicines'); +export const stockAPI = { + ...crud('/stock'), + restock: (data) => api.post('/stock/restock', data), +}; +export const doctorsAPI = crud('/doctors'); +export const customersAPI = { + ...crud('/customers'), + searchByPhone: (phone) => api.get('/customers/search/phone', { params: { phone } }), +}; +export const employeesAPI = crud('/employees'); +export const prescriptionsAPI = crud('/prescriptions'); +export const paymentsAPI = crud('/payments'); + +// ── Orders ─────────────────────────────────────────────── +export const ordersAPI = { + ...crud('/orders'), + getBill: (id) => api.get(`/orders/${id}/bill`), + complete: (id, d)=> api.post(`/orders/${id}/complete`, d), + cancel: (id) => api.put(`/orders/${id}/cancel`), +}; + +// ── Reports ────────────────────────────────────────────── +export const reportsAPI = { + dashboardStats: () => api.get('/reports/dashboard-stats'), + lowStock: () => api.get('/reports/low-stock'), + expiringSoon: () => api.get('/reports/expiring-soon'), + salesSummary: (params) => api.get('/reports/sales-summary', { params }), + topMedicines: () => api.get('/reports/top-medicines'), + employeePerformance: () => api.get('/reports/employee-performance'), +}; + +export default api; diff --git a/msms/frontend/src/components/Bill/BillActions.jsx b/msms/frontend/src/components/Bill/BillActions.jsx new file mode 100644 index 0000000..857c681 --- /dev/null +++ b/msms/frontend/src/components/Bill/BillActions.jsx @@ -0,0 +1,52 @@ +// src/components/Bill/BillActions.jsx +import { useRef } from 'react'; +import { useReactToPrint } from 'react-to-print'; +import jsPDF from 'jspdf'; +import html2canvas from 'html2canvas'; +import { Printer, Download } from 'lucide-react'; +import toast from 'react-hot-toast'; +import { format } from 'date-fns'; + +export default function BillActions({ billRef, invoiceNumber, orderDate }) { + const handlePrint = useReactToPrint({ + content: () => billRef.current, + documentTitle: `Invoice-${invoiceNumber}`, + onAfterPrint: () => toast.success('Bill sent to printer'), + }); + + const handleDownloadPDF = async () => { + if (!billRef.current) return; + const toastId = toast.loading('Generating PDF…'); + try { + const canvas = await html2canvas(billRef.current, { scale: 2, useCORS: true, backgroundColor: '#ffffff' }); + const imgData = canvas.toDataURL('image/png'); + + // A5 size in mm: 148 x 210 + const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a5' }); + const pdfW = pdf.internal.pageSize.getWidth(); + const pdfH = (canvas.height * pdfW) / canvas.width; + + pdf.addImage(imgData, 'PNG', 0, 0, pdfW, pdfH); + + const dateStr = orderDate ? format(new Date(orderDate), 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd'); + pdf.save(`Invoice-${invoiceNumber}-${dateStr}.pdf`); + toast.success('PDF downloaded', { id: toastId }); + } catch (err) { + console.error(err); + toast.error('Failed to generate PDF', { id: toastId }); + } + }; + + return ( +
+ + +
+ ); +} diff --git a/msms/frontend/src/components/Bill/BillPreview.jsx b/msms/frontend/src/components/Bill/BillPreview.jsx new file mode 100644 index 0000000..f6227c5 --- /dev/null +++ b/msms/frontend/src/components/Bill/BillPreview.jsx @@ -0,0 +1,113 @@ +// src/components/Bill/BillPreview.jsx +import { forwardRef } from 'react'; +import { format } from 'date-fns'; + +const BillPreview = forwardRef(({ bill }, ref) => { + if (!bill) return null; + const { header, items } = bill; + const fmt = (n) => parseFloat(n || 0).toFixed(2); + + return ( +
+ {/* Header */} +
+
+ + +
+

CURE PHARMACY

+

123 Main Shahrah, Karachi, Sindh, Pakistan

+

Ph: 021-35141000 | DRAP Lic: DRAP-KHI-2022

+
+ + {/* Invoice Meta */} +
+
Invoice #: {header.invoice_number}
+
Date: {header.order_date ? format(new Date(header.order_date), 'dd-MMM-yyyy') : 'β€”'}
+
Time: {header.order_date ? format(new Date(header.order_date), 'HH:mm') : 'β€”'}
+
Cashier: {header.cashier_name}
+
Customer: {header.customer_name} | Ph: {header.customer_phone}
+ {header.prescription_number && header.prescription_number !== 'RX-000' && ( +
Prescription #: {header.prescription_number}
+ )} +
+ + {/* Items Table */} + + + + + + + + + + + + {items.map((item, i) => ( + + + + + + + + ))} + +
#MedicineQtyPriceSub
{i + 1} + {item.medicine_name} + {item.strength ? {item.strength} : null} + {item.quantity}{fmt(item.unit_price)}{fmt(item.line_subtotal)}
+ + {/* Totals */} +
+
+ Subtotal:PKR {fmt(header.subtotal)} +
+ {parseFloat(header.discount_amount) > 0 && ( +
+ Discount ({header.discount_percent}%): + -PKR {fmt(header.discount_amount)} +
+ )} + {parseFloat(header.tax_amount) > 0 && ( +
+ Tax ({header.tax_percent}%):PKR {fmt(header.tax_amount)} +
+ )} +
+ GRAND TOTAL:PKR {fmt(header.total_amount)} +
+
+ Payment: {header.payment_method || 'N/A'} + + {header.payment_status?.toUpperCase() || 'PENDING'} + +
+ {header.amount_paid && parseFloat(header.amount_paid) > parseFloat(header.total_amount) && ( +
+ Cash Given:PKR {fmt(header.amount_paid)} +
+ )} + {header.amount_paid && parseFloat(header.amount_paid) > parseFloat(header.total_amount) && ( +
+ Change:PKR {(parseFloat(header.amount_paid) - parseFloat(header.total_amount)).toFixed(2)} +
+ )} +
+ + {/* Footer */} +
+

Thank you! Get well soon. 🌿

+

Return policy: 3 days with original receipt

+

For queries: 021-35141000

+
+
+ ); +}); + +BillPreview.displayName = 'BillPreview'; +export default BillPreview; diff --git a/msms/frontend/src/components/Layout/Navbar.jsx b/msms/frontend/src/components/Layout/Navbar.jsx new file mode 100644 index 0000000..bcc73be --- /dev/null +++ b/msms/frontend/src/components/Layout/Navbar.jsx @@ -0,0 +1,66 @@ +// src/components/Layout/Navbar.jsx +import { useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Search, Bell, X } from 'lucide-react'; +import { medicinesAPI } from '../../api'; + +export default function Navbar({ title }) { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + const ref = useRef(null); + + useEffect(() => { + const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const search = async (q) => { + setQuery(q); + if (!q.trim()) { setResults([]); setOpen(false); return; } + try { + const { data } = await medicinesAPI.getAll({ search: q, limit: 6 }); + setResults(data.data || []); + setOpen(true); + } catch {} + }; + + return ( +
+

{title}

+ + {/* Global Search */} +
+ + search(e.target.value)} + placeholder="Search medicines…" + className="w-full pl-9 pr-8 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-teal-500" + /> + {query && ( + + )} + {open && results.length > 0 && ( +
+ {results.map(m => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/msms/frontend/src/components/Layout/ProtectedRoute.jsx b/msms/frontend/src/components/Layout/ProtectedRoute.jsx new file mode 100644 index 0000000..b17f4bc --- /dev/null +++ b/msms/frontend/src/components/Layout/ProtectedRoute.jsx @@ -0,0 +1,10 @@ +// src/components/Layout/ProtectedRoute.jsx +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; + +export default function ProtectedRoute({ children, roles }) { + const { user } = useAuth(); + if (!user) return ; + if (roles && !roles.includes(user.role)) return ; + return children; +} diff --git a/msms/frontend/src/components/Layout/Sidebar.jsx b/msms/frontend/src/components/Layout/Sidebar.jsx new file mode 100644 index 0000000..746a244 --- /dev/null +++ b/msms/frontend/src/components/Layout/Sidebar.jsx @@ -0,0 +1,108 @@ +// src/components/Layout/Sidebar.jsx +import { NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import { + LayoutDashboard, Pill, ShoppingCart, FileText, Users, Truck, + UserCog, CreditCard, BarChart3, LogOut, Moon, Sun, AlertTriangle +} from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { reportsAPI } from '../../api'; + +const navItems = [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, + { to: '/medicines', icon: Pill, label: 'Medicines' }, + { to: '/orders/new', icon: ShoppingCart, label: 'New Sale' }, + { to: '/orders', icon: FileText, label: 'Orders' }, + { to: '/prescriptions',icon: FileText, label: 'Prescriptions' }, + { to: '/customers', icon: Users, label: 'Customers' }, + { to: '/suppliers', icon: Truck, label: 'Suppliers' }, + { to: '/employees', icon: UserCog, label: 'Employees', roles: ['Admin','Manager'] }, + { to: '/payments', icon: CreditCard, label: 'Payments' }, + { to: '/reports', icon: BarChart3, label: 'Reports', badge: true }, +]; + +export default function Sidebar() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const [dark, setDark] = useState(() => localStorage.getItem('dark') === 'true'); + const [expiryBadge, setExpiryBadge] = useState(false); + + useEffect(() => { + document.documentElement.classList.toggle('dark', dark); + localStorage.setItem('dark', dark); + }, [dark]); + + useEffect(() => { + reportsAPI.expiringSoon().then(({ data }) => { + setExpiryBadge(data.some(m => m.days_until_expiry <= 7)); + }).catch(() => {}); + }, []); + + const handleLogout = () => { logout(); navigate('/login'); }; + + return ( + + ); +} diff --git a/msms/frontend/src/components/UI/Badge.jsx b/msms/frontend/src/components/UI/Badge.jsx new file mode 100644 index 0000000..55ba148 --- /dev/null +++ b/msms/frontend/src/components/UI/Badge.jsx @@ -0,0 +1,37 @@ +// src/components/UI/Badge.jsx +const variants = { + green: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300', + red: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', + blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', + gray: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', + teal: 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300', + purple: 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300', +}; + +export default function Badge({ variant = 'gray', children }) { + return ( + + {children} + + ); +} + +export function StockBadge({ status }) { + const map = { + 'In Stock': 'green', + 'Low Stock': 'yellow', + 'Out of Stock': 'red', + }; + return {status}; +} + +export function OrderStatusBadge({ status }) { + const map = { Completed: 'green', Pending: 'yellow', Cancelled: 'red', Refunded: 'purple' }; + return {status}; +} + +export function PaymentBadge({ method }) { + const map = { Cash: 'teal', Card: 'blue', Online: 'purple', Insurance: 'gray' }; + return {method}; +} diff --git a/msms/frontend/src/components/UI/ConfirmDialog.jsx b/msms/frontend/src/components/UI/ConfirmDialog.jsx new file mode 100644 index 0000000..85b94e1 --- /dev/null +++ b/msms/frontend/src/components/UI/ConfirmDialog.jsx @@ -0,0 +1,25 @@ +// src/components/UI/ConfirmDialog.jsx +import Modal from './Modal'; +import { AlertTriangle } from 'lucide-react'; + +export default function ConfirmDialog({ open, onClose, onConfirm, title, message, confirmLabel = 'Delete', danger = true }) { + return ( + +
+
+ +
+
+

{title}

+

{message}

+
+
+ + +
+
+
+ ); +} diff --git a/msms/frontend/src/components/UI/CrudPage.jsx b/msms/frontend/src/components/UI/CrudPage.jsx new file mode 100644 index 0000000..f0f8403 --- /dev/null +++ b/msms/frontend/src/components/UI/CrudPage.jsx @@ -0,0 +1,180 @@ +// src/components/UI/CrudPage.jsx β€” Reusable CRUD list page +import { useState, useEffect, useCallback } from 'react'; +import { Plus, Edit2, Trash2 } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import toast from 'react-hot-toast'; +import Modal from './Modal'; +import SearchBar from './SearchBar'; +import ConfirmDialog from './ConfirmDialog'; +import Pagination from './Pagination'; + +export default function CrudPage({ + title, + api, + schema, + defaultValues = {}, + columns, // [{key, label, render}] + formFields, // [{name, label, type, options, required, placeholder}] + searchPlaceholder, + getId, // item => id + softDelete = false, + onBuildPayload, // (data) => payload +}) { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [search, setSearch] = useState(''); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [deleteId, setDeleteId] = useState(null); + const LIMIT = 10; + + const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm({ + resolver: zodResolver(schema), + defaultValues, + }); + + const fetchItems = useCallback(async () => { + setLoading(true); + try { + const { data } = await api.getAll({ search, page, limit: LIMIT }); + setItems(data.data || data || []); + setTotal(data.total || 0); + } catch { toast.error(`Failed to load ${title}`); } + finally { setLoading(false); } + }, [search, page]); + + useEffect(() => { fetchItems(); }, [fetchItems]); + + const openAdd = () => { setEditing(null); reset(defaultValues); setModalOpen(true); }; + + const openEdit = (item) => { + setEditing(item); + const vals = {}; + Object.keys(defaultValues).forEach(k => { vals[k] = item[k] ?? defaultValues[k]; }); + reset(vals); + setModalOpen(true); + }; + + const onSubmit = async (data) => { + const payload = onBuildPayload ? onBuildPayload(data) : data; + try { + if (editing) { + await api.update(getId(editing), payload); + toast.success('Updated successfully'); + } else { + await api.create(payload); + toast.success('Created successfully'); + } + setModalOpen(false); + fetchItems(); + } catch (err) { + toast.error(err.response?.data?.error || err.response?.data?.errors?.[0]?.msg || 'Operation failed'); + } + }; + + const handleDelete = async () => { + try { + await api.remove(deleteId); + toast.success(softDelete ? 'Deactivated' : 'Deleted'); + fetchItems(); + } catch { toast.error('Failed'); } + }; + + return ( +
+
+ { setSearch(v); setPage(1); }} placeholder={searchPlaceholder} /> + +
+ +
+
+ + + + {columns.map(c => )} + + + + + {loading ? ( + + ) : items.length === 0 ? ( + + ) : items.map(item => ( + + {columns.map(c => ( + + ))} + + + ))} + +
{c.label}
+
+
No records found
+ {c.render ? c.render(item) : (item[c.key] ?? 'β€”')} + +
+ + +
+
+
+ +
+ + {/* Add/Edit Modal */} + setModalOpen(false)} title={editing ? `Edit ${title}` : `Add ${title}`} size="md"> +
+
+ {formFields.map(f => ( +
+ + {f.type === 'select' ? ( + + ) : f.type === 'textarea' ? ( +