อธิบายการแก้ไขล่าสุด เช่น เพิ่มฟีเจอร์ v2.0 สำหรับ Frontend
This commit is contained in:
parent
bf7692fc5f
commit
9cca7dcaa6
136
README.md
136
README.md
@ -1,69 +1,135 @@
|
|||||||
# 🏥 ระบบบริหารจัดการโรงพยาบาล - ฝั่งผู้ใช้งาน (Frontend)
|
# 🏥 ระบบบริหารจัดการโรงพยาบาล - ฝั่งผู้ใช้งาน (Frontend)
|
||||||
|
|
||||||
โปรเจคระบบบริหารจัดการโรงพยาบาล (Hospital Management System)
|
โปรเจคนี้เป็น **ส่วน Frontend ของระบบบริหารจัดการโรงพยาบาล (Hospital Management System)**
|
||||||
ส่วน Frontend พัฒนาโดยใช้ **React (Vite)** สำหรับให้บริการทั้งผู้ใช้งานทั่วไป (เช่น ผู้ป่วย) และผู้ดูแลระบบ (Admin)
|
Frontend พัฒนาโดยใช้ **React (Vite)** ทำงานร่วมกับ **Backend API v2.0** ที่เก็บอยู่ที่:
|
||||||
|
🌐 [Hospital Management API - Gitea](https://gitea.softwarecraft.tech/gitea/hospital-management-api)
|
||||||
|
|
||||||
|
ส่วน Frontend ให้บริการทั้งผู้ใช้งานทั่วไป (ผู้ป่วย) และผู้ดูแลระบบ (Admin) โดยเชื่อมต่อกับ API สำหรับฟีเจอร์ต่าง ๆ ตามเวอร์ชัน 2.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 ฟีเจอร์ที่มีให้ใช้งาน
|
## 🚀 ฟีเจอร์หลักที่รองรับ (v2.0)
|
||||||
|
|
||||||
### 👥 ส่วนของผู้ใช้งานทั่วไป (ผู้ป่วย)
|
### 👨⚕️ การจัดการข้อมูลหลัก (Core Data Management)
|
||||||
|
|
||||||
- 📝 **จองนัดหมายแพทย์ (Book Appointment):**
|
**แพทย์ (Doctor)**
|
||||||
ใช้สำหรับการจองคิวนัดพบแพทย์ได้อย่างสะดวก
|
- R1 - ลงทะเบียนแพทย์
|
||||||
|
- R2 - รายชื่อแพทย์
|
||||||
|
- R3 - แก้ไขข้อมูลแพทย์
|
||||||
|
- R4 - ลบข้อมูลแพทย์
|
||||||
|
|
||||||
- 📩 **ส่งข้อความถึงแอดมิน (Send Message):**
|
**ผู้ป่วย (Patient)**
|
||||||
ผู้ใช้งานสามารถติดต่อสอบถามข้อมูลกับแอดมินผ่านแบบฟอร์ม
|
- R5 - ลงทะเบียนผู้ป่วย
|
||||||
|
- R6 - รายชื่อผู้ป่วย
|
||||||
|
- R7 - แก้ไขข้อมูลผู้ป่วย
|
||||||
|
- R8 - ลบข้อมูลผู้ป่วย
|
||||||
|
|
||||||
- 🔐 **เข้าสู่ระบบ / สมัครสมาชิก (Login / Register):**
|
**พนักงาน / นางพยาบาล (Staff / Nurse)**
|
||||||
ระบบล็อกอินและลงทะเบียนใช้งานด้วยความปลอดภัย
|
- R9 - เพิ่มพนักงาน
|
||||||
|
- R10 - ลงทะเบียนนางพยาบาล
|
||||||
|
- R11 - รายชื่อนางพยาบาล
|
||||||
|
- R12 - แก้ไขข้อมูลนางพยาบาล
|
||||||
|
- R13 - ลบข้อมูลนางพยาบาล
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🛠️ ส่วนแดชบอร์ดสำหรับแอดมิน (Admin Dashboard)
|
### 🩺 การจัดการเฉพาะทาง (Specialized Management)
|
||||||
|
|
||||||
แดชบอร์ดนี้ออกแบบมาเพื่อใช้บริหารจัดการระบบโดยแอดมิน
|
**การนัดหมาย (Consultation)**
|
||||||
|
- R14 - จัดตารางการนัดหมาย
|
||||||
|
- R15 - ยกเลิกนัดหมาย
|
||||||
|
- R16 - ดูรายละเอียดการนัดหมายแต่ละรายการ
|
||||||
|
|
||||||
- 📊 **ภาพรวมระบบ (Dashboard Overview):**
|
**ตารางแพทย์ / นางพยาบาล (Doctor / Nurse Schedule)**
|
||||||
แสดงสถิติ เช่น จำนวนแพทย์ จำนวนผู้ป่วย การนัดหมาย ฯลฯ
|
- R17 - เพิ่มตารางแพทย์
|
||||||
|
- R18 - เพิ่มตารางนางพยาบาล
|
||||||
|
- R19 - ดูตารางนางพยาบาล (Pagination)
|
||||||
|
|
||||||
- ➕ **เพิ่มผู้ใช้งาน (Add Admin / Doctor / Receptionist):**
|
**ห้องผ่าตัด (Operating Room)**
|
||||||
เพิ่มผู้ใช้งานตามบทบาทในระบบได้อย่างง่ายดาย
|
- R20 - เพิ่ม / แก้ไข / ลบ ห้องผ่าตัด
|
||||||
|
- R21 - ดูรายชื่อห้องผ่าตัด
|
||||||
|
- R22 - จัดตารางใช้งานห้องผ่าตัด
|
||||||
|
|
||||||
- 👨⚕️ **ดูรายชื่อแพทย์ (See all doctors):**
|
**เวชระเบียน / ใบสั่งยา / ผลวินิจฉัย (Medical Record / Prescription / Lab Result / Medical Image)**
|
||||||
ตรวจสอบรายชื่อและรายละเอียดของแพทย์ที่ลงทะเบียนแล้ว
|
- R23 - สร้างเวชระเบียน
|
||||||
|
- R24 - แก้ไขเวชระเบียน
|
||||||
|
- R25 - ลบเวชระเบียน
|
||||||
|
- R26 - ดูเวชระเบียนตาม ID
|
||||||
|
- R27 - สร้างใบสั่งยาใหม่
|
||||||
|
- R28 - อัปโหลด / ดาวน์โหลดภาพทางการแพทย์
|
||||||
|
- R29 - สร้าง / ดูผล Lab ตามเวชระเบียน
|
||||||
|
|
||||||
- 📅 **จัดการนัดหมาย (Manage Appointments):**
|
**การจัดการคลังสินค้า (Inventory Management)**
|
||||||
ตรวจสอบ อนุมัติ แก้ไข หรือยกเลิกการนัดหมาย
|
- R30-R33 - จัดการ Inventory Item
|
||||||
|
- R34 - สร้าง Inventory Transaction
|
||||||
|
- R35 - จัดการ Supplier
|
||||||
|
- R36 - จัดการ Item Type
|
||||||
|
|
||||||
|
**การประกัน / การเรียกร้อง (Insurance Management)**
|
||||||
|
- R37-R38 - จัดการ Insurance Provider
|
||||||
|
- R39-R40 - จัดการ Insurance Claim
|
||||||
|
|
||||||
|
**การเรียกเก็บเงิน / การชำระเงิน (Billing & Payment)**
|
||||||
|
- R41-R45 - จัดการ Billing / Payment
|
||||||
|
|
||||||
|
**รายงาน (Reports)**
|
||||||
|
- R46 - รายงานสินค้าคงเหลือต่ำ
|
||||||
|
- R47 - รายงานสรุปการเงิน
|
||||||
|
- R48 - รายงานการนัดหมาย
|
||||||
|
|
||||||
|
**การจัดการผู้ใช้งาน (User Management)**
|
||||||
|
- R49-R56 - อัปเดตบัญชีผู้ใช้ / ตั้งค่าบทบาท / เชื่อมต่อผู้ป่วยและผู้ใช้งานอื่น ๆ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Default Credentials
|
||||||
|
|
||||||
|
สำหรับเข้าระบบครั้งแรก (Admin)
|
||||||
|
- Username: `admin@softwarecraft.tech`
|
||||||
|
- Password: `pasword123`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 API Endpoints
|
||||||
|
|
||||||
|
Frontend ใช้ API v2.0 สำหรับฟีเจอร์ต่าง ๆ เช่น:
|
||||||
|
|
||||||
|
**Authentication**
|
||||||
|
- POST `/api/auth/register-patient` - ลงทะเบียนผู้ป่วย
|
||||||
|
- POST `/api/auth/register-doctor-and-link` - ลงทะเบียนแพทย์และเชื่อมบัญชี
|
||||||
|
- POST `/api/auth/register-staff` - เพิ่มพนักงาน
|
||||||
|
- POST `/api/auth/login` - เข้าสู่ระบบ
|
||||||
|
- POST `/api/auth/link-patient-to-user` - เชื่อมผู้ป่วยกับบัญชี
|
||||||
|
|
||||||
|
**User Management**
|
||||||
|
- PUT `/api/v1.0/users/{id}/username` - อัปเดต username
|
||||||
|
- PUT `/api/v1.0/users/{id}/role` - อัปเดต role
|
||||||
|
- PUT `/api/v1.0/users/{id}/password` - อัปเดต password
|
||||||
|
- PATCH `/api/v1.0/users/{id}/deactivate` - ปิดใช้งานบัญชี
|
||||||
|
- PATCH `/api/v1.0/users/{id}/activate` - เปิดใช้งานบัญชี
|
||||||
|
|
||||||
|
**Doctor / Patient / Staff / Consultation / Medical Record / Prescription / Payment / Inventory**
|
||||||
|
- รองรับ CRUD ตามฟีเจอร์ v2.0 (ดูรายละเอียด API ด้านบน)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧰 เทคโนโลยีที่ใช้
|
## 🧰 เทคโนโลยีที่ใช้
|
||||||
|
|
||||||
- **Frontend Framework:** React (Vite)
|
- **Frontend Framework:** React (Vite)
|
||||||
- **การจัดการสถานะ (State):** React Hooks
|
|
||||||
- **UI Library:** Tailwind CSS / Daisy UI
|
- **UI Library:** Tailwind CSS / Daisy UI
|
||||||
- **Routing:** React Router
|
- **Routing:** React Router
|
||||||
- **การเชื่อมต่อ API:** Fetch (เชื่อมต่อกับ Backend)
|
- **State Management:** React Hooks
|
||||||
- **ระบบยืนยันตัวตน:** JWT (JSON Web Token)
|
- **API Integration:** Fetch / Axios (เชื่อมต่อกับ [Hospital Management API](https://gitea.softwarecraft.tech/gitea/hospital-management-api))
|
||||||
|
- **Authentication:** JWT
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 การพัฒนาแบบ CI/CD
|
## 🧪 การรันในเครื่องนักพัฒนา (Dev Environment)
|
||||||
|
|
||||||
โปรเจคนี้จะถูกพัฒนาโดยใช้แนวทาง **CI/CD** เพื่อให้สามารถ Build และ Deploy ได้อัตโนมัติผ่านระบบ Drone CI
|
|
||||||
|
|
||||||
สามารถเข้าใช้งานระบบเพื่อทดสอบฟีเจอร์ได้ที่:
|
|
||||||
🌐 [https://frontend-sandbox.softwarecraft.tech/](https://frontend-sandbox.softwarecraft.tech/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 วิธีใช้งานในเครื่องนักพัฒนา (Dev Environment)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ติดตั้งแพ็กเกจที่จำเป็น
|
# ติดตั้ง dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# รันโปรเจคบนโหมดพัฒนา
|
# รัน Frontend บนโหมดพัฒนา
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
274
package-lock.json
generated
274
package-lock.json
generated
@ -8,13 +8,17 @@
|
|||||||
"name": "healthcare_app",
|
"name": "healthcare_app",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@tanstack/react-query": "^5.74.4",
|
"@tanstack/react-query": "^5.74.4",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"daisyui": "^5.0.20",
|
"daisyui": "^5.0.20",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.1",
|
||||||
"tailwindcss": "^4.1.4"
|
"tailwindcss": "^4.1.4"
|
||||||
},
|
},
|
||||||
@ -769,9 +773,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -784,9 +788,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers": {
|
"node_modules/@eslint/config-helpers": {
|
||||||
"version": "0.2.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
|
||||||
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
|
"integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -794,9 +798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.12.0",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
|
||||||
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
|
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -844,13 +848,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.24.0",
|
"version": "9.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
|
||||||
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
|
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://eslint.org/donate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/object-schema": {
|
"node_modules/@eslint/object-schema": {
|
||||||
@ -864,32 +871,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.2.8",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.13.0",
|
"@eslint/core": "^0.15.2",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@ -1009,6 +1003,32 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.40.0",
|
"version": "4.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
|
||||||
@ -1269,6 +1289,18 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
|
||||||
@ -1611,7 +1643,7 @@
|
|||||||
"version": "19.1.2",
|
"version": "19.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
||||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -1627,6 +1659,12 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz",
|
||||||
@ -1648,9 +1686,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1735,9 +1773,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1913,7 +1951,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
@ -2111,20 +2149,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.24.0",
|
"version": "9.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz",
|
||||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.20.0",
|
"@eslint/config-array": "^0.21.0",
|
||||||
"@eslint/config-helpers": "^0.2.0",
|
"@eslint/config-helpers": "^0.3.1",
|
||||||
"@eslint/core": "^0.12.0",
|
"@eslint/core": "^0.15.2",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.24.0",
|
"@eslint/js": "9.34.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.5",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@ -2135,9 +2173,9 @@
|
|||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"eslint-scope": "^8.3.0",
|
"eslint-scope": "^8.4.0",
|
||||||
"eslint-visitor-keys": "^4.2.0",
|
"eslint-visitor-keys": "^4.2.1",
|
||||||
"espree": "^10.3.0",
|
"espree": "^10.4.0",
|
||||||
"esquery": "^1.5.0",
|
"esquery": "^1.5.0",
|
||||||
"esutils": "^2.0.2",
|
"esutils": "^2.0.2",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
@ -2195,9 +2233,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "8.3.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2212,9 +2250,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-visitor-keys": {
|
"node_modules/eslint-visitor-keys": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2225,15 +2263,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.3.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.15.0",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -2395,14 +2433,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2582,6 +2621,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@ -2652,7 +2701,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@ -2990,6 +3038,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@ -3082,6 +3142,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -3221,6 +3290,17 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@ -3274,6 +3354,44 @@
|
|||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@ -3322,6 +3440,27 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@ -3536,6 +3675,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
|
|||||||
@ -10,13 +10,17 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@tanstack/react-query": "^5.74.4",
|
"@tanstack/react-query": "^5.74.4",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"daisyui": "^5.0.20",
|
"daisyui": "^5.0.20",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.1",
|
||||||
"tailwindcss": "^4.1.4"
|
"tailwindcss": "^4.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
32
src/App.jsx
32
src/App.jsx
@ -1,15 +1,29 @@
|
|||||||
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
// src/App.jsx
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
import PublicRoutes from "./routes/PublicRoutes.jsx";
|
import PublicRoutes from "./routes/PublicRoutes.jsx";
|
||||||
|
import AdminRoutes from "./routes/AdminRoutes.jsx";
|
||||||
|
import ProtectedRoute from "./components/ProtectedRoute.jsx"; // Import ProtectedRoute
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<Routes>
|
||||||
<Routes>
|
{/* เส้นทางสาธารณะที่ทุกคนเข้าถึงได้ */}
|
||||||
<Route path="/*" element={<PublicRoutes/>}/>
|
<Route path="/*" element={<PublicRoutes />} />
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
{/*
|
||||||
)
|
เส้นทางสำหรับผู้ดูแลระบบที่ต้องมีการล็อกอินเท่านั้น
|
||||||
|
ProtectedRoute จะทำหน้าที่ตรวจสอบสิทธิ์ก่อนที่จะแสดง AdminRoutes
|
||||||
|
*/}
|
||||||
|
<Route
|
||||||
|
path="/admin/*"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AdminRoutes />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
52
src/api/axios.js
Normal file
52
src/api/axios.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// 1. สร้าง Axios Instance
|
||||||
|
// กำหนด URL พื้นฐานของ API และตั้งค่าเริ่มต้นอื่น ๆ
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:8080/api/v1.0',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. สร้าง Request Interceptor
|
||||||
|
// Interceptor นี้จะทำงานก่อนที่ทุก ๆ Request จะถูกส่งออกไป
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// ดึง JWT Token จาก Local Storage
|
||||||
|
const token = localStorage.getItem('jwtToken');
|
||||||
|
|
||||||
|
// ถ้ามี Token อยู่ ให้แนบ Token เข้าไปใน Header ของ Request
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// จัดการข้อผิดพลาดที่อาจเกิดขึ้นก่อนการส่ง Request
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. สร้าง Response Interceptor (Optional แต่มีประโยชน์)
|
||||||
|
// Interceptor นี้จะทำงานเมื่อได้รับ Response จาก Server
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// ถ้า Response บอกว่า Unauthorized (401)
|
||||||
|
// ให้ทำการลบ Token และเปลี่ยนเส้นทางกลับไปหน้า Login
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
console.log("Unauthorized, logging out...");
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
// window.location.href = '/login'; // วิธีนี้จะรีโหลดหน้าเว็บใหม่
|
||||||
|
|
||||||
|
// การใช้ Redux dispatch จะเป็นวิธีที่ดีกว่า
|
||||||
|
// แต่ต้องจัดการใน Component ที่เหมาะสม เช่น ใน App.jsx
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
9
src/app/store.js
Normal file
9
src/app/store.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import authReducer from '../features/auth/authSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
auth: authReducer,
|
||||||
|
// เพิ่ม Reducer อื่น ๆ ที่นี่ในอนาคต
|
||||||
|
},
|
||||||
|
});
|
||||||
36
src/components/About.jsx
Normal file
36
src/components/About.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// src/components/About.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function About({ title, description, image }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto flex flex-col-reverse lg:flex-row-reverse items-center justify-between gap-12">
|
||||||
|
{/* Text Section */}
|
||||||
|
<div className="w-full lg:w-1/2 text-center lg:text-left">
|
||||||
|
<h2 className="text-4xl font-extrabold text-gray-900 sm:text-5xl mb-4">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto lg:mx-0 leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Image Section */}
|
||||||
|
<div className="w-full lg:w-1/2">
|
||||||
|
<img
|
||||||
|
className="w-full h-auto rounded-xl shadow-2xl"
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
About.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
image: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
36
src/components/CTA.jsx
Normal file
36
src/components/CTA.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// src/components/CTA.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function CTA({ title, subtitle, linkText, linkTo }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-indigo-600">
|
||||||
|
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:flex lg:items-center lg:justify-between lg:py-16 lg:px-8">
|
||||||
|
{/* Text Section */}
|
||||||
|
<h2 className="text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
|
||||||
|
<span className="block">{title}</span>
|
||||||
|
<span className="block text-indigo-200">{subtitle}</span>
|
||||||
|
</h2>
|
||||||
|
{/* CTA Button */}
|
||||||
|
<div className="mt-8 flex lg:mt-0 lg:flex-shrink-0">
|
||||||
|
<div className="inline-flex rounded-md shadow">
|
||||||
|
<Link
|
||||||
|
to={linkTo}
|
||||||
|
className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-700 hover:bg-indigo-800"
|
||||||
|
>
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CTA.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
subtitle: PropTypes.string.isRequired,
|
||||||
|
linkText: PropTypes.string.isRequired,
|
||||||
|
linkTo: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
31
src/components/Feature.jsx
Normal file
31
src/components/Feature.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// src/components/Feature.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function Feature({ title, description, icon }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md text-center">
|
||||||
|
{/* SVG Icon */}
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 text-indigo-600 bg-indigo-100 rounded-full">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={icon} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Description */}
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
|
||||||
|
<p className="text-gray-600">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Feature.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
icon: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
44
src/components/Hero.jsx
Normal file
44
src/components/Hero.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// src/components/Hero.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function Hero({ title, description, image, primaryLink, secondaryLink }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto flex flex-col-reverse lg:flex-row items-center">
|
||||||
|
<div className="lg:w-1/2 mt-10 lg:mt-0 text-center lg:text-left">
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-gray-900">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-lg text-gray-500 max-w-2xl mx-auto lg:mx-0">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex justify-center lg:justify-start">
|
||||||
|
{primaryLink && (
|
||||||
|
<Link {...primaryLink} className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
|
||||||
|
{primaryLink.text}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{secondaryLink && (
|
||||||
|
<Link {...secondaryLink} className="ml-4 inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200">
|
||||||
|
{secondaryLink.text}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:w-1/2">
|
||||||
|
<img className="w-full h-auto" src={image} alt="Hero" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Hero.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
image: PropTypes.string,
|
||||||
|
primaryLink: PropTypes.object,
|
||||||
|
secondaryLink: PropTypes.object,
|
||||||
|
};
|
||||||
22
src/components/ProtectedRoute.jsx
Normal file
22
src/components/ProtectedRoute.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// src/routes/ProtectedRoute.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function ProtectedRoute({ children }) {
|
||||||
|
// ดึงสถานะการล็อกอินจาก Redux Store
|
||||||
|
const { isAuthenticated } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
|
// ถ้ายังไม่ได้ล็อกอิน ให้ Redirect ไปที่หน้า Login
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ถ้าล็อกอินแล้ว ให้แสดง Component ที่ถูกครอบไว้
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtectedRoute.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
81
src/features/auth/authSlice.js
Normal file
81
src/features/auth/authSlice.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import api from '../../api/axios'; // ใช้ axios instance เดิม
|
||||||
|
|
||||||
|
// Thunk สำหรับการ Login (จัดการ API Call)
|
||||||
|
export const loginUser = createAsyncThunk(
|
||||||
|
'auth/loginUser',
|
||||||
|
async (credentials, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/auth/login', credentials);
|
||||||
|
localStorage.setItem('jwtToken', response.data.token);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Thunk สำหรับการ Logout
|
||||||
|
export const logoutUser = createAsyncThunk(
|
||||||
|
'auth/logoutUser',
|
||||||
|
async () => {
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
// ถ้ามี API สำหรับ Logout ก็สามารถเรียกได้ที่นี่
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
user: null,
|
||||||
|
isLoggedIn: false,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// สร้าง Slice
|
||||||
|
const authSlice = createSlice({
|
||||||
|
name: 'auth',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
// Reducer ที่ไม่มี async logic
|
||||||
|
// เช่น การตั้งค่าผู้ใช้จาก Token ที่มีอยู่แล้ว
|
||||||
|
setUserFromToken: (state) => {
|
||||||
|
const token = localStorage.getItem('jwtToken');
|
||||||
|
if (token) {
|
||||||
|
state.user = { token };
|
||||||
|
state.isLoggedIn = true;
|
||||||
|
} else {
|
||||||
|
state.user = null;
|
||||||
|
state.isLoggedIn = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
// กรณี Login สำเร็จ
|
||||||
|
.addCase(loginUser.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(loginUser.fulfilled, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.user = action.payload;
|
||||||
|
state.isLoggedIn = true;
|
||||||
|
})
|
||||||
|
.addCase(loginUser.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload;
|
||||||
|
state.isLoggedIn = false;
|
||||||
|
})
|
||||||
|
// กรณี Logout สำเร็จ
|
||||||
|
.addCase(logoutUser.fulfilled, (state) => {
|
||||||
|
state.user = null;
|
||||||
|
state.isLoggedIn = false;
|
||||||
|
state.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setUserFromToken } = authSlice.actions;
|
||||||
|
|
||||||
|
export default authSlice.reducer;
|
||||||
80
src/layouts/AdminLayout.jsx
Normal file
80
src/layouts/AdminLayout.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
|
import { FaBars, FaTimes, FaTachometerAlt, FaUserMd, FaUsers, FaCalendarCheck, FaSignOutAlt, FaUserCog } from 'react-icons/fa';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { logoutUser } from '../features/auth/authSlice';
|
||||||
|
|
||||||
|
export default function AdminLayout() {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
dispatch(logoutUser());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-gray-100 font-sans">
|
||||||
|
{/* Overlay for Mobile */}
|
||||||
|
{isSidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside
|
||||||
|
className={`fixed inset-y-0 left-0 transform ${
|
||||||
|
isSidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
} lg:relative lg:translate-x-0 w-64 bg-white transition-transform duration-300 ease-in-out z-50 shadow-lg flex flex-col`}
|
||||||
|
>
|
||||||
|
<div className="p-6 text-center border-b border-gray-200">
|
||||||
|
<Link to="/admin/dashboard" className="text-2xl font-extrabold text-indigo-600 tracking-tight">
|
||||||
|
Admin Panel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 px-4 py-6 space-y-2">
|
||||||
|
<NavLink to="/admin/dashboard" icon={FaTachometerAlt} label="Dashboard" />
|
||||||
|
<NavLink to="/admin/doctors" icon={FaUserMd} label="จัดการแพทย์" />
|
||||||
|
<NavLink to="/admin/patients" icon={FaUsers} label="จัดการผู้ป่วย" />
|
||||||
|
<NavLink to="/admin/consultations" icon={FaCalendarCheck} label="จัดการนัดหมาย" />
|
||||||
|
<NavLink to="/admin/users" icon={FaUserCog} label="จัดการผู้ใช้งาน" />
|
||||||
|
</nav>
|
||||||
|
<div className="p-4 border-t border-gray-200">
|
||||||
|
<button onClick={handleLogout} className="w-full flex items-center p-3 text-sm font-medium text-red-600 rounded-lg hover:bg-gray-100 transition-colors duration-200">
|
||||||
|
<FaSignOutAlt className="w-5 h-5 mr-3" />
|
||||||
|
ออกจากระบบ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Mobile Header */}
|
||||||
|
<header className="flex items-center justify-between p-4 bg-white shadow-md lg:hidden sticky top-0 z-30">
|
||||||
|
<h1 className="text-xl font-bold text-indigo-600">Admin Dashboard</h1>
|
||||||
|
<button onClick={toggleSidebar} className="p-2 text-gray-600">
|
||||||
|
{isSidebarOpen ? <FaTimes className="w-6 h-6" /> : <FaBars className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Page Content */}
|
||||||
|
<main className="flex-1 p-6 lg:p-10 overflow-y-auto">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reusable NavLink Component
|
||||||
|
const NavLink = ({ to, icon: Icon, label }) => (
|
||||||
|
<Link to={to} className="flex items-center p-3 text-sm font-medium text-gray-700 rounded-lg hover:bg-indigo-100 hover:text-indigo-800 transition-colors duration-200">
|
||||||
|
<Icon className="w-5 h-5 mr-3 text-indigo-500" />
|
||||||
|
<span>{label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
@ -1,20 +1,31 @@
|
|||||||
import {Outlet} from "react-router-dom";
|
// src/layouts/AuthLayout.jsx
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default function AuthLayout(){
|
export default function AuthLayout({ description }) {
|
||||||
return(
|
return (
|
||||||
<>
|
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||||
<div className="flex h-screen items-center justify-center bg-base-200">
|
<div className="w-full max-w-5xl overflow-hidden rounded-xl bg-white shadow-2xl flex flex-col lg:flex-row">
|
||||||
<div className="w-full max-w-5xl shadow-xl rounded-xl bg-white p-6 flex flex-col lg:flex-row">
|
{/* Section ด้านซ้ายสำหรับแสดงข้อมูล */}
|
||||||
<div className="flex-1 hidden lg:flex flex-col justify-center items-center p-6">
|
<div className="flex-1 hidden lg:flex flex-col justify-center items-center p-8 bg-primary-50 text-center">
|
||||||
<h2 className="text-3xl font-bold mb-4 text-primary">My App</h2>
|
<h2 className="text-4xl font-extrabold mb-4 text-primary">
|
||||||
<p className="text-gray-500">เข้าสู่ระบบเพื่อเข้าถึงแดชบอร์ด จัดการข้อมูล และสำรวจฟีเจอร์ทั้งหมดที่เราพร้อมมอบให้คุณ ปลอดภัย รวดเร็ว และง่ายต่อการใช้งาน
|
Hospital Management System
|
||||||
|
</h2>
|
||||||
|
{description && (
|
||||||
|
<p className="text-gray-600 text-lg">
|
||||||
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
<div className="flex-1">
|
</div>
|
||||||
<Outlet/>
|
{/* Section ด้านขวาสำหรับแสดงฟอร์ม */}
|
||||||
</div>
|
<div className="flex-1 p-8">
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AuthLayout.propTypes = {
|
||||||
|
description: PropTypes.string,
|
||||||
|
};
|
||||||
@ -1,31 +1,58 @@
|
|||||||
import {Link, Outlet} from "react-router-dom";
|
import { Link, Outlet } from "react-router-dom";
|
||||||
|
import { FaHeartbeat } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export default function PublicLayout() {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
export default function PublicLayout(){
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col min-h-screen bg-gray-50 text-gray-800">
|
||||||
{/* แสดงทั้งหน้าจอ ใช้ Flex จัดเรียงแนวตั้ง*/}
|
{/* Navbar */}
|
||||||
<div className="min-h-screen flex flex-col bg-base-200 ">
|
<nav className="sticky top-0 z-50 bg-white shadow-sm">
|
||||||
{/* Navbar */}
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="navbar bg-base-100 shadow-md">
|
<div className="flex items-center justify-between h-16">
|
||||||
<div className="flex-1">
|
{/* Logo and App Name */}
|
||||||
<Link to="/" className="btn btn-ghost text-xl text-primary">MyAPP</Link>
|
<div className="flex items-center">
|
||||||
</div>
|
<Link to="/" className="flex items-center space-x-2 text-xl font-bold text-indigo-600 hover:text-indigo-700 transition-colors duration-200">
|
||||||
<div>
|
<FaHeartbeat className="h-6 w-6" />
|
||||||
<Link to="/about" className="btn btn-ghost">เกี่ยวกับเรา</Link>
|
<span>Hospital Management</span>
|
||||||
<Link to="/login" className="btn btn-outline btn-primary">เข้าสู่ระบบ</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<div className="ml-10 flex items-center space-x-4">
|
||||||
|
|
||||||
|
{/* Adjusted Login Button to match Hero.jsx */}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
เข้าสู่ระบบ
|
||||||
|
</Link>
|
||||||
|
{/* Adjusted Register Button to match Hero.jsx */}
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="ml-4 px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
ลงทะเบียน
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Content */}
|
</nav>
|
||||||
<main className="flex-1 container mx-auto px-4 py-6">
|
|
||||||
<Outlet/>
|
{/* Main Content */}
|
||||||
</main>
|
<main className="flex-1 container mx-auto px-4 py-8">
|
||||||
{/* Footer */}
|
<Outlet />
|
||||||
<footer className="footer footer-center p-4 bg-base-100 text-base-content border-t">
|
</main>
|
||||||
<aside>
|
|
||||||
<p>© {new Date().getFullYear()} MyApp. All rights reserved.</p>
|
{/* Footer */}
|
||||||
</aside>
|
<footer className="bg-gray-800 text-gray-300 py-6 text-center">
|
||||||
</footer>
|
<div className="container mx-auto px-4">
|
||||||
</div>
|
<p>© {currentYear} Hospital Management System. All rights reserved.</p>
|
||||||
</>
|
</div>
|
||||||
)
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
40
src/main.jsx
40
src/main.jsx
@ -1,11 +1,33 @@
|
|||||||
import { StrictMode } from 'react'
|
// src/main.jsx
|
||||||
import { createRoot } from 'react-dom/client'
|
import React from 'react';
|
||||||
import './index.css'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.jsx'
|
import App from './App.jsx';
|
||||||
|
import './index.css';
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
// Import BrowserRouter มาครอบ App Component
|
||||||
<StrictMode>
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
// Import Redux Toolkit และ TanStack Query Provider
|
||||||
)
|
import { Provider } from 'react-redux';
|
||||||
|
import { store } from './app/store.js';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// สร้าง Query Client
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
{/*
|
||||||
|
<BrowserRouter> ควรอยู่ระดับบนสุดของ Component Tree
|
||||||
|
เพื่อให้ทุก Component ที่อยู่ข้างในใช้งานได้
|
||||||
|
*/}
|
||||||
|
<BrowserRouter>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</Provider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
176
src/pages/ConsultationsPage.jsx
Normal file
176
src/pages/ConsultationsPage.jsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import api from '../api/axios';
|
||||||
|
import { FaPlus, FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับดึงข้อมูลที่เกี่ยวข้อง
|
||||||
|
const fetchConsultations = async () => {
|
||||||
|
const { data } = await api.get('/consultations');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDoctors = async () => {
|
||||||
|
const { data } = await api.get('/doctors');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPatients = async () => {
|
||||||
|
const { data } = await api.get('/patients');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับสร้างนัดหมายใหม่
|
||||||
|
const createConsultation = async (newConsultation) => {
|
||||||
|
const { data } = await api.post('/consultations', newConsultation);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับยกเลิกนัดหมาย
|
||||||
|
const deleteConsultation = async (id) => {
|
||||||
|
await api.delete(`/consultations/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ConsultationsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// ดึงข้อมูลการนัดหมายทั้งหมด
|
||||||
|
const { data: consultations, isLoading, isError, error } = useQuery({
|
||||||
|
queryKey: ['consultations'],
|
||||||
|
queryFn: fetchConsultations,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ดึงข้อมูลแพทย์และผู้ป่วยสำหรับ dropdown ในฟอร์ม
|
||||||
|
const { data: doctors } = useQuery({ queryKey: ['doctors'], queryFn: fetchDoctors });
|
||||||
|
const { data: patients } = useQuery({ queryKey: ['patients'], queryFn: fetchPatients });
|
||||||
|
|
||||||
|
const { register, handleSubmit, reset } = useForm();
|
||||||
|
|
||||||
|
const createConsultationMutation = useMutation({
|
||||||
|
mutationFn: createConsultation,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['consultations']);
|
||||||
|
reset();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteConsultationMutation = useMutation({
|
||||||
|
mutationFn: deleteConsultation,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['consultations']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
// จัดการข้อมูลให้ตรงกับ API Endpoint
|
||||||
|
const consultationData = {
|
||||||
|
...data,
|
||||||
|
doctorId: parseInt(data.doctorId),
|
||||||
|
patientId: parseInt(data.patientId),
|
||||||
|
};
|
||||||
|
createConsultationMutation.mutate(consultationData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('คุณต้องการยกเลิกนัดหมายนี้หรือไม่?')) {
|
||||||
|
deleteConsultationMutation.mutate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>กำลังโหลดข้อมูล...</div>;
|
||||||
|
if (isError) return <div>เกิดข้อผิดพลาด: {error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">จัดการนัดหมาย</h1>
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<FaPlus className="mr-2" /> สร้างนัดหมายใหม่
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto shadow-xl rounded-lg">
|
||||||
|
<table className="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th>แพทย์</th>
|
||||||
|
<th>ผู้ป่วย</th>
|
||||||
|
<th>วันที่</th>
|
||||||
|
<th>เวลา</th>
|
||||||
|
<th>การดำเนินการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{consultations.map((consultation) => (
|
||||||
|
<tr key={consultation.id}>
|
||||||
|
<td>{consultation.doctor.name}</td>
|
||||||
|
<td>{consultation.patient.name}</td>
|
||||||
|
<td>{consultation.date}</td>
|
||||||
|
<td>{consultation.time}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
|
onClick={() => handleDelete(consultation.id)}
|
||||||
|
disabled={deleteConsultationMutation.isLoading}
|
||||||
|
>
|
||||||
|
{deleteConsultationMutation.isLoading ? 'กำลังลบ...' : <FaTrash />}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg mb-4">สร้างนัดหมายใหม่</h3>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">แพทย์</label>
|
||||||
|
<select className="select select-bordered" {...register("doctorId", { required: true })}>
|
||||||
|
<option value="">เลือกแพทย์</option>
|
||||||
|
{doctors?.map(doc => (
|
||||||
|
<option key={doc.id} value={doc.id}>{doc.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">ผู้ป่วย</label>
|
||||||
|
<select className="select select-bordered" {...register("patientId", { required: true })}>
|
||||||
|
<option value="">เลือกผู้ป่วย</option>
|
||||||
|
{patients?.map(pat => (
|
||||||
|
<option key={pat.id} value={pat.id}>{pat.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">วันที่</label>
|
||||||
|
<input type="date" className="input input-bordered" {...register("date", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">เวลา</label>
|
||||||
|
<input type="time" className="input input-bordered" {...register("time", { required: true })} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-action mt-4">
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={createConsultationMutation.isLoading}>
|
||||||
|
{createConsultationMutation.isLoading ? 'กำลังสร้าง...' : 'สร้างนัดหมาย'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn" onClick={() => setIsModalOpen(false)}>
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/pages/DashboardPage.jsx
Normal file
88
src/pages/DashboardPage.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from '../api/axios';
|
||||||
|
import { FaUserMd, FaUsers, FaCalendarAlt } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเรียก API แต่ละตัว
|
||||||
|
const fetchDoctorsCount = async () => {
|
||||||
|
const { data } = await api.get('/doctors');
|
||||||
|
return data.length; // นับจำนวนแพทย์
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPatientsCount = async () => {
|
||||||
|
const { data } = await api.get('/patients');
|
||||||
|
return data.length; // นับจำนวนผู้ป่วย
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchConsultations = async () => {
|
||||||
|
const { data } = await api.get('/consultations');
|
||||||
|
// คุณอาจต้องกรองข้อมูลเพื่อหาจำนวนนัดหมายที่กำลังจะมาถึง
|
||||||
|
// เช่น data.filter(c => new Date(c.date) > new Date()).length
|
||||||
|
return data.length; // นับจำนวนการนัดหมายทั้งหมด
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
// ใช้ useQuery แยกกันสำหรับแต่ละข้อมูล
|
||||||
|
const { data: doctorsCount, isLoading: isDoctorsLoading, isError: isDoctorsError } = useQuery({
|
||||||
|
queryKey: ['doctorsCount'],
|
||||||
|
queryFn: fetchDoctorsCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: patientsCount, isLoading: isPatientsLoading, isError: isPatientsError } = useQuery({
|
||||||
|
queryKey: ['patientsCount'],
|
||||||
|
queryFn: fetchPatientsCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: consultationsCount, isLoading: isConsultationsLoading, isError: isConsultationsError } = useQuery({
|
||||||
|
queryKey: ['consultationsCount'],
|
||||||
|
queryFn: fetchConsultations,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDoctorsLoading || isPatientsLoading || isConsultationsLoading) {
|
||||||
|
return <div className="text-center p-8">กำลังโหลดข้อมูล...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDoctorsError || isPatientsError || isConsultationsError) {
|
||||||
|
return <div className="alert alert-error">เกิดข้อผิดพลาดในการโหลดข้อมูล</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">ภาพรวมระบบ</h1>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Card แสดงจำนวนแพทย์ */}
|
||||||
|
<div className="stats shadow bg-white">
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-primary">
|
||||||
|
<FaUserMd size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">จำนวนแพทย์</div>
|
||||||
|
<div className="stat-value text-primary">{doctorsCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card แสดงจำนวนผู้ป่วย */}
|
||||||
|
<div className="stats shadow bg-white">
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-secondary">
|
||||||
|
<FaUsers size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">จำนวนผู้ป่วย</div>
|
||||||
|
<div className="stat-value text-secondary">{patientsCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card แสดงจำนวนนัดหมาย */}
|
||||||
|
<div className="stats shadow bg-white">
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-accent">
|
||||||
|
<FaCalendarAlt size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">จำนวนนัดหมาย</div>
|
||||||
|
<div className="stat-value text-accent">{consultationsCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
src/pages/DoctorsPage.jsx
Normal file
162
src/pages/DoctorsPage.jsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import api from '../api/axios'; // Import Axios instance ที่มี JWT Interceptor
|
||||||
|
import { FaPlus, FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเรียก API (Read)
|
||||||
|
const fetchDoctors = async () => {
|
||||||
|
const { data } = await api.get('/doctors');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเพิ่มแพทย์ใหม่ (Create)
|
||||||
|
const addDoctor = async (newDoctor) => {
|
||||||
|
const { data } = await api.post('/doctors', newDoctor);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับลบแพทย์ (Delete)
|
||||||
|
const deleteDoctor = async (id) => {
|
||||||
|
await api.delete(`/doctors/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DoctorsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 1. ดึงข้อมูลแพทย์จาก API (Read)
|
||||||
|
const { data: doctors, isLoading, isError, error } = useQuery({
|
||||||
|
queryKey: ['doctors'],
|
||||||
|
queryFn: fetchDoctors,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. จัดการฟอร์มสำหรับเพิ่มแพทย์ (Create)
|
||||||
|
const { register, handleSubmit, reset, formState: { errors } } = useForm();
|
||||||
|
|
||||||
|
// 3. จัดการการเพิ่มข้อมูล (Mutation)
|
||||||
|
const addDoctorMutation = useMutation({
|
||||||
|
mutationFn: addDoctor,
|
||||||
|
onSuccess: () => {
|
||||||
|
// เมื่อเพิ่มสำเร็จ ให้ Invalidate cache เพื่อให้ React Query ดึงข้อมูลใหม่
|
||||||
|
queryClient.invalidateQueries(['doctors']);
|
||||||
|
reset();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. จัดการการลบข้อมูล (Mutation)
|
||||||
|
const deleteDoctorMutation = useMutation({
|
||||||
|
mutationFn: deleteDoctor,
|
||||||
|
onSuccess: () => {
|
||||||
|
// เมื่อลบสำเร็จ ให้ Invalidate cache เพื่อให้ React Query ดึงข้อมูลใหม่
|
||||||
|
queryClient.invalidateQueries(['doctors']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
addDoctorMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('คุณต้องการลบแพทย์คนนี้หรือไม่?')) {
|
||||||
|
deleteDoctorMutation.mutate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>กำลังโหลดข้อมูล...</div>;
|
||||||
|
if (isError) return <div>เกิดข้อผิดพลาด: {error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">จัดการข้อมูลแพทย์</h1>
|
||||||
|
|
||||||
|
{/* ปุ่มสำหรับเปิด Modal เพิ่มแพทย์ */}
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<FaPlus className="mr-2" /> เพิ่มแพทย์ใหม่
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ตารางแสดงข้อมูลแพทย์ */}
|
||||||
|
<div className="overflow-x-auto shadow-xl rounded-lg">
|
||||||
|
<table className="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th>ชื่อ</th>
|
||||||
|
<th>อีเมล</th>
|
||||||
|
<th>CRM</th>
|
||||||
|
<th>สาขา</th>
|
||||||
|
<th>เบอร์โทรศัพท์</th>
|
||||||
|
<th>การดำเนินการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{doctors.map((doctor) => (
|
||||||
|
<tr key={doctor.id}>
|
||||||
|
<td>{doctor.name}</td>
|
||||||
|
<td>{doctor.email}</td>
|
||||||
|
<td>{doctor.crm}</td>
|
||||||
|
<td>{doctor.specialty}</td>
|
||||||
|
<td>{doctor.telephone}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
|
onClick={() => handleDelete(doctor.id)}
|
||||||
|
disabled={deleteDoctorMutation.isLoading}
|
||||||
|
>
|
||||||
|
{deleteDoctorMutation.isLoading ? 'กำลังลบ...' : <FaTrash />}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal สำหรับเพิ่มแพทย์ */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg mb-4">เพิ่มแพทย์ใหม่</h3>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">ชื่อ</label>
|
||||||
|
<input type="text" placeholder="ชื่อ" className="input input-bordered" {...register("name", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">อีเมล</label>
|
||||||
|
<input type="email" placeholder="อีเมล" className="input input-bordered" {...register("email", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">CRM</label>
|
||||||
|
<input type="text" placeholder="CRM" className="input input-bordered" {...register("crm", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">สาขา</label>
|
||||||
|
<input type="text" placeholder="สาขา" className="input input-bordered" {...register("specialty", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">เบอร์โทรศัพท์</label>
|
||||||
|
<input type="text" placeholder="เบอร์โทรศัพท์" className="input input-bordered" {...register("telephone", { required: true })} />
|
||||||
|
</div>
|
||||||
|
{/* ... เพิ่ม input สำหรับข้อมูลอื่น ๆ ... */}
|
||||||
|
|
||||||
|
<div className="modal-action mt-4">
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={addDoctorMutation.isLoading}>
|
||||||
|
{addDoctorMutation.isLoading ? 'กำลังบันทึก...' : 'บันทึก'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn" onClick={() => setIsModalOpen(false)}>
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/pages/HomePage.jsx
Normal file
58
src/pages/HomePage.jsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// src/pages/HomePage.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import Hero from '../components/Hero';
|
||||||
|
import Feature from '../components/Feature';
|
||||||
|
import About from '../components/About';
|
||||||
|
import CTA from '../components/CTA';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white">
|
||||||
|
<Hero
|
||||||
|
title="ระบบจัดการโรงพยาบาลอัจฉริยะ"
|
||||||
|
description="ยกระดับการจัดการข้อมูลผู้ป่วย, ตารางนัดหมาย, และการบริการทางการแพทย์ ให้ง่ายดายและมีประสิทธิภาพกว่าที่เคย"
|
||||||
|
image="https://placehold.co/600x400"
|
||||||
|
primaryLink={{ to: "/login", text: "เข้าสู่ระบบ" }}
|
||||||
|
secondaryLink={{ to: "/register", text: "ลงทะเบียน" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-extrabold text-center text-gray-900 mb-8">
|
||||||
|
คุณสมบัติเด่นของระบบ
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<Feature
|
||||||
|
title="ข้อมูลผู้ป่วย"
|
||||||
|
description="จัดการและเข้าถึงประวัติผู้ป่วยได้อย่างรวดเร็วและปลอดภัย"
|
||||||
|
icon="M12 11c-2.21 0-4 1.79-4 4v2h8v-2c0-2.21-1.79-4-4-4zm-4 4c0-1.1.9-2 2-2s2 .9 2 2"
|
||||||
|
/>
|
||||||
|
<Feature
|
||||||
|
title="ตารางนัดหมาย"
|
||||||
|
description="วางแผนและจัดการนัดหมายแพทย์ได้อย่างเป็นระบบ"
|
||||||
|
icon="M12 11c-2.21 0-4 1.79-4 4v2h8v-2c0-2.21-1.79-4-4-4zm-4 4c0-1.1.9-2 2-2s2 .9 2 2"
|
||||||
|
/>
|
||||||
|
<Feature
|
||||||
|
title="การสื่อสาร"
|
||||||
|
description="ส่งข้อความแจ้งเตือนและอัปเดตข้อมูลสำคัญถึงผู้ป่วยได้โดยตรง"
|
||||||
|
icon="M12 11c-2.21 0-4 1.79-4 4v2h8v-2c0-2.21-1.79-4-4-4zm-4 4c0-1.1.9-2 2-2s2 .9 2 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<About
|
||||||
|
title="เกี่ยวกับเรา"
|
||||||
|
description="เรามุ่งมั่นที่จะพัฒนาเทคโนโลยีเพื่อยกระดับการดูแลสุขภาพ ระบบของเราถูกออกแบบมาเพื่อช่วยให้บุคลากรทางการแพทย์สามารถทำงานได้อย่างมีประสิทธิภาพ"
|
||||||
|
image="https://placehold.co/600x400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CTA
|
||||||
|
title="พร้อมที่จะเริ่มต้นหรือยัง?"
|
||||||
|
subtitle="เข้าร่วมกับเราวันนี้เพื่อการจัดการที่เหนือกว่า."
|
||||||
|
linkText="ลงทะเบียนตอนนี้"
|
||||||
|
linkTo="/register"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/pages/LoginPage.jsx
Normal file
96
src/pages/LoginPage.jsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// src/pages/LoginPage.jsx
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { loginUser } from '../features/auth/authSlice';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { loading } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
setError
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
try {
|
||||||
|
await dispatch(loginUser(data)).unwrap();
|
||||||
|
} catch (err) {
|
||||||
|
setError('root.loginFailed', {
|
||||||
|
type: 'manual',
|
||||||
|
message: 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card w-full max-w-sm shrink-0 bg-white shadow-lg p-6">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-6 text-gray-800">
|
||||||
|
เข้าสู่ระบบ
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{/* Error จากการล็อกอิน */}
|
||||||
|
{errors.root?.loginFailed && (
|
||||||
|
<div className="alert alert-error text-sm">
|
||||||
|
{errors.root.loginFailed.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ช่องกรอกชื่อผู้ใช้ */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">ชื่อผู้ใช้</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="username"
|
||||||
|
className={`input input-bordered w-full ${errors.login ? 'input-error' : ''}`}
|
||||||
|
{...register('login', { required: 'กรุณากรอกชื่อผู้ใช้' })}
|
||||||
|
/>
|
||||||
|
{errors.login && <span className="text-red-500 text-sm mt-1">{errors.login.message}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ช่องกรอกรหัสผ่าน */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">รหัสผ่าน</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
className={`input input-bordered w-full ${errors.password ? 'input-error' : ''}`}
|
||||||
|
{...register('password', { required: 'กรุณากรอกรหัสผ่าน' })}
|
||||||
|
/>
|
||||||
|
{errors.password && <span className="text-red-500 text-sm mt-1">{errors.password.message}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ปุ่มเข้าสู่ระบบ */}
|
||||||
|
<div className="form-control pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* ลิงก์สำหรับเปลี่ยนหน้า */}
|
||||||
|
<p className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
ยังไม่มีบัญชี?{' '}
|
||||||
|
<Link to="/register" className="text-primary hover:underline font-medium">
|
||||||
|
ลงทะเบียน
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-center">
|
||||||
|
<Link to="/" className="link link-secondary">
|
||||||
|
กลับไปหน้าหลัก
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
src/pages/PatientsPage.jsx
Normal file
143
src/pages/PatientsPage.jsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import api from '../api/axios';
|
||||||
|
import { FaPlus, FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเรียก API (Read)
|
||||||
|
const fetchPatients = async () => {
|
||||||
|
const { data } = await api.get('/patients');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเพิ่มผู้ป่วยใหม่ (Create)
|
||||||
|
const addPatient = async (newPatient) => {
|
||||||
|
const { data } = await api.post('/patients', newPatient);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับลบผู้ป่วย (Delete)
|
||||||
|
const deletePatient = async (id) => {
|
||||||
|
await api.delete(`/patients/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PatientsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 1. ดึงข้อมูลผู้ป่วยจาก API (Read)
|
||||||
|
const { data: patients, isLoading, isError, error } = useQuery({
|
||||||
|
queryKey: ['patients'],
|
||||||
|
queryFn: fetchPatients,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. จัดการฟอร์มสำหรับเพิ่มผู้ป่วย (Create)
|
||||||
|
const { register, handleSubmit, reset, formState: { errors } } = useForm();
|
||||||
|
|
||||||
|
// 3. จัดการการเพิ่มข้อมูล (Mutation)
|
||||||
|
const addPatientMutation = useMutation({
|
||||||
|
mutationFn: addPatient,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['patients']);
|
||||||
|
reset();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. จัดการการลบข้อมูล (Mutation)
|
||||||
|
const deletePatientMutation = useMutation({
|
||||||
|
mutationFn: deletePatient,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['patients']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
addPatientMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('คุณต้องการลบผู้ป่วยคนนี้หรือไม่?')) {
|
||||||
|
deletePatientMutation.mutate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>กำลังโหลดข้อมูล...</div>;
|
||||||
|
if (isError) return <div>เกิดข้อผิดพลาด: {error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">จัดการข้อมูลผู้ป่วย</h1>
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<FaPlus className="mr-2" /> เพิ่มผู้ป่วยใหม่
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto shadow-xl rounded-lg">
|
||||||
|
<table className="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th>ชื่อ</th>
|
||||||
|
<th>อีเมล</th>
|
||||||
|
<th>วันเกิด</th>
|
||||||
|
<th>การดำเนินการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{patients.map((patient) => (
|
||||||
|
<tr key={patient.id}>
|
||||||
|
<td>{patient.name}</td>
|
||||||
|
<td>{patient.email}</td>
|
||||||
|
<td>{patient.birthday}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
|
onClick={() => handleDelete(patient.id)}
|
||||||
|
disabled={deletePatientMutation.isLoading}
|
||||||
|
>
|
||||||
|
{deletePatientMutation.isLoading ? 'กำลังลบ...' : <FaTrash />}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg mb-4">เพิ่มผู้ป่วยใหม่</h3>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">ชื่อ</label>
|
||||||
|
<input type="text" placeholder="ชื่อ" className="input input-bordered" {...register("name", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">อีเมล</label>
|
||||||
|
<input type="email" placeholder="อีเมล" className="input input-bordered" {...register("email", { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-control mt-2">
|
||||||
|
<label className="label">วันเกิด</label>
|
||||||
|
<input type="date" className="input input-bordered" {...register("birthday", { required: true })} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-action mt-4">
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={addPatientMutation.isLoading}>
|
||||||
|
{addPatientMutation.isLoading ? 'กำลังบันทึก...' : 'บันทึก'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn" onClick={() => setIsModalOpen(false)}>
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/pages/RegisterPage.jsx
Normal file
116
src/pages/RegisterPage.jsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import api from '../api/axios';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเรียก API ลงทะเบียน
|
||||||
|
const registerUser = async (userData) => {
|
||||||
|
// API ส่วนใหญ่จะต้องการแค่ username และ password
|
||||||
|
// ดังนั้นเราจะลบ confirmPassword ออกก่อนส่งไป
|
||||||
|
const { confirmPassword, ...dataToSend } = userData;
|
||||||
|
const { data } = await api.post('/auth/register', dataToSend);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
watch
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
// watch() ใช้เพื่อดูค่าของฟิลด์ password แบบ real-time
|
||||||
|
const password = watch("password", "");
|
||||||
|
|
||||||
|
const registerMutation = useMutation({
|
||||||
|
mutationFn: registerUser,
|
||||||
|
onSuccess: () => {
|
||||||
|
console.log("Registration successful!");
|
||||||
|
alert("ลงทะเบียนสำเร็จแล้ว! โปรดเข้าสู่ระบบ.");
|
||||||
|
navigate('/login');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Registration failed:", error.response?.data || error.message);
|
||||||
|
alert("การลงทะเบียนไม่สำเร็จ: " + (error.response?.data?.message || "มีข้อผิดพลาดเกิดขึ้น"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
registerMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center p-6">
|
||||||
|
<h3 className="text-2xl font-bold mb-4">ลงทะเบียน</h3>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-sm">
|
||||||
|
{/* ช่องสำหรับ Username หรือ Email */}
|
||||||
|
<div className="form-control mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Username</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="ชื่อผู้ใช้"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register("username", { required: "กรุณาใส่ชื่อผู้ใช้" })}
|
||||||
|
/>
|
||||||
|
{errors.username && <p className="text-red-500 text-sm mt-1">{errors.username.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ช่องสำหรับ Password */}
|
||||||
|
<div className="form-control mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Password</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="รหัสผ่าน"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register("password", {
|
||||||
|
required: "กรุณาใส่รหัสผ่าน",
|
||||||
|
minLength: {
|
||||||
|
value: 6,
|
||||||
|
message: "รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร"
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ช่องสำหรับ Confirm Password */}
|
||||||
|
<div className="form-control mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Confirm Password</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="ยืนยันรหัสผ่าน"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register("confirmPassword", {
|
||||||
|
required: "กรุณายืนยันรหัสผ่าน",
|
||||||
|
validate: (value) =>
|
||||||
|
value === password || "รหัสผ่านไม่ตรงกัน" // ใช้ validate() เพื่อเปรียบเทียบ
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ปุ่มสำหรับลงทะเบียน */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary w-full mt-4"
|
||||||
|
disabled={registerMutation.isLoading}
|
||||||
|
>
|
||||||
|
{registerMutation.isLoading ? 'กำลังลงทะเบียน...' : 'ลงทะเบียน'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
มีบัญชีอยู่แล้วใช่ไหม? <Link to="/login" className="link link-primary">เข้าสู่ระบบที่นี่</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
src/pages/UserManagementPage.jsx
Normal file
187
src/pages/UserManagementPage.jsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import api from '../api/axios';
|
||||||
|
|
||||||
|
// ฟังก์ชันสำหรับเรียก API
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
const { data } = await api.get('/admin/users'); // Endpoint ที่ดึงข้อมูลผู้ใช้ทั้งหมด
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (userData) => {
|
||||||
|
const { data } = await api.post('/admin/users', userData); // Endpoint สำหรับสร้างผู้ใช้ใหม่
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (userId) => {
|
||||||
|
await api.delete(`/admin/users/${userId}`); // Endpoint สำหรับลบผู้ใช้
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UserManagementPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [isFormVisible, setIsFormVisible] = useState(false); // สถานะสำหรับแสดง/ซ่อนฟอร์ม
|
||||||
|
|
||||||
|
// ดึงข้อมูลผู้ใช้งานด้วย useQuery
|
||||||
|
const { data: users, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: fetchUsers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// สร้าง Mutation สำหรับเพิ่มผู้ใช้
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: createUser,
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('เพิ่มผู้ใช้ใหม่เรียบร้อยแล้ว');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] }); // รีเฟรชข้อมูลผู้ใช้
|
||||||
|
setIsFormVisible(false); // ซ่อนฟอร์มเมื่อสำเร็จ
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('เกิดข้อผิดพลาดในการเพิ่มผู้ใช้: ' + (err.response?.data?.message || err.message));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// สร้าง Mutation สำหรับลบผู้ใช้
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: deleteUser,
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('ลบผู้ใช้เรียบร้อยแล้ว');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] }); // รีเฟรชข้อมูลผู้ใช้
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('เกิดข้อผิดพลาดในการลบผู้ใช้: ' + (err.response?.data?.message || err.message));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState: { errors }, reset } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
createMutation.mutate(data);
|
||||||
|
reset(); // ล้างฟอร์มหลังจากส่งข้อมูล
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (userId) => {
|
||||||
|
if (window.confirm('คุณแน่ใจหรือไม่ที่จะลบผู้ใช้นี้?')) {
|
||||||
|
deleteMutation.mutate(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>กำลังโหลดข้อมูลผู้ใช้งาน...</div>;
|
||||||
|
if (error) return <div>เกิดข้อผิดพลาด: {error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-3xl font-bold mb-6 text-gray-800">จัดการผู้ใช้งาน</h1>
|
||||||
|
|
||||||
|
{/* ปุ่มสำหรับแสดง/ซ่อนฟอร์มเพิ่มผู้ใช้ */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setIsFormVisible(!isFormVisible)}
|
||||||
|
>
|
||||||
|
{isFormVisible ? 'ซ่อนฟอร์ม' : 'เพิ่มผู้ใช้งานใหม่'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ฟอร์มสำหรับเพิ่มผู้ใช้งานใหม่ */}
|
||||||
|
{isFormVisible && (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md mb-6 transition-all duration-300">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">ฟอร์มเพิ่มผู้ใช้งาน</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{/* ฟิลด์สำหรับ Username */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">ชื่อผู้ใช้</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register('username', { required: 'กรุณากรอกชื่อผู้ใช้' })}
|
||||||
|
/>
|
||||||
|
{errors.username && <p className="text-red-500 text-sm mt-1">{errors.username.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ฟิลด์สำหรับ Password */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">รหัสผ่าน</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
{...register('password', { required: 'กรุณากรอกรหัสผ่าน' })}
|
||||||
|
/>
|
||||||
|
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ฟิลด์สำหรับเลือกบทบาท (Role) */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">บทบาท</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered w-full"
|
||||||
|
{...register('role', { required: 'กรุณาเลือกบทบาท' })}
|
||||||
|
>
|
||||||
|
<option value="">เลือกบทบาท</option>
|
||||||
|
<option value="admin">ผู้ดูแลระบบ</option>
|
||||||
|
<option value="doctor">แพทย์</option>
|
||||||
|
<option value="nurse">พยาบาล</option>
|
||||||
|
<option value="pharmacist">เภสัชกร</option>
|
||||||
|
<option value="patient">คนไข้</option>
|
||||||
|
</select>
|
||||||
|
{errors.role && <p className="text-red-500 text-sm mt-1">{errors.role.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ปุ่ม Submit */}
|
||||||
|
<div className="form-control pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={createMutation.isLoading}
|
||||||
|
>
|
||||||
|
{createMutation.isLoading ? 'กำลังเพิ่ม...' : 'เพิ่มผู้ใช้งาน'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ตารางแสดงรายชื่อผู้ใช้งาน */}
|
||||||
|
<div className="overflow-x-auto bg-white rounded-lg shadow-md">
|
||||||
|
<table className="table w-full">
|
||||||
|
{/* ส่วนหัวตาราง */}
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ชื่อผู้ใช้</th>
|
||||||
|
<th>บทบาท</th>
|
||||||
|
<th>สถานะ</th>
|
||||||
|
<th className="text-center">การจัดการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{/* ส่วนเนื้อหาตาราง */}
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge badge-lg capitalize">
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><span className="badge badge-success">Active</span></td>
|
||||||
|
<td className="text-center">
|
||||||
|
<button
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
|
onClick={() => handleDelete(user.id)}
|
||||||
|
disabled={deleteMutation.isLoading}
|
||||||
|
>
|
||||||
|
ลบ
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/routes/AdminRoutes.jsx
Normal file
23
src/routes/AdminRoutes.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// src/routes/AdminRoutes.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
import AdminLayout from '../layouts/AdminLayout.jsx';
|
||||||
|
import DashboardPage from "../pages/DashboardPage.jsx";
|
||||||
|
import DoctorsPage from "../pages/DoctorsPage.jsx";
|
||||||
|
import PatientsPage from "../pages/PatientsPage.jsx";
|
||||||
|
import ConsultationsPage from "../pages/ConsultationsPage.jsx";
|
||||||
|
import UserManagementPage from "../pages/UserManagementPage.jsx";
|
||||||
|
|
||||||
|
export default function AdminRoutes() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route element={<AdminLayout />}>
|
||||||
|
<Route index element={<DashboardPage />} />
|
||||||
|
<Route path="users" element={<UserManagementPage />} />
|
||||||
|
<Route path="doctors" element={<DoctorsPage />} />
|
||||||
|
<Route path="patients" element={<PatientsPage />} />
|
||||||
|
<Route path="consultations" element={<ConsultationsPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import {Route, Routes} from "react-router-dom";
|
import {Route, Routes} from "react-router-dom";
|
||||||
import PublicLayout from "../layouts/PublicLayout.jsx";
|
import PublicLayout from "../layouts/PublicLayout.jsx";
|
||||||
import AuthLayout from "../layouts/AuthLayout.jsx";
|
import AuthLayout from "../layouts/AuthLayout.jsx";
|
||||||
|
import LoginPage from '../pages/LoginPage.jsx';
|
||||||
|
import RegisterPage from '../pages/RegisterPage.jsx';
|
||||||
|
import HomePage from "../pages/HomePage.jsx";
|
||||||
|
|
||||||
export default function PublicRoutes(){
|
export default function PublicRoutes(){
|
||||||
return(
|
return(
|
||||||
@ -8,12 +11,12 @@ export default function PublicRoutes(){
|
|||||||
<Routes>
|
<Routes>
|
||||||
{/* PublicLayout สำหรับหน้าไม่ต้อง login */}
|
{/* PublicLayout สำหรับหน้าไม่ต้อง login */}
|
||||||
<Route element={<PublicLayout/>}>
|
<Route element={<PublicLayout/>}>
|
||||||
<Route path="/" element={<div>หน้าหลัก</div>}/>
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/about" element={<div>หน้าเกี่ยวกับเรา</div>}/>
|
|
||||||
</Route>
|
</Route>
|
||||||
{/* AuthLayout สำหรับหน้า login */}
|
{/* AuthLayout สำหรับหน้า login */}
|
||||||
<Route element={<AuthLayout/>}>
|
<Route element={<AuthLayout/>}>
|
||||||
<Route path="/login" element={<div>หน้าล็อกอิน</div>}/>
|
<Route path="login" element={<LoginPage />} />
|
||||||
|
<Route path="register" element={<RegisterPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user