diff --git a/README.md b/README.md index f3e9824..3a3d244 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,135 @@ # 🏥 ระบบบริหารจัดการโรงพยาบาล - ฝั่งผู้ใช้งาน (Frontend) -โปรเจคระบบบริหารจัดการโรงพยาบาล (Hospital Management System) -ส่วน Frontend พัฒนาโดยใช้ **React (Vite)** สำหรับให้บริการทั้งผู้ใช้งานทั่วไป (เช่น ผู้ป่วย) และผู้ดูแลระบบ (Admin) +โปรเจคนี้เป็น **ส่วน Frontend ของระบบบริหารจัดการโรงพยาบาล (Hospital Management System)** +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) -- **การจัดการสถานะ (State):** React Hooks - **UI Library:** Tailwind CSS / Daisy UI - **Routing:** React Router -- **การเชื่อมต่อ API:** Fetch (เชื่อมต่อกับ Backend) -- **ระบบยืนยันตัวตน:** JWT (JSON Web Token) +- **State Management:** React Hooks +- **API Integration:** Fetch / Axios (เชื่อมต่อกับ [Hospital Management API](https://gitea.softwarecraft.tech/gitea/hospital-management-api)) +- **Authentication:** JWT --- -## 🔄 การพัฒนาแบบ CI/CD - -โปรเจคนี้จะถูกพัฒนาโดยใช้แนวทาง **CI/CD** เพื่อให้สามารถ Build และ Deploy ได้อัตโนมัติผ่านระบบ Drone CI - -สามารถเข้าใช้งานระบบเพื่อทดสอบฟีเจอร์ได้ที่: -🌐 [https://frontend-sandbox.softwarecraft.tech/](https://frontend-sandbox.softwarecraft.tech/) - ---- - -## 🧪 วิธีใช้งานในเครื่องนักพัฒนา (Dev Environment) +## 🧪 การรันในเครื่องนักพัฒนา (Dev Environment) ```bash -# ติดตั้งแพ็กเกจที่จำเป็น +# ติดตั้ง dependencies npm install -# รันโปรเจคบนโหมดพัฒนา +# รัน Frontend บนโหมดพัฒนา npm run dev ``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0097fa3..3ff0880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,17 @@ "name": "healthcare_app", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.8.2", "@tailwindcss/vite": "^4.1.4", "@tanstack/react-query": "^5.74.4", "axios": "^1.8.4", "daisyui": "^5.0.20", + "prop-types": "^15.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.1", + "react-icons": "^5.5.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.5.1", "tailwindcss": "^4.1.4" }, @@ -769,9 +773,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -784,9 +788,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -794,9 +798,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -844,13 +848,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -864,32 +871,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "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": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1009,6 +1003,32 @@ "@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": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", @@ -1269,6 +1289,18 @@ "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": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz", @@ -1611,7 +1643,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1627,6 +1659,12 @@ "@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": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz", @@ -1648,9 +1686,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1735,9 +1773,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1913,7 +1951,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/daisyui": { @@ -2111,20 +2149,20 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2135,9 +2173,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2195,9 +2233,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2212,9 +2250,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2225,15 +2263,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2395,14 +2433,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2582,6 +2621,16 @@ "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2652,7 +2701,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2990,6 +3038,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3082,6 +3142,15 @@ "dev": true, "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": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3221,6 +3290,17 @@ "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": { "version": "1.1.0", "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" } }, + "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": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3322,6 +3440,27 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3536,6 +3675,15 @@ "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": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/package.json b/package.json index 4a7334a..bf15233 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.8.2", "@tailwindcss/vite": "^4.1.4", "@tanstack/react-query": "^5.74.4", "axios": "^1.8.4", "daisyui": "^5.0.20", + "prop-types": "^15.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.1", + "react-icons": "^5.5.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.5.1", "tailwindcss": "^4.1.4" }, diff --git a/src/App.jsx b/src/App.jsx index 9b4a2a5..bdc11fc 100644 --- a/src/App.jsx +++ b/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 AdminRoutes from "./routes/AdminRoutes.jsx"; +import ProtectedRoute from "./components/ProtectedRoute.jsx"; // Import ProtectedRoute function App() { - return ( - - - }/> - - - ) + + {/* เส้นทางสาธารณะที่ทุกคนเข้าถึงได้ */} + } /> + + {/* + เส้นทางสำหรับผู้ดูแลระบบที่ต้องมีการล็อกอินเท่านั้น + ProtectedRoute จะทำหน้าที่ตรวจสอบสิทธิ์ก่อนที่จะแสดง AdminRoutes + */} + + + + } + /> + + ); } -export default App +export default App; \ No newline at end of file diff --git a/src/api/axios.js b/src/api/axios.js new file mode 100644 index 0000000..1e18c7c --- /dev/null +++ b/src/api/axios.js @@ -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; \ No newline at end of file diff --git a/src/app/store.js b/src/app/store.js new file mode 100644 index 0000000..c6be9b4 --- /dev/null +++ b/src/app/store.js @@ -0,0 +1,9 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from '../features/auth/authSlice'; + +export const store = configureStore({ + reducer: { + auth: authReducer, + // เพิ่ม Reducer อื่น ๆ ที่นี่ในอนาคต + }, +}); \ No newline at end of file diff --git a/src/components/About.jsx b/src/components/About.jsx new file mode 100644 index 0000000..73185f5 --- /dev/null +++ b/src/components/About.jsx @@ -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 ( +
+
+ {/* Text Section */} +
+

+ {title} +

+

+ {description} +

+
+ {/* Image Section */} +
+ {title} +
+ +
+
+ ); +} + +About.propTypes = { + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/src/components/CTA.jsx b/src/components/CTA.jsx new file mode 100644 index 0000000..11a3814 --- /dev/null +++ b/src/components/CTA.jsx @@ -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 ( +
+
+ {/* Text Section */} +

+ {title} + {subtitle} +

+ {/* CTA Button */} +
+
+ + {linkText} + +
+
+
+
+ ); +} + +CTA.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + linkText: PropTypes.string.isRequired, + linkTo: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/src/components/Feature.jsx b/src/components/Feature.jsx new file mode 100644 index 0000000..d8291e1 --- /dev/null +++ b/src/components/Feature.jsx @@ -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 ( +
+ {/* SVG Icon */} +
+ + + +
+ + {/* Title & Description */} +

{title}

+

{description}

+
+ ); +} + +Feature.propTypes = { + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx new file mode 100644 index 0000000..8d12556 --- /dev/null +++ b/src/components/Hero.jsx @@ -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 ( +
+
+
+

+ {title} +

+

+ {description} +

+
+ {primaryLink && ( + + {primaryLink.text} + + )} + {secondaryLink && ( + + {secondaryLink.text} + + )} +
+
+
+ Hero +
+
+
+ ); +} + +Hero.propTypes = { + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + image: PropTypes.string, + primaryLink: PropTypes.object, + secondaryLink: PropTypes.object, +}; \ No newline at end of file diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..b32f0f2 --- /dev/null +++ b/src/components/ProtectedRoute.jsx @@ -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 ; + } + + // ถ้าล็อกอินแล้ว ให้แสดง Component ที่ถูกครอบไว้ + return children; +} + +ProtectedRoute.propTypes = { + children: PropTypes.node.isRequired, +}; \ No newline at end of file diff --git a/src/features/auth/authSlice.js b/src/features/auth/authSlice.js new file mode 100644 index 0000000..fd95dcf --- /dev/null +++ b/src/features/auth/authSlice.js @@ -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; \ No newline at end of file diff --git a/src/layouts/AdminLayout.jsx b/src/layouts/AdminLayout.jsx new file mode 100644 index 0000000..05ae903 --- /dev/null +++ b/src/layouts/AdminLayout.jsx @@ -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 ( +
+ {/* Overlay for Mobile */} + {isSidebarOpen && ( +
+ )} + + {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Mobile Header */} +
+

Admin Dashboard

+ +
+ + {/* Page Content */} +
+ +
+
+
+ ); +} + +// Reusable NavLink Component +const NavLink = ({ to, icon: Icon, label }) => ( + + + {label} + +); \ No newline at end of file diff --git a/src/layouts/AuthLayout.jsx b/src/layouts/AuthLayout.jsx index 37aeee3..56767a2 100644 --- a/src/layouts/AuthLayout.jsx +++ b/src/layouts/AuthLayout.jsx @@ -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(){ - return( - <> -
-
-
-

My App

-

เข้าสู่ระบบเพื่อเข้าถึงแดชบอร์ด จัดการข้อมูล และสำรวจฟีเจอร์ทั้งหมดที่เราพร้อมมอบให้คุณ ปลอดภัย รวดเร็ว และง่ายต่อการใช้งาน +export default function AuthLayout({ description }) { + return ( +

+
+ {/* Section ด้านซ้ายสำหรับแสดงข้อมูล */} +
+

+ Hospital Management System +

+ {description && ( +

+ {description}

-
-
- -
+ )} +
+ {/* Section ด้านขวาสำหรับแสดงฟอร์ม */} +
+
- - ) -} \ No newline at end of file +
+ ); +} + +AuthLayout.propTypes = { + description: PropTypes.string, +}; \ No newline at end of file diff --git a/src/layouts/PublicLayout.jsx b/src/layouts/PublicLayout.jsx index 32cc297..af3ba04 100644 --- a/src/layouts/PublicLayout.jsx +++ b/src/layouts/PublicLayout.jsx @@ -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 ( - <> - {/* แสดงทั้งหน้าจอ ใช้ Flex จัดเรียงแนวตั้ง*/} -
- {/* Navbar */} -
-
- MyAPP -
-
- เกี่ยวกับเรา - เข้าสู่ระบบ +
+ {/* Navbar */} +
- - ) + + + {/* Main Content */} +
+ +
+ + {/* Footer */} +
+
+

© {currentYear} Hospital Management System. All rights reserved.

+
+
+
+ ); } \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 9f6071d..e490e91 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,11 +1,33 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' +// src/main.jsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './index.css'; import './styles.css' -createRoot(document.getElementById('root')).render( - - - , -) +// Import BrowserRouter มาครอบ App Component +import { BrowserRouter } from 'react-router-dom'; + +// 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( + + {/* + ควรอยู่ระดับบนสุดของ Component Tree + เพื่อให้ทุก Component ที่อยู่ข้างในใช้งานได้ + */} + + + + + + + + +); \ No newline at end of file diff --git a/src/pages/ConsultationsPage.jsx b/src/pages/ConsultationsPage.jsx new file mode 100644 index 0000000..9246bcc --- /dev/null +++ b/src/pages/ConsultationsPage.jsx @@ -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
กำลังโหลดข้อมูล...
; + if (isError) return
เกิดข้อผิดพลาด: {error.message}
; + + return ( +
+

จัดการนัดหมาย

+
+ +
+ +
+ + + + + + + + + + + + {consultations.map((consultation) => ( + + + + + + + + ))} + +
แพทย์ผู้ป่วยวันที่เวลาการดำเนินการ
{consultation.doctor.name}{consultation.patient.name}{consultation.date}{consultation.time} + +
+
+ + {isModalOpen && ( +
+
+

สร้างนัดหมายใหม่

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx new file mode 100644 index 0000000..9b61592 --- /dev/null +++ b/src/pages/DashboardPage.jsx @@ -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
กำลังโหลดข้อมูล...
; + } + + if (isDoctorsError || isPatientsError || isConsultationsError) { + return
เกิดข้อผิดพลาดในการโหลดข้อมูล
; + } + + return ( +
+

ภาพรวมระบบ

+
+ {/* Card แสดงจำนวนแพทย์ */} +
+
+
+ +
+
จำนวนแพทย์
+
{doctorsCount}
+
+
+ + {/* Card แสดงจำนวนผู้ป่วย */} +
+
+
+ +
+
จำนวนผู้ป่วย
+
{patientsCount}
+
+
+ + {/* Card แสดงจำนวนนัดหมาย */} +
+
+
+ +
+
จำนวนนัดหมาย
+
{consultationsCount}
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/DoctorsPage.jsx b/src/pages/DoctorsPage.jsx new file mode 100644 index 0000000..0bdb60c --- /dev/null +++ b/src/pages/DoctorsPage.jsx @@ -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
กำลังโหลดข้อมูล...
; + if (isError) return
เกิดข้อผิดพลาด: {error.message}
; + + return ( +
+

จัดการข้อมูลแพทย์

+ + {/* ปุ่มสำหรับเปิด Modal เพิ่มแพทย์ */} +
+ +
+ + {/* ตารางแสดงข้อมูลแพทย์ */} +
+ + + + + + + + + + + + + {doctors.map((doctor) => ( + + + + + + + + + ))} + +
ชื่ออีเมลCRMสาขาเบอร์โทรศัพท์การดำเนินการ
{doctor.name}{doctor.email}{doctor.crm}{doctor.specialty}{doctor.telephone} + +
+
+ + {/* Modal สำหรับเพิ่มแพทย์ */} + {isModalOpen && ( +
+
+

เพิ่มแพทย์ใหม่

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {/* ... เพิ่ม input สำหรับข้อมูลอื่น ๆ ... */} + +
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 0000000..6404833 --- /dev/null +++ b/src/pages/HomePage.jsx @@ -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 ( +
+ + +
+
+

+ คุณสมบัติเด่นของระบบ +

+
+ + + +
+
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx new file mode 100644 index 0000000..a545b61 --- /dev/null +++ b/src/pages/LoginPage.jsx @@ -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 ( +
+

+ เข้าสู่ระบบ +

+
+ {/* Error จากการล็อกอิน */} + {errors.root?.loginFailed && ( +
+ {errors.root.loginFailed.message} +
+ )} + + {/* ช่องกรอกชื่อผู้ใช้ */} +
+ + + {errors.login && {errors.login.message}} +
+ + {/* ช่องกรอกรหัสผ่าน */} +
+ + + {errors.password && {errors.password.message}} +
+ + {/* ปุ่มเข้าสู่ระบบ */} +
+ +
+
+ + {/* ลิงก์สำหรับเปลี่ยนหน้า */} +

+ ยังไม่มีบัญชี?{' '} + + ลงทะเบียน + +

+
+ + กลับไปหน้าหลัก + +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/PatientsPage.jsx b/src/pages/PatientsPage.jsx new file mode 100644 index 0000000..859f2ff --- /dev/null +++ b/src/pages/PatientsPage.jsx @@ -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
กำลังโหลดข้อมูล...
; + if (isError) return
เกิดข้อผิดพลาด: {error.message}
; + + return ( +
+

จัดการข้อมูลผู้ป่วย

+
+ +
+ +
+ + + + + + + + + + + {patients.map((patient) => ( + + + + + + + ))} + +
ชื่ออีเมลวันเกิดการดำเนินการ
{patient.name}{patient.email}{patient.birthday} + +
+
+ + {isModalOpen && ( +
+
+

เพิ่มผู้ป่วยใหม่

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx new file mode 100644 index 0000000..65db072 --- /dev/null +++ b/src/pages/RegisterPage.jsx @@ -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 ( +
+

ลงทะเบียน

+
+ {/* ช่องสำหรับ Username หรือ Email */} +
+ + + {errors.username &&

{errors.username.message}

} +
+ + {/* ช่องสำหรับ Password */} +
+ + + {errors.password &&

{errors.password.message}

} +
+ + {/* ช่องสำหรับ Confirm Password */} +
+ + + value === password || "รหัสผ่านไม่ตรงกัน" // ใช้ validate() เพื่อเปรียบเทียบ + })} + /> + {errors.confirmPassword &&

{errors.confirmPassword.message}

} +
+ + {/* ปุ่มสำหรับลงทะเบียน */} + +
+ +
+ มีบัญชีอยู่แล้วใช่ไหม? เข้าสู่ระบบที่นี่ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/UserManagementPage.jsx b/src/pages/UserManagementPage.jsx new file mode 100644 index 0000000..a10afba --- /dev/null +++ b/src/pages/UserManagementPage.jsx @@ -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
กำลังโหลดข้อมูลผู้ใช้งาน...
; + if (error) return
เกิดข้อผิดพลาด: {error.message}
; + + return ( +
+

จัดการผู้ใช้งาน

+ + {/* ปุ่มสำหรับแสดง/ซ่อนฟอร์มเพิ่มผู้ใช้ */} +
+ +
+ + {/* ฟอร์มสำหรับเพิ่มผู้ใช้งานใหม่ */} + {isFormVisible && ( +
+

ฟอร์มเพิ่มผู้ใช้งาน

+
+ {/* ฟิลด์สำหรับ Username */} +
+ + + {errors.username &&

{errors.username.message}

} +
+ + {/* ฟิลด์สำหรับ Password */} +
+ + + {errors.password &&

{errors.password.message}

} +
+ + {/* ฟิลด์สำหรับเลือกบทบาท (Role) */} +
+ + + {errors.role &&

{errors.role.message}

} +
+ + {/* ปุ่ม Submit */} +
+ +
+
+
+ )} + + {/* ตารางแสดงรายชื่อผู้ใช้งาน */} +
+ + {/* ส่วนหัวตาราง */} + + + + + + + + + {/* ส่วนเนื้อหาตาราง */} + + {users.map((user) => ( + + + + + + + ))} + +
ชื่อผู้ใช้บทบาทสถานะการจัดการ
{user.username} + + {user.role} + + Active + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/routes/AdminRoutes.jsx b/src/routes/AdminRoutes.jsx new file mode 100644 index 0000000..3ce7cb0 --- /dev/null +++ b/src/routes/AdminRoutes.jsx @@ -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 ( + + }> + } /> + } /> + } /> + } /> + } /> + + + ); +} \ No newline at end of file diff --git a/src/routes/PublicRoutes.jsx b/src/routes/PublicRoutes.jsx index 234e987..b670dbd 100644 --- a/src/routes/PublicRoutes.jsx +++ b/src/routes/PublicRoutes.jsx @@ -1,6 +1,9 @@ import {Route, Routes} from "react-router-dom"; import PublicLayout from "../layouts/PublicLayout.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(){ return( @@ -8,12 +11,12 @@ export default function PublicRoutes(){ {/* PublicLayout สำหรับหน้าไม่ต้อง login */} }> - หน้าหลัก
}/> - หน้าเกี่ยวกับเรา
}/> + } /> {/* AuthLayout สำหรับหน้า login */} }> - หน้าล็อกอิน
}/> + } /> + } />