update
This commit is contained in:
@@ -1,138 +1,138 @@
|
|||||||
# Routing Configuration - Hướng dẫn Test
|
# Routing Configuration - Testing Guide
|
||||||
|
|
||||||
## ✅ Đã hoàn thành Chức năng 2
|
## ✅ Function 2 Completed
|
||||||
|
|
||||||
### Components đã tạo:
|
### Components Created:
|
||||||
|
|
||||||
1. **ProtectedRoute** - `src/components/auth/ProtectedRoute.tsx`
|
1. **ProtectedRoute** - `src/components/auth/ProtectedRoute.tsx`
|
||||||
- Bảo vệ routes yêu cầu authentication
|
- Protects routes requiring authentication
|
||||||
- Redirect về `/login` nếu chưa đăng nhập
|
- Redirects to `/login` if not logged in
|
||||||
- Lưu location để quay lại sau khi login
|
- Saves location to return after login
|
||||||
|
|
||||||
2. **AdminRoute** - `src/components/auth/AdminRoute.tsx`
|
2. **AdminRoute** - `src/components/auth/AdminRoute.tsx`
|
||||||
- Bảo vệ routes chỉ dành cho Admin
|
- Protects routes for Admin only
|
||||||
- Redirect về `/` nếu không phải admin
|
- Redirects to `/` if not admin
|
||||||
- Kiểm tra `userInfo.role === 'admin'`
|
- Checks `userInfo.role === 'admin'`
|
||||||
|
|
||||||
3. **Page Components**:
|
3. **Page Components**:
|
||||||
- `RoomListPage` - Danh sách phòng (public)
|
- `RoomListPage` - Room list (public)
|
||||||
- `BookingListPage` - Lịch sử đặt phòng (protected)
|
- `BookingListPage` - Booking history (protected)
|
||||||
- `DashboardPage` - Dashboard cá nhân (protected)
|
- `DashboardPage` - Personal dashboard (protected)
|
||||||
|
|
||||||
### Cấu trúc Routes:
|
### Route Structure:
|
||||||
|
|
||||||
#### Public Routes (Không cần đăng nhập):
|
#### Public Routes (No login required):
|
||||||
```
|
```
|
||||||
/ → HomePage
|
/ → HomePage
|
||||||
/rooms → RoomListPage
|
/rooms → RoomListPage
|
||||||
/about → About Page
|
/about → About Page
|
||||||
/login → Login Page (chưa có)
|
/login → Login Page (not yet)
|
||||||
/register → Register Page (chưa có)
|
/register → Register Page (not yet)
|
||||||
/forgot-password → Forgot Password Page (chưa có)
|
/forgot-password → Forgot Password Page (not yet)
|
||||||
/reset-password/:token → Reset Password Page (chưa có)
|
/reset-password/:token → Reset Password Page (not yet)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Protected Routes (Cần đăng nhập):
|
#### Protected Routes (Login required):
|
||||||
```
|
```
|
||||||
/dashboard → DashboardPage (ProtectedRoute)
|
/dashboard → DashboardPage (ProtectedRoute)
|
||||||
/bookings → BookingListPage (ProtectedRoute)
|
/bookings → BookingListPage (ProtectedRoute)
|
||||||
/profile → Profile Page (ProtectedRoute)
|
/profile → Profile Page (ProtectedRoute)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Admin Routes (Chỉ Admin):
|
#### Admin Routes (Admin only):
|
||||||
```
|
```
|
||||||
/admin → AdminLayout (AdminRoute)
|
/admin → AdminLayout (AdminRoute)
|
||||||
/admin/dashboard → Admin Dashboard
|
/admin/dashboard → Admin Dashboard
|
||||||
/admin/users → Quản lý người dùng
|
/admin/users → User Management
|
||||||
/admin/rooms → Quản lý phòng
|
/admin/rooms → Room Management
|
||||||
/admin/bookings → Quản lý đặt phòng
|
/admin/bookings → Booking Management
|
||||||
/admin/payments → Quản lý thanh toán
|
/admin/payments → Payment Management
|
||||||
/admin/services → Quản lý dịch vụ
|
/admin/services → Service Management
|
||||||
/admin/promotions → Quản lý khuyến mãi
|
/admin/promotions → Promotion Management
|
||||||
/admin/banners → Quản lý banner
|
/admin/banners → Banner Management
|
||||||
/admin/reports → Báo cáo
|
/admin/reports → Reports
|
||||||
/admin/settings → Cài đặt
|
/admin/settings → Settings
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Cách Test
|
## 🧪 How to Test
|
||||||
|
|
||||||
### 1. Khởi động Dev Server:
|
### 1. Start Dev Server:
|
||||||
```bash
|
```bash
|
||||||
cd /d/hotel-booking/client
|
cd /d/hotel-booking/client
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Mở `http://localhost:5173`
|
Open `http://localhost:5173`
|
||||||
|
|
||||||
### 2. Test Public Routes:
|
### 2. Test Public Routes:
|
||||||
- Truy cập `/` → Hiển thị HomePage ✅
|
- Access `/` → Display HomePage ✅
|
||||||
- Truy cập `/rooms` → Hiển thị RoomListPage ✅
|
- Access `/rooms` → Display RoomListPage ✅
|
||||||
- Truy cập `/about` → Hiển thị About Page ✅
|
- Access `/about` → Display About Page ✅
|
||||||
|
|
||||||
### 3. Test Protected Routes (Chưa login):
|
### 3. Test Protected Routes (Not logged in):
|
||||||
- Truy cập `/dashboard` → Redirect về `/login` ✅
|
- Access `/dashboard` → Redirect to `/login` ✅
|
||||||
- Truy cập `/bookings` → Redirect về `/login` ✅
|
- Access `/bookings` → Redirect to `/login` ✅
|
||||||
- Truy cập `/profile` → Redirect về `/login` ✅
|
- Access `/profile` → Redirect to `/login` ✅
|
||||||
|
|
||||||
### 4. Test Protected Routes (Đã login):
|
### 4. Test Protected Routes (Logged in):
|
||||||
- Click nút **"🔒 Demo Login"** ở góc dưới phải
|
- Click **"🔒 Demo Login"** button at bottom right
|
||||||
- Truy cập `/dashboard` → Hiển thị Dashboard ✅
|
- Access `/dashboard` → Display Dashboard ✅
|
||||||
- Truy cập `/bookings` → Hiển thị Booking List ✅
|
- Access `/bookings` → Display Booking List ✅
|
||||||
- Truy cập `/profile` → Hiển thị Profile ✅
|
- Access `/profile` → Display Profile ✅
|
||||||
|
|
||||||
### 5. Test Admin Routes (Role = Customer):
|
### 5. Test Admin Routes (Role = Customer):
|
||||||
- Đảm bảo đã login (role = customer)
|
- Ensure logged in (role = customer)
|
||||||
- Truy cập `/admin` → Redirect về `/` ✅
|
- Access `/admin` → Redirect to `/` ✅
|
||||||
- Truy cập `/admin/dashboard` → Redirect về `/` ✅
|
- Access `/admin/dashboard` → Redirect to `/` ✅
|
||||||
|
|
||||||
### 6. Test Admin Routes (Role = Admin):
|
### 6. Test Admin Routes (Role = Admin):
|
||||||
- Click nút **"👑 Switch to Admin"**
|
- Click **"👑 Switch to Admin"** button
|
||||||
- Truy cập `/admin` → Redirect về `/admin/dashboard` ✅
|
- Access `/admin` → Redirect to `/admin/dashboard` ✅
|
||||||
- Truy cập `/admin/users` → Hiển thị User Management ✅
|
- Access `/admin/users` → Display User Management ✅
|
||||||
- Truy cập `/admin/rooms` → Hiển thị Room Management ✅
|
- Access `/admin/rooms` → Display Room Management ✅
|
||||||
- Click các menu trong SidebarAdmin → Hoạt động bình thường ✅
|
- Click menu items in SidebarAdmin → Works normally ✅
|
||||||
|
|
||||||
### 7. Test Logout:
|
### 7. Test Logout:
|
||||||
- Click nút **"🔓 Demo Logout"**
|
- Click **"🔓 Demo Logout"** button
|
||||||
- Truy cập `/dashboard` → Redirect về `/login` ✅
|
- Access `/dashboard` → Redirect to `/login` ✅
|
||||||
- Truy cập `/admin` → Redirect về `/` ✅
|
- Access `/admin` → Redirect to `/` ✅
|
||||||
|
|
||||||
## 🎯 Kết quả mong đợi
|
## 🎯 Expected Results
|
||||||
|
|
||||||
### ✅ ProtectedRoute:
|
### ✅ ProtectedRoute:
|
||||||
1. User chưa login không thể truy cập protected routes
|
1. Users not logged in cannot access protected routes
|
||||||
2. Redirect về `/login` và lưu `state.from` để quay lại sau
|
2. Redirect to `/login` and save `state.from` to return later
|
||||||
3. User đã login có thể truy cập protected routes bình thường
|
3. Logged in users can access protected routes normally
|
||||||
|
|
||||||
### ✅ AdminRoute:
|
### ✅ AdminRoute:
|
||||||
1. User không phải admin không thể truy cập `/admin/*`
|
1. Non-admin users cannot access `/admin/*`
|
||||||
2. Redirect về `/` nếu không phải admin
|
2. Redirect to `/` if not admin
|
||||||
3. Admin có thể truy cập tất cả admin routes
|
3. Admin can access all admin routes
|
||||||
|
|
||||||
### ✅ Không có redirect loop:
|
### ✅ No redirect loop:
|
||||||
1. Redirect chỉ xảy ra 1 lần
|
1. Redirect only happens once
|
||||||
2. Không có vòng lặp redirect vô tận
|
2. No infinite redirect loop
|
||||||
3. Browser history hoạt động đúng (back/forward)
|
3. Browser history works correctly (back/forward)
|
||||||
|
|
||||||
## 📝 Demo Buttons (Tạm thời)
|
## 📝 Demo Buttons (Temporary)
|
||||||
|
|
||||||
### 🔒 Demo Login/Logout:
|
### 🔒 Demo Login/Logout:
|
||||||
- Click để toggle authentication state
|
- Click to toggle authentication state
|
||||||
- Mô phỏng login/logout
|
- Simulates login/logout
|
||||||
- Sẽ được thay bằng Zustand store ở Chức năng 3
|
- Will be replaced by Zustand store in Function 3
|
||||||
|
|
||||||
### 👑 Switch Role:
|
### 👑 Switch Role:
|
||||||
- Chỉ hiển thị khi đã login
|
- Only displays when logged in
|
||||||
- Toggle giữa `customer` ↔ `admin`
|
- Toggle between `customer` ↔ `admin`
|
||||||
- Test AdminRoute hoạt động đúng
|
- Test AdminRoute works correctly
|
||||||
|
|
||||||
## 🚀 Bước tiếp theo
|
## 🚀 Next Steps
|
||||||
|
|
||||||
Chức năng 3: useAuthStore (Zustand Store)
|
Function 3: useAuthStore (Zustand Store)
|
||||||
- Tạo store quản lý auth state toàn cục
|
- Create store to manage global auth state
|
||||||
- Thay thế demo state bằng Zustand
|
- Replace demo state with Zustand
|
||||||
- Tích hợp với localStorage
|
- Integrate with localStorage
|
||||||
- Xóa demo toggle buttons
|
- Remove demo toggle buttons
|
||||||
|
|
||||||
## 🔧 File Structure
|
## 🔧 File Structure
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# useAuthStore - Zustand Authentication Store
|
# useAuthStore - Zustand Authentication Store
|
||||||
|
|
||||||
## ✅ Hoàn thành Chức năng 3
|
## ✅ Function 3 Completed
|
||||||
|
|
||||||
### 📦 Files đã tạo:
|
### 📦 Files Created:
|
||||||
|
|
||||||
1. **`src/store/useAuthStore.ts`** - Zustand store quản lý auth
|
1. **`src/store/useAuthStore.ts`** - Zustand store managing auth
|
||||||
2. **`src/services/api/apiClient.ts`** - Axios client với interceptors
|
2. **`src/services/api/apiClient.ts`** - Axios client with interceptors
|
||||||
3. **`src/services/api/authService.ts`** - Auth API service
|
3. **`src/services/api/authService.ts`** - Auth API service
|
||||||
4. **`.env.example`** - Template cho environment variables
|
4. **`.env.example`** - Template for environment variables
|
||||||
|
|
||||||
### 🎯 Tính năng đã implement:
|
### 🎯 Features Implemented:
|
||||||
|
|
||||||
#### State Management:
|
#### State Management:
|
||||||
```typescript
|
```typescript
|
||||||
@@ -24,19 +24,19 @@ interface AuthState {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Actions:
|
#### Actions:
|
||||||
- ✅ `login(credentials)` - Đăng nhập
|
- ✅ `login(credentials)` - Login
|
||||||
- ✅ `register(data)` - Đăng ký tài khoản mới
|
- ✅ `register(data)` - Register new account
|
||||||
- ✅ `logout()` - Đăng xuất
|
- ✅ `logout()` - Logout
|
||||||
- ✅ `setUser(user)` - Cập nhật thông tin user
|
- ✅ `setUser(user)` - Update user information
|
||||||
- ✅ `refreshAuthToken()` - Làm mới token
|
- ✅ `refreshAuthToken()` - Refresh token
|
||||||
- ✅ `forgotPassword(data)` - Quên mật khẩu
|
- ✅ `forgotPassword(data)` - Forgot password
|
||||||
- ✅ `resetPassword(data)` - Đặt lại mật khẩu
|
- ✅ `resetPassword(data)` - Reset password
|
||||||
- ✅ `initializeAuth()` - Khởi tạo auth từ localStorage
|
- ✅ `initializeAuth()` - Initialize auth from localStorage
|
||||||
- ✅ `clearError()` - Xóa error message
|
- ✅ `clearError()` - Clear error message
|
||||||
|
|
||||||
### 📝 Cách sử dụng:
|
### 📝 Usage:
|
||||||
|
|
||||||
#### 1. Khởi tạo trong App.tsx:
|
#### 1. Initialize in App.tsx:
|
||||||
```typescript
|
```typescript
|
||||||
import useAuthStore from './store/useAuthStore';
|
import useAuthStore from './store/useAuthStore';
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Sử dụng trong Login Form:
|
#### 2. Use in Login Form:
|
||||||
```typescript
|
```typescript
|
||||||
import useAuthStore from '../store/useAuthStore';
|
import useAuthStore from '../store/useAuthStore';
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ const LoginPage = () => {
|
|||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
try {
|
try {
|
||||||
await login(data);
|
await login(data);
|
||||||
navigate('/dashboard'); // Redirect sau khi login
|
navigate('/dashboard'); // Redirect after login
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý bởi store
|
// Error has been handled by store
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,14 +78,14 @@ const LoginPage = () => {
|
|||||||
{/* Form fields */}
|
{/* Form fields */}
|
||||||
{error && <div>{error}</div>}
|
{error && <div>{error}</div>}
|
||||||
<button disabled={isLoading}>
|
<button disabled={isLoading}>
|
||||||
{isLoading ? 'Đang xử lý...' : 'Đăng nhập'}
|
{isLoading ? 'Processing...' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Sử dụng trong Register Form:
|
#### 3. Use in Register Form:
|
||||||
```typescript
|
```typescript
|
||||||
const RegisterPage = () => {
|
const RegisterPage = () => {
|
||||||
const { register, isLoading } = useAuthStore();
|
const { register, isLoading } = useAuthStore();
|
||||||
@@ -94,9 +94,9 @@ const RegisterPage = () => {
|
|||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
try {
|
try {
|
||||||
await register(data);
|
await register(data);
|
||||||
navigate('/login'); // Redirect về login
|
navigate('/login'); // Redirect to login
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error được hiển thị qua toast
|
// Error displayed via toast
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,21 +111,21 @@ const Header = () => {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
await logout();
|
||||||
// Auto redirect về login nếu cần
|
// Auto redirect to login if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
return <button onClick={handleLogout}>Đăng xuất</button>;
|
return <button onClick={handleLogout}>Logout</button>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Hiển thị thông tin user:
|
#### 5. Display user information:
|
||||||
```typescript
|
```typescript
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { userInfo } = useAuthStore();
|
const { userInfo } = useAuthStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Xin chào, {userInfo?.name}</h1>
|
<h1>Hello, {userInfo?.name}</h1>
|
||||||
<p>Email: {userInfo?.email}</p>
|
<p>Email: {userInfo?.email}</p>
|
||||||
<p>Role: {userInfo?.role}</p>
|
<p>Role: {userInfo?.role}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,68 +135,68 @@ const Profile = () => {
|
|||||||
|
|
||||||
### 🔐 LocalStorage Persistence:
|
### 🔐 LocalStorage Persistence:
|
||||||
|
|
||||||
Store tự động lưu và đọc từ localStorage:
|
Store automatically saves and reads from localStorage:
|
||||||
- `token` - JWT access token
|
- `token` - JWT access token
|
||||||
- `refreshToken` - JWT refresh token
|
- `refreshToken` - JWT refresh token
|
||||||
- `userInfo` - Thông tin user
|
- `userInfo` - User information
|
||||||
|
|
||||||
Khi reload page, auth state được khôi phục tự động qua `initializeAuth()`.
|
When page reloads, auth state is automatically restored via `initializeAuth()`.
|
||||||
|
|
||||||
### 🌐 API Integration:
|
### 🌐 API Integration:
|
||||||
|
|
||||||
#### Base URL Configuration:
|
#### Base URL Configuration:
|
||||||
Tạo file `.env` trong thư mục `client/`:
|
Create `.env` file in `client/` directory:
|
||||||
```env
|
```env
|
||||||
VITE_API_URL=http://localhost:3000
|
VITE_API_URL=http://localhost:3000
|
||||||
VITE_ENV=development
|
VITE_ENV=development
|
||||||
```
|
```
|
||||||
|
|
||||||
#### API Endpoints được sử dụng:
|
#### API Endpoints Used:
|
||||||
- `POST /api/auth/login` - Đăng nhập
|
- `POST /api/auth/login` - Login
|
||||||
- `POST /api/auth/register` - Đăng ký
|
- `POST /api/auth/register` - Register
|
||||||
- `POST /api/auth/logout` - Đăng xuất
|
- `POST /api/auth/logout` - Logout
|
||||||
- `GET /api/auth/profile` - Lấy profile
|
- `GET /api/auth/profile` - Get profile
|
||||||
- `POST /api/auth/refresh-token` - Refresh token
|
- `POST /api/auth/refresh-token` - Refresh token
|
||||||
- `POST /api/auth/forgot-password` - Quên mật khẩu
|
- `POST /api/auth/forgot-password` - Forgot password
|
||||||
- `POST /api/auth/reset-password` - Đặt lại mật khẩu
|
- `POST /api/auth/reset-password` - Reset password
|
||||||
|
|
||||||
### 🛡️ Security Features:
|
### 🛡️ Security Features:
|
||||||
|
|
||||||
1. **Auto Token Injection**:
|
1. **Auto Token Injection**:
|
||||||
- Axios interceptor tự động thêm token vào headers
|
- Axios interceptor automatically adds token to headers
|
||||||
```typescript
|
```typescript
|
||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Auto Logout on 401**:
|
2. **Auto Logout on 401**:
|
||||||
- Khi token hết hạn (401), tự động logout và redirect về login
|
- When token expires (401), automatically logout and redirect to login
|
||||||
|
|
||||||
3. **Token Refresh**:
|
3. **Token Refresh**:
|
||||||
- Có thể refresh token khi sắp hết hạn
|
- Can refresh token when about to expire
|
||||||
|
|
||||||
4. **Password Hashing**:
|
4. **Password Hashing**:
|
||||||
- Backend xử lý bcrypt hashing
|
- Backend handles bcrypt hashing
|
||||||
|
|
||||||
### 📱 Toast Notifications:
|
### 📱 Toast Notifications:
|
||||||
|
|
||||||
Store tự động hiển thị toast cho các events:
|
Store automatically displays toast for events:
|
||||||
- ✅ Login thành công
|
- ✅ Login successful
|
||||||
- ✅ Đăng ký thành công
|
- ✅ Registration successful
|
||||||
- ✅ Logout
|
- ✅ Logout
|
||||||
- ❌ Login thất bại
|
- ❌ Login failed
|
||||||
- ❌ Đăng ký thất bại
|
- ❌ Registration failed
|
||||||
- ❌ API errors
|
- ❌ API errors
|
||||||
|
|
||||||
### 🔄 Component Updates:
|
### 🔄 Component Updates:
|
||||||
|
|
||||||
#### ProtectedRoute:
|
#### ProtectedRoute:
|
||||||
```typescript
|
```typescript
|
||||||
// TRƯỚC (với props)
|
// BEFORE (with props)
|
||||||
<ProtectedRoute isAuthenticated={isAuthenticated}>
|
<ProtectedRoute isAuthenticated={isAuthenticated}>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
// SAU (tự động lấy từ store)
|
// AFTER (automatically gets from store)
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@@ -204,19 +204,19 @@ Store tự động hiển thị toast cho các events:
|
|||||||
|
|
||||||
#### AdminRoute:
|
#### AdminRoute:
|
||||||
```typescript
|
```typescript
|
||||||
// TRƯỚC (với props)
|
// BEFORE (with props)
|
||||||
<AdminRoute userInfo={userInfo}>
|
<AdminRoute userInfo={userInfo}>
|
||||||
<AdminPanel />
|
<AdminPanel />
|
||||||
</AdminRoute>
|
</AdminRoute>
|
||||||
|
|
||||||
// SAU (tự động lấy từ store)
|
// AFTER (automatically gets from store)
|
||||||
<AdminRoute>
|
<AdminRoute>
|
||||||
<AdminPanel />
|
<AdminPanel />
|
||||||
</AdminRoute>
|
</AdminRoute>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### LayoutMain:
|
#### LayoutMain:
|
||||||
Vẫn nhận props từ App.tsx để hiển thị Header/Navbar:
|
Still receives props from App.tsx to display Header/Navbar:
|
||||||
```typescript
|
```typescript
|
||||||
<LayoutMain
|
<LayoutMain
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
@@ -227,44 +227,44 @@ Vẫn nhận props từ App.tsx để hiển thị Header/Navbar:
|
|||||||
|
|
||||||
### 🧪 Testing:
|
### 🧪 Testing:
|
||||||
|
|
||||||
Để test authentication flow:
|
To test authentication flow:
|
||||||
|
|
||||||
1. **Tạo file `.env`**:
|
1. **Create `.env` file**:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Ensure backend đang chạy**:
|
2. **Ensure backend is running**:
|
||||||
```bash
|
```bash
|
||||||
cd server
|
cd server
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Chạy frontend**:
|
3. **Run frontend**:
|
||||||
```bash
|
```bash
|
||||||
cd client
|
cd client
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Test flow**:
|
4. **Test flow**:
|
||||||
- Truy cập `/register` → Đăng ký tài khoản
|
- Access `/register` → Register account
|
||||||
- Truy cập `/login` → Đăng nhập
|
- Access `/login` → Login
|
||||||
- Truy cập `/dashboard` → Xem dashboard (protected)
|
- Access `/dashboard` → View dashboard (protected)
|
||||||
- Click logout → Xóa session
|
- Click logout → Clear session
|
||||||
- Reload page → Auth state được khôi phục
|
- Reload page → Auth state restored
|
||||||
|
|
||||||
### 🚀 Next Steps:
|
### 🚀 Next Steps:
|
||||||
|
|
||||||
**Chức năng 4: Form Login**
|
**Function 4: Login Form**
|
||||||
- Tạo LoginPage với React Hook Form + Yup
|
- Create LoginPage with React Hook Form + Yup
|
||||||
- Tích hợp với useAuthStore
|
- Integrate with useAuthStore
|
||||||
- UX enhancements (loading, show/hide password, remember me)
|
- UX enhancements (loading, show/hide password, remember me)
|
||||||
|
|
||||||
**Chức năng 5: Form Register**
|
**Function 5: Register Form**
|
||||||
- Tạo RegisterPage với validation
|
- Create RegisterPage with validation
|
||||||
- Tích hợp với useAuthStore
|
- Integrate with useAuthStore
|
||||||
|
|
||||||
**Chức năng 6-7: Password Reset Flow**
|
**Function 6-7: Password Reset Flow**
|
||||||
- ForgotPasswordPage
|
- ForgotPasswordPage
|
||||||
- ResetPasswordPage
|
- ResetPasswordPage
|
||||||
|
|
||||||
@@ -295,11 +295,11 @@ interface UserInfo {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✅ Kết quả đạt được:
|
### ✅ Results Achieved:
|
||||||
|
|
||||||
1. ✅ Toàn bộ thông tin user được quản lý tập trung
|
1. ✅ All user information managed centrally
|
||||||
2. ✅ Duy trì đăng nhập sau khi reload trang
|
2. ✅ Maintain login after page reload
|
||||||
3. ✅ Dễ dàng truy cập userInfo trong mọi component
|
3. ✅ Easy access to userInfo in any component
|
||||||
4. ✅ Auto token management
|
4. ✅ Auto token management
|
||||||
5. ✅ Type-safe với TypeScript
|
5. ✅ Type-safe with TypeScript
|
||||||
6. ✅ Clean code, dễ maintain
|
6. ✅ Clean code, easy to maintain
|
||||||
|
|||||||
@@ -69,20 +69,20 @@ import {
|
|||||||
CheckOutPage,
|
CheckOutPage,
|
||||||
} from './pages/admin';
|
} from './pages/admin';
|
||||||
|
|
||||||
// Demo component cho các page chưa có
|
// Demo component for pages not yet created
|
||||||
const DemoPage: React.FC<{ title: string }> = ({ title }) => (
|
const DemoPage: React.FC<{ title: string }> = ({ title }) => (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-800">
|
<h1 className="text-3xl font-bold text-gray-800">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mt-4">
|
<p className="text-gray-600 mt-4">
|
||||||
Page này đang được phát triển...
|
This page is under development...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Sử dụng Zustand store
|
// Use Zustand store
|
||||||
const {
|
const {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
userInfo,
|
userInfo,
|
||||||
@@ -96,7 +96,7 @@ function App() {
|
|||||||
loadGuestFavorites,
|
loadGuestFavorites,
|
||||||
} = useFavoritesStore();
|
} = useFavoritesStore();
|
||||||
|
|
||||||
// Khởi tạo auth state khi app load
|
// Initialize auth state when app loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeAuth();
|
initializeAuth();
|
||||||
}, [initializeAuth]);
|
}, [initializeAuth]);
|
||||||
@@ -161,10 +161,10 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="about"
|
path="about"
|
||||||
element={<DemoPage title="Giới thiệu" />}
|
element={<DemoPage title="About" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Protected Routes - Yêu cầu đăng nhập */}
|
{/* Protected Routes - Requires login */}
|
||||||
<Route
|
<Route
|
||||||
path="dashboard"
|
path="dashboard"
|
||||||
element={
|
element={
|
||||||
@@ -225,7 +225,7 @@ function App() {
|
|||||||
path="profile"
|
path="profile"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DemoPage title="Hồ sơ" />
|
<DemoPage title="Profile" />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -249,7 +249,7 @@ function App() {
|
|||||||
element={<ResetPasswordPage />}
|
element={<ResetPasswordPage />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Admin Routes - Chỉ admin mới truy cập được */}
|
{/* Admin Routes - Only admin can access */}
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
@@ -301,22 +301,22 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="banners"
|
path="banners"
|
||||||
element={<DemoPage title="Quản lý banner" />}
|
element={<DemoPage title="Banner Management" />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="reports"
|
path="reports"
|
||||||
element={<DemoPage title="Báo cáo" />}
|
element={<DemoPage title="Reports" />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
element={<DemoPage title="Cài đặt" />}
|
element={<DemoPage title="Settings" />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* 404 Route */}
|
{/* 404 Route */}
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={<DemoPage title="404 - Không tìm thấy trang" />}
|
element={<DemoPage title="404 - Page not found" />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ interface AdminRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AdminRoute - Bảo vệ các route chỉ dành cho Admin
|
* AdminRoute - Protects routes that are only for Admin
|
||||||
*
|
*
|
||||||
* Kiểm tra:
|
* Checks:
|
||||||
* 1. User đã đăng nhập chưa → nếu chưa, redirect /login
|
* 1. Is user logged in → if not, redirect to /login
|
||||||
* 2. User có role admin không → nếu không, redirect /
|
* 2. Does user have admin role → if not, redirect to /
|
||||||
*/
|
*/
|
||||||
const AdminRoute: React.FC<AdminRouteProps> = ({
|
const AdminRoute: React.FC<AdminRouteProps> = ({
|
||||||
children
|
children
|
||||||
@@ -19,7 +19,7 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||||
|
|
||||||
// Đang loading auth state → hiển thị loading
|
// Loading auth state → show loading
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,14 +32,14 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
border-b-2 border-indigo-600 mx-auto"
|
border-b-2 border-indigo-600 mx-auto"
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-gray-600">
|
<p className="mt-4 text-gray-600">
|
||||||
Đang xác thực...
|
Authenticating...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chưa đăng nhập → redirect về /login
|
// Not logged in → redirect to /login
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
@@ -50,7 +50,7 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Đã đăng nhập nhưng không phải admin → redirect về /
|
// Logged in but not admin → redirect to /
|
||||||
const isAdmin = userInfo?.role === 'admin';
|
const isAdmin = userInfo?.role === 'admin';
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ interface ProtectedRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProtectedRoute - Bảo vệ các route yêu cầu authentication
|
* ProtectedRoute - Protects routes that require authentication
|
||||||
*
|
*
|
||||||
* Nếu user chưa đăng nhập, redirect về /login
|
* If user is not logged in, redirect to /login
|
||||||
* và lưu location hiện tại để redirect về sau khi login
|
* and save current location to redirect back after login
|
||||||
*/
|
*/
|
||||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
children
|
children
|
||||||
@@ -18,7 +18,7 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAuthenticated, isLoading } = useAuthStore();
|
const { isAuthenticated, isLoading } = useAuthStore();
|
||||||
|
|
||||||
// Đang loading auth state → hiển thị loading
|
// Loading auth state → show loading
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -31,14 +31,14 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
border-b-2 border-indigo-600 mx-auto"
|
border-b-2 border-indigo-600 mx-auto"
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-gray-600">
|
<p className="mt-4 text-gray-600">
|
||||||
Đang tải...
|
Loading...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chưa đăng nhập → redirect về /login
|
// Not logged in → redirect to /login
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
<h1 className="text-2xl font-bold
|
<h1 className="text-2xl font-bold
|
||||||
text-gray-900 text-center mb-2"
|
text-gray-900 text-center mb-2"
|
||||||
>
|
>
|
||||||
Đã xảy ra lỗi
|
An Error Occurred
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-gray-600 text-center mb-6">
|
<p className="text-gray-600 text-center mb-6">
|
||||||
Xin lỗi, đã có lỗi xảy ra. Vui lòng thử lại
|
Sorry, an error has occurred. Please try again
|
||||||
hoặc liên hệ hỗ trợ nếu vấn đề vẫn tiếp diễn.
|
or contact support if the problem persists.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{process.env.NODE_ENV === 'development' &&
|
{process.env.NODE_ENV === 'development' &&
|
||||||
@@ -96,7 +96,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
text-red-700 cursor-pointer
|
text-red-700 cursor-pointer
|
||||||
hover:text-red-800"
|
hover:text-red-800"
|
||||||
>
|
>
|
||||||
Chi tiết lỗi
|
Error Details
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-2 text-xs
|
<pre className="mt-2 text-xs
|
||||||
text-red-600 overflow-auto
|
text-red-600 overflow-auto
|
||||||
@@ -119,7 +119,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-5 h-5" />
|
<RefreshCw className="w-5 h-5" />
|
||||||
Tải lại trang
|
Reload Page
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = '/'}
|
onClick={() => window.location.href = '/'}
|
||||||
@@ -128,7 +128,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
hover:bg-gray-300 transition-colors
|
hover:bg-gray-300 transition-colors
|
||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
Về trang chủ
|
Go to Home
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
: 'text-gray-700 hover:bg-gray-50'
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Trước
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
@@ -80,7 +80,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
: 'text-gray-700 hover:bg-gray-50'
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Sau
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,10 +88,10 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700">
|
||||||
Hiển thị{' '}
|
Showing{' '}
|
||||||
<span className="font-medium">{startItem}</span> đến{' '}
|
<span className="font-medium">{startItem}</span> to{' '}
|
||||||
<span className="font-medium">{endItem}</span> trong tổng số{' '}
|
<span className="font-medium">{endItem}</span> of{' '}
|
||||||
<span className="font-medium">{totalItems || 0}</span> kết quả
|
<span className="font-medium">{totalItems || 0}</span> results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -53,32 +53,32 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
|
|||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: '/admin/users',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
label: 'Người dùng'
|
label: 'Users'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/rooms',
|
path: '/admin/rooms',
|
||||||
icon: Hotel,
|
icon: Hotel,
|
||||||
label: 'Phòng'
|
label: 'Rooms'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/bookings',
|
path: '/admin/bookings',
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
label: 'Đặt phòng'
|
label: 'Bookings'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/payments',
|
path: '/admin/payments',
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
label: 'Thanh toán'
|
label: 'Payments'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/services',
|
path: '/admin/services',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
label: 'Dịch vụ'
|
label: 'Services'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/promotions',
|
path: '/admin/promotions',
|
||||||
icon: Tag,
|
icon: Tag,
|
||||||
label: 'Khuyến mãi'
|
label: 'Promotions'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/check-in',
|
path: '/admin/check-in',
|
||||||
@@ -93,22 +93,22 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
|
|||||||
{
|
{
|
||||||
path: '/admin/reviews',
|
path: '/admin/reviews',
|
||||||
icon: Star,
|
icon: Star,
|
||||||
label: 'Đánh giá'
|
label: 'Reviews'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/banners',
|
path: '/admin/banners',
|
||||||
icon: Image,
|
icon: Image,
|
||||||
label: 'Banner'
|
label: 'Banners'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/reports',
|
path: '/admin/reports',
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
label: 'Báo cáo'
|
label: 'Reports'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/settings',
|
path: '/admin/settings',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
label: 'Cài đặt'
|
label: 'Settings'
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const BannerCarousel: React.FC<BannerCarouselProps> = ({
|
|||||||
// Default fallback banner if no banners provided
|
// Default fallback banner if no banners provided
|
||||||
const defaultBanner = {
|
const defaultBanner = {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: 'Chào mừng đến với Hotel Booking',
|
title: 'Welcome to Hotel Booking',
|
||||||
image_url: '/images/default-banner.jpg',
|
image_url: '/images/default-banner.jpg',
|
||||||
position: 'home',
|
position: 'home',
|
||||||
display_order: 0,
|
display_order: 0,
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ const FavoriteButton: React.FC<FavoriteButtonProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tooltipText = favorited
|
const tooltipText = favorited
|
||||||
? 'Bỏ yêu thích'
|
? 'Remove from favorites'
|
||||||
: 'Thêm vào yêu thích';
|
: 'Add to favorites';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Example: Cách sử dụng useAuthStore trong components
|
* Example: How to use useAuthStore in components
|
||||||
*
|
*
|
||||||
* File này chỉ để tham khảo, không được sử dụng
|
* This file is for reference only, should not be used
|
||||||
* trong production
|
* in production
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -23,7 +23,7 @@ export const LoginExample = () => {
|
|||||||
await login({ email, password });
|
await login({ email, password });
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý trong store
|
// Error has been handled in store
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -38,7 +38,7 @@ export const LoginExample = () => {
|
|||||||
)}
|
)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Đang xử lý...' : 'Đăng nhập'}
|
{isLoading ? 'Processing...' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -70,7 +70,7 @@ export const RegisterExample = () => {
|
|||||||
onClick={handleRegister}
|
onClick={handleRegister}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Đang xử lý...' : 'Đăng ký'}
|
{isLoading ? 'Processing...' : 'Register'}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -82,13 +82,13 @@ export const UserProfileExample = () => {
|
|||||||
const { userInfo, isAuthenticated } = useAuthStore();
|
const { userInfo, isAuthenticated } = useAuthStore();
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <p>Vui lòng đăng nhập</p>;
|
return <p>Please login</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Thông tin người dùng</h2>
|
<h2>User Information</h2>
|
||||||
<p>Tên: {userInfo?.name}</p>
|
<p>Name: {userInfo?.name}</p>
|
||||||
<p>Email: {userInfo?.email}</p>
|
<p>Email: {userInfo?.email}</p>
|
||||||
<p>Role: {userInfo?.role}</p>
|
<p>Role: {userInfo?.role}</p>
|
||||||
{userInfo?.avatar && (
|
{userInfo?.avatar && (
|
||||||
@@ -118,7 +118,7 @@ export const LogoutButtonExample = () => {
|
|||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Đang xử lý...' : 'Đăng xuất'}
|
{isLoading ? 'Processing...' : 'Logout'}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -134,7 +134,7 @@ export const ForgotPasswordExample = () => {
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await forgotPassword({ email });
|
await forgotPassword({ email });
|
||||||
// Toast sẽ hiển thị thông báo thành công
|
// Toast will display success message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Forgot password failed:', error);
|
console.error('Forgot password failed:', error);
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ export const ForgotPasswordExample = () => {
|
|||||||
}
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Gửi email đặt lại mật khẩu
|
Send password reset email
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -185,7 +185,7 @@ export const ResetPasswordExample = () => {
|
|||||||
}
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Đặt lại mật khẩu
|
Reset Password
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -222,14 +222,14 @@ export const AuthStateCheckExample = () => {
|
|||||||
} = useAuthStore();
|
} = useAuthStore();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <p>Đang tải...</p>;
|
return <p>Loading...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated || !token) {
|
if (!isAuthenticated || !token) {
|
||||||
return <p>Bạn chưa đăng nhập</p>;
|
return <p>You are not logged in</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Bạn đã đăng nhập</p>;
|
return <p>You are logged in</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -250,7 +250,7 @@ export const UpdateUserInfoExample = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={handleUpdateProfile}>
|
<button onClick={handleUpdateProfile}>
|
||||||
Cập nhật thông tin
|
Update Information
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -270,7 +270,7 @@ export const ErrorHandlingExample = () => {
|
|||||||
onClick={clearError}
|
onClick={clearError}
|
||||||
className="mt-2 text-sm text-red-600"
|
className="mt-2 text-sm text-red-600"
|
||||||
>
|
>
|
||||||
Đóng
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ const BookingManagementPage: React.FC = () => {
|
|||||||
onClick={() => setShowDetailModal(false)}
|
onClick={() => setShowDetailModal(false)}
|
||||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
Đóng
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ const CheckInPage: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={actualRoomNumber}
|
value={actualRoomNumber}
|
||||||
onChange={(e) => setActualRoomNumber(e.target.value)}
|
onChange={(e) => setActualRoomNumber(e.target.value)}
|
||||||
placeholder="VD: 101, 202, 305"
|
placeholder="e.g: 101, 202, 305"
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
@@ -271,7 +271,7 @@ const CheckInPage: React.FC = () => {
|
|||||||
value={guest.name}
|
value={guest.name}
|
||||||
onChange={(e) => handleGuestChange(index, 'name', e.target.value)}
|
onChange={(e) => handleGuestChange(index, 'name', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="Nguyễn Văn A"
|
placeholder="John Doe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +236,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Top Rooms */}
|
{/* Top Rooms */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Top phòng được đặt</h2>
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Top Booked Rooms</h2>
|
||||||
{stats?.top_rooms && stats.top_rooms.length > 0 ? (
|
{stats?.top_rooms && stats.top_rooms.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{stats.top_rooms.map((room, index) => (
|
{stats.top_rooms.map((room, index) => (
|
||||||
@@ -246,8 +246,8 @@ const DashboardPage: React.FC = () => {
|
|||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Phòng {room.room_number}</p>
|
<p className="font-medium text-gray-900">Room {room.room_number}</p>
|
||||||
<p className="text-sm text-gray-500">{room.bookings} lượt đặt</p>
|
<p className="text-sm text-gray-500">{room.bookings} bookings</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-green-600">
|
<span className="font-semibold text-green-600">
|
||||||
@@ -257,20 +257,20 @@ const DashboardPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service Usage */}
|
{/* Service Usage */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Dịch vụ được sử dụng</h2>
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Services Used</h2>
|
||||||
{stats?.service_usage && stats.service_usage.length > 0 ? (
|
{stats?.service_usage && stats.service_usage.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{stats.service_usage.map((service) => (
|
{stats.service_usage.map((service) => (
|
||||||
<div key={service.service_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
<div key={service.service_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">{service.service_name}</p>
|
<p className="font-medium text-gray-900">{service.service_name}</p>
|
||||||
<p className="text-sm text-gray-500">{service.usage_count} lần sử dụng</p>
|
<p className="text-sm text-gray-500">{service.usage_count} times used</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-purple-600">
|
<span className="font-semibold text-purple-600">
|
||||||
{formatCurrency(service.total_revenue)}
|
{formatCurrency(service.total_revenue)}
|
||||||
@@ -279,7 +279,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Quản lý khuyến mãi</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Promotion Management</h1>
|
||||||
<p className="text-gray-500 mt-1">Quản lý mã giảm giá và chương trình khuyến mãi</p>
|
<p className="text-gray-500 mt-1">Manage discount codes and promotion programs</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -163,7 +163,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2"
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Thêm khuyến mãi
|
Add Promotion
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Tìm theo code hoặc tên..."
|
placeholder="Search by code or name..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
@@ -187,18 +187,18 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
onChange={(e) => setFilters({ ...filters, type: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, type: e.target.value })}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Tất cả loại</option>
|
<option value="">All Types</option>
|
||||||
<option value="percentage">Phần trăm</option>
|
<option value="percentage">Percentage</option>
|
||||||
<option value="fixed">Số tiền cố định</option>
|
<option value="fixed">Fixed Amount</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="active">Hoạt động</option>
|
<option value="active">Active</option>
|
||||||
<option value="inactive">Ngừng</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,25 +209,25 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Mã code
|
Code
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Tên chương trình
|
Program Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Giá trị
|
Value
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Thời gian
|
Period
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Đã dùng
|
Used
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Trạng thái
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
||||||
Thao tác
|
Actions
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -301,7 +301,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">
|
<h2 className="text-xl font-bold">
|
||||||
{editingPromotion ? 'Cập nhật khuyến mãi' : 'Thêm khuyến mãi mới'}
|
{editingPromotion ? 'Update Promotion' : 'Add New Promotion'}
|
||||||
</h2>
|
</h2>
|
||||||
<button onClick={() => setShowModal(false)}>
|
<button onClick={() => setShowModal(false)}>
|
||||||
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
||||||
@@ -311,27 +311,27 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Mã code <span className="text-red-500">*</span>
|
Code <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.code}
|
value={formData.code}
|
||||||
onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })}
|
onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="VD: SUMMER2024"
|
placeholder="e.g: SUMMER2024"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tên chương trình <span className="text-red-500">*</span>
|
Program Name <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="VD: Giảm giá mùa hè"
|
placeholder="e.g: Summer Sale"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,34 +339,34 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Mô tả
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Mô tả chi tiết về chương trình..."
|
placeholder="Detailed description of the program..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Loại giảm giá <span className="text-red-500">*</span>
|
Discount Type <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.discount_type}
|
value={formData.discount_type}
|
||||||
onChange={(e) => setFormData({ ...formData, discount_type: e.target.value as 'percentage' | 'fixed' })}
|
onChange={(e) => setFormData({ ...formData, discount_type: e.target.value as 'percentage' | 'fixed' })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="percentage">Phần trăm (%)</option>
|
<option value="percentage">Percentage (%)</option>
|
||||||
<option value="fixed">Số tiền cố định (VND)</option>
|
<option value="fixed">Fixed Amount (VND)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Giá trị giảm <span className="text-red-500">*</span>
|
Discount Value <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -383,7 +383,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Giá trị đơn tối thiểu (VND)
|
Minimum Order Value (VND)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -395,7 +395,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Giảm tối đa (VND)
|
Maximum Discount (VND)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -410,7 +410,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Ngày bắt đầu <span className="text-red-500">*</span>
|
Start Date <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
@@ -422,7 +422,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Ngày kết thúc <span className="text-red-500">*</span>
|
End Date <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
@@ -437,7 +437,7 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Giới hạn lượt dùng (0 = không giới hạn)
|
Usage Limit (0 = unlimited)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -449,15 +449,15 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Trạng thái
|
Status
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'active' | 'inactive' })}
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'active' | 'inactive' })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="active">Hoạt động</option>
|
<option value="active">Active</option>
|
||||||
<option value="inactive">Ngừng</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -468,13 +468,13 @@ const PromotionManagementPage: React.FC = () => {
|
|||||||
onClick={() => setShowModal(false)}
|
onClick={() => setShowModal(false)}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Hủy
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{editingPromotion ? 'Cập nhật' : 'Thêm mới'}
|
{editingPromotion ? 'Update' : 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const ReviewManagementPage: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
Phòng {review.room?.room_number} - {review.room?.room_type?.name}
|
Room {review.room?.room_number} - {review.room?.room_type?.name}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
@@ -173,14 +173,14 @@ const ReviewManagementPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleApprove(review.id)}
|
onClick={() => handleApprove(review.id)}
|
||||||
className="text-green-600 hover:text-green-900 mr-3"
|
className="text-green-600 hover:text-green-900 mr-3"
|
||||||
title="Phê duyệt"
|
title="Approve"
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleReject(review.id)}
|
onClick={() => handleReject(review.id)}
|
||||||
className="text-red-600 hover:text-red-900"
|
className="text-red-600 hover:text-red-900"
|
||||||
title="Từ chối"
|
title="Reject"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -155,29 +155,29 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleDeleteImage = async (imageUrl: string) => {
|
const handleDeleteImage = async (imageUrl: string) => {
|
||||||
if (!editingRoom) return;
|
if (!editingRoom) return;
|
||||||
if (!window.confirm('Bạn có chắc muốn xóa ảnh này?')) return;
|
if (!window.confirm('Are you sure you want to delete this image?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
|
await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
|
||||||
data: { imageUrl },
|
data: { imageUrl },
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Xóa ảnh thành công');
|
toast.success('Image deleted successfully');
|
||||||
fetchRooms();
|
fetchRooms();
|
||||||
|
|
||||||
// Refresh editing room data
|
// Refresh editing room data
|
||||||
const response = await roomService.getRoomById(editingRoom.id);
|
const response = await roomService.getRoomById(editingRoom.id);
|
||||||
setEditingRoom(response.data.room);
|
setEditingRoom(response.data.room);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.response?.data?.message || 'Không thể xóa ảnh');
|
toast.error(error.response?.data?.message || 'Unable to delete image');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const badges: Record<string, { bg: string; text: string; label: string }> = {
|
const badges: Record<string, { bg: string; text: string; label: string }> = {
|
||||||
available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Trống' },
|
available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Available' },
|
||||||
occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Đang sử dụng' },
|
occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Occupied' },
|
||||||
maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Bảo trì' },
|
maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Maintenance' },
|
||||||
};
|
};
|
||||||
const badge = badges[status] || badges.available;
|
const badge = badges[status] || badges.available;
|
||||||
return (
|
return (
|
||||||
@@ -196,8 +196,8 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Quản lý phòng</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Room Management</h1>
|
||||||
<p className="text-gray-500 mt-1">Quản lý thông tin phòng khách sạn</p>
|
<p className="text-gray-500 mt-1">Manage hotel room information</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -207,7 +207,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Thêm phòng
|
Add Room
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Tìm kiếm phòng..."
|
placeholder="Search rooms..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
@@ -229,17 +229,17 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="available">Trống</option>
|
<option value="available">Available</option>
|
||||||
<option value="occupied">Đang sử dụng</option>
|
<option value="occupied">Occupied</option>
|
||||||
<option value="maintenance">Bảo trì</option>
|
<option value="maintenance">Maintenance</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={filters.type}
|
value={filters.type}
|
||||||
onChange={(e) => setFilters({ ...filters, type: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, type: e.target.value })}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Tất cả loại phòng</option>
|
<option value="">All Room Types</option>
|
||||||
<option value="1">Standard</option>
|
<option value="1">Standard</option>
|
||||||
<option value="2">Deluxe</option>
|
<option value="2">Deluxe</option>
|
||||||
<option value="3">Suite</option>
|
<option value="3">Suite</option>
|
||||||
@@ -253,25 +253,25 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Số phòng
|
Room Number
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Loại phòng
|
Room Type
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Tầng
|
Floor
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Giá
|
Price
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Trạng thái
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Nổi bật
|
Featured
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Thao tác
|
Actions
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -285,7 +285,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<div className="text-sm text-gray-900">{room.room_type?.name || 'N/A'}</div>
|
<div className="text-sm text-gray-900">{room.room_type?.name || 'N/A'}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-900">Tầng {room.floor}</div>
|
<div className="text-sm text-gray-900">Floor {room.floor}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
@@ -340,7 +340,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">
|
<h2 className="text-xl font-bold">
|
||||||
{editingRoom ? 'Cập nhật phòng' : 'Thêm phòng mới'}
|
{editingRoom ? 'Update Room' : 'Add New Room'}
|
||||||
</h2>
|
</h2>
|
||||||
<button onClick={() => setShowModal(false)}>
|
<button onClick={() => setShowModal(false)}>
|
||||||
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
||||||
@@ -351,7 +351,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Số phòng
|
Room Number
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -363,7 +363,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tầng
|
Floor
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -378,7 +378,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Loại phòng
|
Room Type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.room_type_id}
|
value={formData.room_type_id}
|
||||||
@@ -394,7 +394,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Trạng thái
|
Status
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
@@ -402,9 +402,9 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="available">Trống</option>
|
<option value="available">Available</option>
|
||||||
<option value="occupied">Đang sử dụng</option>
|
<option value="occupied">Occupied</option>
|
||||||
<option value="maintenance">Bảo trì</option>
|
<option value="maintenance">Maintenance</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="featured" className="ml-2 text-sm text-gray-700">
|
<label htmlFor="featured" className="ml-2 text-sm text-gray-700">
|
||||||
Phòng nổi bật
|
Featured Room
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -427,13 +427,13 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
onClick={() => setShowModal(false)}
|
onClick={() => setShowModal(false)}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Hủy
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{editingRoom ? 'Cập nhật' : 'Thêm'}
|
{editingRoom ? 'Update' : 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -443,13 +443,13 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
<ImageIcon className="w-5 h-5" />
|
<ImageIcon className="w-5 h-5" />
|
||||||
Hình ảnh phòng
|
Room Images
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Current Images */}
|
{/* Current Images */}
|
||||||
{editingRoom.room_type?.images && editingRoom.room_type.images.length > 0 && (
|
{editingRoom.room_type?.images && editingRoom.room_type.images.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm text-gray-600 mb-2">Ảnh hiện tại:</p>
|
<p className="text-sm text-gray-600 mb-2">Current Images:</p>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{editingRoom.room_type.images.map((img, index) => (
|
{editingRoom.room_type.images.map((img, index) => (
|
||||||
<div key={index} className="relative group">
|
<div key={index} className="relative group">
|
||||||
@@ -474,7 +474,7 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
{/* Upload New Images */}
|
{/* Upload New Images */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Thêm ảnh mới (tối đa 5 ảnh):
|
Add New Images (max 5 images):
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<input
|
<input
|
||||||
@@ -491,12 +491,12 @@ const RoomManagementPage: React.FC = () => {
|
|||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-4 h-4" />
|
||||||
{uploadingImages ? 'Đang tải...' : 'Upload'}
|
{uploadingImages ? 'Uploading...' : 'Upload'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{selectedFiles.length > 0 && (
|
{selectedFiles.length > 0 && (
|
||||||
<p className="text-sm text-gray-600 mt-2">
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
{selectedFiles.length} file đã chọn
|
{selectedFiles.length} file(s) selected
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
unit: 'lần',
|
unit: 'time',
|
||||||
status: 'active' as 'active' | 'inactive',
|
status: 'active' as 'active' | 'inactive',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,9 +155,9 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="active">Hoạt động</option>
|
<option value="active">Active</option>
|
||||||
<option value="inactive">Tạm dừng</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,22 +167,22 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Tên dịch vụ
|
Service Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Mô tả
|
Description
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Giá
|
Price
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Đơn vị
|
Unit
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
Trạng thái
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
||||||
Thao tác
|
Actions
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -207,7 +207,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-gray-100 text-gray-800'
|
: 'bg-gray-100 text-gray-800'
|
||||||
}`}>
|
}`}>
|
||||||
{service.status === 'active' ? 'Hoạt động' : 'Tạm dừng'}
|
{service.status === 'active' ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
@@ -242,7 +242,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">
|
<h2 className="text-xl font-bold">
|
||||||
{editingService ? 'Cập nhật dịch vụ' : 'Thêm dịch vụ mới'}
|
{editingService ? 'Update Service' : 'Add New Service'}
|
||||||
</h2>
|
</h2>
|
||||||
<button onClick={() => setShowModal(false)}>
|
<button onClick={() => setShowModal(false)}>
|
||||||
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
|
||||||
@@ -251,7 +251,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tên dịch vụ
|
Service Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -263,7 +263,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Mô tả
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
@@ -274,7 +274,7 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Giá
|
Price
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -287,27 +287,27 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Đơn vị
|
Unit
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.unit}
|
value={formData.unit}
|
||||||
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="VD: lần, giờ, ngày..."
|
placeholder="e.g: time, hour, day..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Trạng thái
|
Status
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="active">Hoạt động</option>
|
<option value="active">Active</option>
|
||||||
<option value="inactive">Tạm dừng</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 mt-6">
|
<div className="flex gap-3 mt-6">
|
||||||
@@ -316,13 +316,13 @@ const ServiceManagementPage: React.FC = () => {
|
|||||||
onClick={() => setShowModal(false)}
|
onClick={() => setShowModal(false)}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Hủy
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{editingService ? 'Cập nhật' : 'Thêm'}
|
{editingService ? 'Update' : 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const ForgotPasswordPage: React.FC = () => {
|
|||||||
// Show success state
|
// Show success state
|
||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý trong store
|
// Error has been handled in store
|
||||||
console.error('Forgot password error:', error);
|
console.error('Forgot password error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ const LoginPage: React.FC = () => {
|
|||||||
rememberMe: data.rememberMe,
|
rememberMe: data.rememberMe,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect về trang trước đó hoặc dashboard
|
// Redirect to previous page or dashboard
|
||||||
const from = location.state?.from?.pathname ||
|
const from = location.state?.from?.pathname ||
|
||||||
'/dashboard';
|
'/dashboard';
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý trong store
|
// Error has been handled in store
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const RegisterPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch password để hiển thị password strength
|
// Watch password to display password strength
|
||||||
const password = watch('password');
|
const password = watch('password');
|
||||||
|
|
||||||
// Password strength checker
|
// Password strength checker
|
||||||
@@ -88,7 +88,7 @@ const RegisterPage: React.FC = () => {
|
|||||||
// Redirect to login page
|
// Redirect to login page
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý trong store
|
// Error has been handled in store
|
||||||
console.error('Register error:', error);
|
console.error('Register error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch password để hiển thị password strength
|
// Watch password to display password strength
|
||||||
const password = watch('password');
|
const password = watch('password');
|
||||||
|
|
||||||
// Check if token exists
|
// Check if token exists
|
||||||
@@ -66,11 +66,11 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
if (/[@$!%*?&]/.test(pwd)) strength++;
|
if (/[@$!%*?&]/.test(pwd)) strength++;
|
||||||
|
|
||||||
const labels = [
|
const labels = [
|
||||||
{ label: 'Rất yếu', color: 'bg-red-500' },
|
{ label: 'Very Weak', color: 'bg-red-500' },
|
||||||
{ label: 'Yếu', color: 'bg-orange-500' },
|
{ label: 'Weak', color: 'bg-orange-500' },
|
||||||
{ label: 'Trung bình', color: 'bg-yellow-500' },
|
{ label: 'Medium', color: 'bg-yellow-500' },
|
||||||
{ label: 'Mạnh', color: 'bg-blue-500' },
|
{ label: 'Strong', color: 'bg-blue-500' },
|
||||||
{ label: 'Rất mạnh', color: 'bg-green-500' },
|
{ label: 'Very Strong', color: 'bg-green-500' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return { strength, ...labels[strength] };
|
return { strength, ...labels[strength] };
|
||||||
@@ -100,7 +100,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error đã được xử lý trong store
|
// Error has been handled in store
|
||||||
console.error('Reset password error:', error);
|
console.error('Reset password error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -129,12 +129,12 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-gray-900">
|
<h2 className="text-3xl font-bold text-gray-900">
|
||||||
{isSuccess ? 'Hoàn tất!' : 'Đặt lại mật khẩu'}
|
{isSuccess ? 'Complete!' : 'Reset Password'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-gray-600">
|
<p className="mt-2 text-sm text-gray-600">
|
||||||
{isSuccess
|
{isSuccess
|
||||||
? 'Mật khẩu đã được đặt lại thành công'
|
? 'Password has been reset successfully'
|
||||||
: 'Nhập mật khẩu mới cho tài khoản của bạn'}
|
: 'Enter a new password for your account'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,13 +160,13 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
className="text-xl font-semibold
|
className="text-xl font-semibold
|
||||||
text-gray-900"
|
text-gray-900"
|
||||||
>
|
>
|
||||||
Đặt lại mật khẩu thành công!
|
Password reset successful!
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Mật khẩu của bạn đã được cập nhật.
|
Your password has been updated.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Bạn có thể đăng nhập bằng mật khẩu mới.
|
You can now login with your new password.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
rounded-lg p-4"
|
rounded-lg p-4"
|
||||||
>
|
>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700">
|
||||||
Đang chuyển hướng đến trang đăng nhập...
|
Redirecting to login page...
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex justify-center">
|
<div className="mt-2 flex justify-center">
|
||||||
<Loader2
|
<Loader2
|
||||||
@@ -198,7 +198,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
transition-colors"
|
transition-colors"
|
||||||
>
|
>
|
||||||
<KeyRound className="-ml-1 mr-2 h-5 w-5" />
|
<KeyRound className="-ml-1 mr-2 h-5 w-5" />
|
||||||
Đăng nhập ngay
|
Login Now
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -224,7 +224,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{isReuseError
|
{isReuseError
|
||||||
? 'Mật khẩu mới phải khác mật khẩu cũ'
|
? 'New password must be different from old password'
|
||||||
: error}
|
: error}
|
||||||
</p>
|
</p>
|
||||||
{isTokenError && (
|
{isTokenError && (
|
||||||
@@ -234,7 +234,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
font-medium underline
|
font-medium underline
|
||||||
hover:text-yellow-900"
|
hover:text-yellow-900"
|
||||||
>
|
>
|
||||||
Yêu cầu link mới
|
Request new link
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -248,7 +248,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
className="block text-sm font-medium
|
className="block text-sm font-medium
|
||||||
text-gray-700 mb-2"
|
text-gray-700 mb-2"
|
||||||
>
|
>
|
||||||
Mật khẩu mới
|
New Password
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
@@ -338,23 +338,23 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
met={password.length >= 8}
|
met={password.length >= 8}
|
||||||
text="Ít nhất 8 ký tự"
|
text="At least 8 characters"
|
||||||
/>
|
/>
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
met={/[a-z]/.test(password)}
|
met={/[a-z]/.test(password)}
|
||||||
text="Chữ thường (a-z)"
|
text="Lowercase letter (a-z)"
|
||||||
/>
|
/>
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
met={/[A-Z]/.test(password)}
|
met={/[A-Z]/.test(password)}
|
||||||
text="Chữ hoa (A-Z)"
|
text="Uppercase letter (A-Z)"
|
||||||
/>
|
/>
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
met={/\d/.test(password)}
|
met={/\d/.test(password)}
|
||||||
text="Số (0-9)"
|
text="Number (0-9)"
|
||||||
/>
|
/>
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
met={/[@$!%*?&]/.test(password)}
|
met={/[@$!%*?&]/.test(password)}
|
||||||
text="Ký tự đặc biệt (@$!%*?&)"
|
text="Special character (@$!%*?&)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,7 +368,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
className="block text-sm font-medium
|
className="block text-sm font-medium
|
||||||
text-gray-700 mb-2"
|
text-gray-700 mb-2"
|
||||||
>
|
>
|
||||||
Xác nhận mật khẩu
|
Confirm Password
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
@@ -453,14 +453,14 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
className="animate-spin -ml-1 mr-2
|
className="animate-spin -ml-1 mr-2
|
||||||
h-5 w-5"
|
h-5 w-5"
|
||||||
/>
|
/>
|
||||||
Đang xử lý...
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<KeyRound
|
<KeyRound
|
||||||
className="-ml-1 mr-2 h-5 w-5"
|
className="-ml-1 mr-2 h-5 w-5"
|
||||||
/>
|
/>
|
||||||
Đặt lại mật khẩu
|
Reset Password
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -473,7 +473,7 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
text-indigo-600 hover:text-indigo-500
|
text-indigo-600 hover:text-indigo-500
|
||||||
transition-colors"
|
transition-colors"
|
||||||
>
|
>
|
||||||
Quay lại đăng nhập
|
Back to Login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -492,16 +492,16 @@ const ResetPasswordPage: React.FC = () => {
|
|||||||
gap-2"
|
gap-2"
|
||||||
>
|
>
|
||||||
<Lock className="h-4 w-4" />
|
<Lock className="h-4 w-4" />
|
||||||
Bảo mật
|
Security
|
||||||
</h3>
|
</h3>
|
||||||
<ul
|
<ul
|
||||||
className="text-xs text-gray-600 space-y-1
|
className="text-xs text-gray-600 space-y-1
|
||||||
list-disc list-inside"
|
list-disc list-inside"
|
||||||
>
|
>
|
||||||
<li>Link đặt lại chỉ có hiệu lực trong 1 giờ</li>
|
<li>Reset link is valid for 1 hour only</li>
|
||||||
<li>Mật khẩu được mã hóa an toàn</li>
|
<li>Password is securely encrypted</li>
|
||||||
<li>
|
<li>
|
||||||
Nếu link hết hạn, hãy yêu cầu link mới
|
If the link expires, please request a new link
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
toast.error(
|
toast.error(
|
||||||
'Vui lòng đăng nhập để xem chi tiết đặt phòng'
|
'Please login to view booking details'
|
||||||
);
|
);
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: { from: `/bookings/${id}` }
|
state: { from: `/bookings/${id}` }
|
||||||
@@ -79,14 +79,14 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
setBooking(response.data.booking);
|
setBooking(response.data.booking);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Không thể tải thông tin đặt phòng'
|
'Unable to load booking information'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error fetching booking:', err);
|
console.error('Error fetching booking:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể tải thông tin đặt phòng';
|
'Unable to load booking information';
|
||||||
setError(message);
|
setError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -98,12 +98,12 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
if (!booking) return;
|
if (!booking) return;
|
||||||
|
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
`Bạn có chắc muốn hủy đặt phòng ` +
|
`Are you sure you want to cancel booking ` +
|
||||||
`${booking.booking_number}?\n\n` +
|
`${booking.booking_number}?\n\n` +
|
||||||
`⚠️ Lưu ý:\n` +
|
`⚠️ Note:\n` +
|
||||||
`- Bạn sẽ bị giữ 20% giá trị đơn\n` +
|
`- You will be charged 20% of the order value\n` +
|
||||||
`- 80% còn lại sẽ được hoàn trả\n` +
|
`- The remaining 80% will be refunded\n` +
|
||||||
`- Trạng thái phòng sẽ được cập nhật về "available"`
|
`- Room status will be updated to "available"`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -115,8 +115,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success(
|
toast.success(
|
||||||
`✅ Đã hủy đặt phòng ${booking.booking_number} ` +
|
`✅ Booking ${booking.booking_number} cancelled successfully!`
|
||||||
`thành công!`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
@@ -128,14 +127,14 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
response.message ||
|
response.message ||
|
||||||
'Không thể hủy đặt phòng'
|
'Unable to cancel booking'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error cancelling booking:', err);
|
console.error('Error cancelling booking:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể hủy đặt phòng. Vui lòng thử lại.';
|
'Unable to cancel booking. Please try again.';
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
setCancelling(false);
|
setCancelling(false);
|
||||||
@@ -164,31 +163,31 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
return {
|
return {
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: 'bg-yellow-100 text-yellow-800',
|
color: 'bg-yellow-100 text-yellow-800',
|
||||||
text: 'Chờ xác nhận',
|
text: 'Pending confirmation',
|
||||||
};
|
};
|
||||||
case 'confirmed':
|
case 'confirmed':
|
||||||
return {
|
return {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: 'bg-green-100 text-green-800',
|
color: 'bg-green-100 text-green-800',
|
||||||
text: 'Đã xác nhận',
|
text: 'Confirmed',
|
||||||
};
|
};
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
return {
|
return {
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
color: 'bg-red-100 text-red-800',
|
color: 'bg-red-100 text-red-800',
|
||||||
text: 'Đã hủy',
|
text: 'Cancelled',
|
||||||
};
|
};
|
||||||
case 'checked_in':
|
case 'checked_in':
|
||||||
return {
|
return {
|
||||||
icon: DoorOpen,
|
icon: DoorOpen,
|
||||||
color: 'bg-blue-100 text-blue-800',
|
color: 'bg-blue-100 text-blue-800',
|
||||||
text: 'Đã nhận phòng',
|
text: 'Checked in',
|
||||||
};
|
};
|
||||||
case 'checked_out':
|
case 'checked_out':
|
||||||
return {
|
return {
|
||||||
icon: DoorClosed,
|
icon: DoorClosed,
|
||||||
color: 'bg-gray-100 text-gray-800',
|
color: 'bg-gray-100 text-gray-800',
|
||||||
text: 'Đã trả phòng',
|
text: 'Checked out',
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
@@ -207,7 +206,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loading fullScreen text="Đang tải..." />;
|
return <Loading fullScreen text="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !booking) {
|
if (error || !booking) {
|
||||||
@@ -223,7 +222,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
mx-auto mb-3"
|
mx-auto mb-3"
|
||||||
/>
|
/>
|
||||||
<p className="text-red-700 font-medium mb-4">
|
<p className="text-red-700 font-medium mb-4">
|
||||||
{error || 'Không tìm thấy đặt phòng'}
|
{error || 'Booking not found'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/bookings')}
|
onClick={() => navigate('/bookings')}
|
||||||
@@ -231,7 +230,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
text-white rounded-lg
|
text-white rounded-lg
|
||||||
hover:bg-red-700 transition-colors"
|
hover:bg-red-700 transition-colors"
|
||||||
>
|
>
|
||||||
Quay lại danh sách
|
Back to list
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,7 +254,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
mb-6 transition-colors"
|
mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Quay lại danh sách</span>
|
<span>Back to list</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
@@ -263,7 +262,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
mb-6"
|
mb-6"
|
||||||
>
|
>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
Chi tiết đặt phòng
|
Booking Details
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Status Badge */}
|
{/* Status Badge */}
|
||||||
@@ -284,7 +283,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<p className="text-sm text-indigo-600
|
<p className="text-sm text-indigo-600
|
||||||
font-medium mb-1"
|
font-medium mb-1"
|
||||||
>
|
>
|
||||||
Mã đặt phòng
|
Booking Number
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-bold text-indigo-900
|
<p className="text-2xl font-bold text-indigo-900
|
||||||
font-mono"
|
font-mono"
|
||||||
@@ -300,7 +299,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-bold text-gray-900
|
<h2 className="text-xl font-bold text-gray-900
|
||||||
mb-4"
|
mb-4"
|
||||||
>
|
>
|
||||||
Thông tin phòng
|
Room Information
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{roomType && (
|
{roomType && (
|
||||||
@@ -328,25 +327,25 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<MapPin className="w-4 h-4 inline mr-1" />
|
<MapPin className="w-4 h-4 inline mr-1" />
|
||||||
Phòng {room?.room_number} -
|
Room {room?.room_number} -
|
||||||
Tầng {room?.floor}
|
Floor {room?.floor}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Sức chứa
|
Capacity
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
Tối đa {roomType.capacity} người
|
Max {roomType.capacity} guests
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Giá phòng
|
Room Price
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-indigo-600">
|
<p className="font-medium text-indigo-600">
|
||||||
{formatPrice(roomType.base_price)}/đêm
|
{formatPrice(roomType.base_price)}/night
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,7 +361,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-bold text-gray-900
|
<h2 className="text-xl font-bold text-gray-900
|
||||||
mb-4"
|
mb-4"
|
||||||
>
|
>
|
||||||
Chi tiết đặt phòng
|
Booking Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -373,7 +372,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Calendar className="w-4 h-4 inline mr-1" />
|
<Calendar className="w-4 h-4 inline mr-1" />
|
||||||
Ngày nhận phòng
|
Check-in Date
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{formatDate(booking.check_in_date)}
|
{formatDate(booking.check_in_date)}
|
||||||
@@ -382,7 +381,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Calendar className="w-4 h-4 inline mr-1" />
|
<Calendar className="w-4 h-4 inline mr-1" />
|
||||||
Ngày trả phòng
|
Check-out Date
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{formatDate(booking.check_out_date)}
|
{formatDate(booking.check_out_date)}
|
||||||
@@ -394,10 +393,10 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Users className="w-4 h-4 inline mr-1" />
|
<Users className="w-4 h-4 inline mr-1" />
|
||||||
Số người
|
Number of Guests
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_count} người
|
{booking.guest_count} guest(s)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -406,7 +405,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<FileText className="w-4 h-4 inline mr-1" />
|
<FileText className="w-4 h-4 inline mr-1" />
|
||||||
Ghi chú
|
Notes
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.notes}
|
{booking.notes}
|
||||||
@@ -418,16 +417,16 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<CreditCard className="w-4 h-4 inline mr-1" />
|
<CreditCard className="w-4 h-4 inline mr-1" />
|
||||||
Phương thức thanh toán
|
Payment Method
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900 mb-2">
|
<p className="font-medium text-gray-900 mb-2">
|
||||||
{booking.payment_method === 'cash'
|
{booking.payment_method === 'cash'
|
||||||
? '💵 Thanh toán tại chỗ'
|
? '💵 Pay at hotel'
|
||||||
: '🏦 Chuyển khoản ngân hàng'}
|
: '🏦 Bank transfer'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
Trạng thái:
|
Status:
|
||||||
</span>
|
</span>
|
||||||
<PaymentStatusBadge
|
<PaymentStatusBadge
|
||||||
status={booking.payment_status}
|
status={booking.payment_status}
|
||||||
@@ -444,7 +443,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<span className="text-lg font-semibold
|
<span className="text-lg font-semibold
|
||||||
text-gray-900"
|
text-gray-900"
|
||||||
>
|
>
|
||||||
Tổng thanh toán
|
Total Payment
|
||||||
</span>
|
</span>
|
||||||
<span className="text-2xl font-bold
|
<span className="text-2xl font-bold
|
||||||
text-indigo-600"
|
text-indigo-600"
|
||||||
@@ -464,13 +463,13 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-bold text-gray-900
|
<h2 className="text-xl font-bold text-gray-900
|
||||||
mb-4"
|
mb-4"
|
||||||
>
|
>
|
||||||
Thông tin khách hàng
|
Customer Information
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
<User className="w-4 h-4 inline mr-1" />
|
<User className="w-4 h-4 inline mr-1" />
|
||||||
Họ và tên
|
Full Name
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_info.full_name}
|
{booking.guest_info.full_name}
|
||||||
@@ -488,7 +487,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
<Phone className="w-4 h-4 inline mr-1" />
|
<Phone className="w-4 h-4 inline mr-1" />
|
||||||
Số điện thoại
|
Phone Number
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_info.phone}
|
{booking.guest_info.phone}
|
||||||
@@ -512,25 +511,25 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-bold text-blue-900 mb-2">
|
<h3 className="font-bold text-blue-900 mb-2">
|
||||||
Thông tin chuyển khoản
|
Bank Transfer Information
|
||||||
</h3>
|
</h3>
|
||||||
<div className="bg-white rounded p-4
|
<div className="bg-white rounded p-4
|
||||||
space-y-2 text-sm"
|
space-y-2 text-sm"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<strong>Ngân hàng:</strong>
|
<strong>Bank:</strong>
|
||||||
Vietcombank (VCB)
|
Vietcombank (VCB)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Số tài khoản:</strong>
|
<strong>Account Number:</strong>
|
||||||
0123456789
|
0123456789
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Chủ tài khoản:</strong>
|
<strong>Account Holder:</strong>
|
||||||
KHACH SAN ABC
|
KHACH SAN ABC
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Số tiền:</strong>{' '}
|
<strong>Amount:</strong>{' '}
|
||||||
<span className="text-indigo-600
|
<span className="text-indigo-600
|
||||||
font-bold"
|
font-bold"
|
||||||
>
|
>
|
||||||
@@ -538,7 +537,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Nội dung:</strong>{' '}
|
<strong>Content:</strong>{' '}
|
||||||
<span className="font-mono
|
<span className="font-mono
|
||||||
text-indigo-600"
|
text-indigo-600"
|
||||||
>
|
>
|
||||||
@@ -594,7 +593,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
<CreditCard className="w-5 h-5" />
|
<CreditCard className="w-5 h-5" />
|
||||||
Xác nhận thanh toán
|
Confirm Payment
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -614,12 +613,12 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
<Loader2
|
<Loader2
|
||||||
className="w-5 h-5 animate-spin"
|
className="w-5 h-5 animate-spin"
|
||||||
/>
|
/>
|
||||||
Đang hủy...
|
Cancelling...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<XCircle className="w-5 h-5" />
|
<XCircle className="w-5 h-5" />
|
||||||
Hủy đặt phòng
|
Cancel Booking
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -633,7 +632,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
hover:bg-gray-700 transition-colors
|
hover:bg-gray-700 transition-colors
|
||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
Quay lại danh sách
|
Back to list
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ const BookingListPage: React.FC = () => {
|
|||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||||
Lịch sử đặt phòng
|
Booking History
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Quản lý và theo dõi các đặt phòng của bạn
|
Manage and track your bookings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ const BookingListPage: React.FC = () => {
|
|||||||
<h3 className="text-xl font-semibold
|
<h3 className="text-xl font-semibold
|
||||||
text-gray-800"
|
text-gray-800"
|
||||||
>
|
>
|
||||||
Phòng {booking}01 - Deluxe
|
Room {booking}01 - Deluxe
|
||||||
</h3>
|
</h3>
|
||||||
<span className="px-3 py-1
|
<span className="px-3 py-1
|
||||||
bg-green-100 text-green-800
|
bg-green-100 text-green-800
|
||||||
rounded-full text-sm font-medium"
|
rounded-full text-sm font-medium"
|
||||||
>
|
>
|
||||||
Đã xác nhận
|
Confirmed
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ const BookingListPage: React.FC = () => {
|
|||||||
text-blue-500"
|
text-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Nhận phòng: 15/11/2025
|
Check-in: 15/11/2025
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center
|
<div className="flex items-center
|
||||||
@@ -61,7 +61,7 @@ const BookingListPage: React.FC = () => {
|
|||||||
text-blue-500"
|
text-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Trả phòng: 18/11/2025
|
Check-out: 18/11/2025
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center
|
<div className="flex items-center
|
||||||
@@ -70,7 +70,7 @@ const BookingListPage: React.FC = () => {
|
|||||||
<Clock className="w-4 h-4
|
<Clock className="w-4 h-4
|
||||||
text-blue-500"
|
text-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>3 đêm</span>
|
<span>3 nights</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@ const BookingListPage: React.FC = () => {
|
|||||||
hover:bg-blue-700 transition-colors
|
hover:bg-blue-700 transition-colors
|
||||||
text-sm"
|
text-sm"
|
||||||
>
|
>
|
||||||
Xem chi tiết
|
View Details
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,16 +105,16 @@ const BookingListPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{/* Uncomment khi không có booking
|
{/* Uncomment when there are no bookings
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-500 text-lg">
|
<p className="text-gray-500 text-lg">
|
||||||
Bạn chưa có đặt phòng nào
|
You don't have any bookings yet
|
||||||
</p>
|
</p>
|
||||||
<button className="mt-4 px-6 py-3
|
<button className="mt-4 px-6 py-3
|
||||||
bg-blue-600 text-white rounded-lg
|
bg-blue-600 text-white rounded-lg
|
||||||
hover:bg-blue-700 transition-colors"
|
hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Đặt phòng ngay
|
Book Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ const BookingPage: React.FC = () => {
|
|||||||
border-gray-300 rounded-lg
|
border-gray-300 rounded-lg
|
||||||
focus:ring-2 focus:ring-indigo-500
|
focus:ring-2 focus:ring-indigo-500
|
||||||
focus:border-indigo-500"
|
focus:border-indigo-500"
|
||||||
placeholder="Nguyễn Văn A"
|
placeholder="John Doe"
|
||||||
/>
|
/>
|
||||||
{errors.fullName && (
|
{errors.fullName && (
|
||||||
<p className="text-sm text-red-600 mt-1">
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
|||||||
@@ -82,14 +82,14 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Không thể tải thông tin đặt phòng'
|
'Unable to load booking information'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error fetching booking:', err);
|
console.error('Error fetching booking:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể tải thông tin đặt phòng';
|
'Unable to load booking information';
|
||||||
setError(message);
|
setError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -133,15 +133,15 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'confirmed':
|
case 'confirmed':
|
||||||
return 'Đã xác nhận';
|
return 'Confirmed';
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return 'Chờ xác nhận';
|
return 'Pending confirmation';
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
return 'Đã hủy';
|
return 'Cancelled';
|
||||||
case 'checked_in':
|
case 'checked_in':
|
||||||
return 'Đã nhận phòng';
|
return 'Checked in';
|
||||||
case 'checked_out':
|
case 'checked_out':
|
||||||
return 'Đã trả phòng';
|
return 'Checked out';
|
||||||
default:
|
default:
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -155,10 +155,10 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
booking.booking_number
|
booking.booking_number
|
||||||
);
|
);
|
||||||
setCopiedBookingNumber(true);
|
setCopiedBookingNumber(true);
|
||||||
toast.success('Đã sao chép mã đặt phòng');
|
toast.success('Booking number copied');
|
||||||
setTimeout(() => setCopiedBookingNumber(false), 2000);
|
setTimeout(() => setCopiedBookingNumber(false), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Không thể sao chép');
|
toast.error('Unable to copy');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,13 +170,13 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
|
|
||||||
// Validate file type
|
// Validate file type
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
toast.error('Vui lòng chọn file ảnh');
|
toast.error('Please select an image file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file size (max 5MB)
|
// Validate file size (max 5MB)
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
toast.error('Kích thước ảnh không được vượt quá 5MB');
|
toast.error('Image size must not exceed 5MB');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,8 +208,8 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success(
|
toast.success(
|
||||||
'✅ Đã gửi xác nhận thanh toán thành công! ' +
|
'✅ Payment confirmation sent successfully! ' +
|
||||||
'Chúng tôi sẽ xác nhận trong thời gian sớm nhất.'
|
'We will confirm as soon as possible.'
|
||||||
);
|
);
|
||||||
setReceiptUploaded(true);
|
setReceiptUploaded(true);
|
||||||
|
|
||||||
@@ -228,15 +228,15 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
response.message ||
|
response.message ||
|
||||||
'Không thể xác nhận thanh toán'
|
'Unable to confirm payment'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error uploading receipt:', err);
|
console.error('Error uploading receipt:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể gửi xác nhận thanh toán. ' +
|
'Unable to send payment confirmation. ' +
|
||||||
'Vui lòng thử lại.';
|
'Please try again.';
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingReceipt(false);
|
setUploadingReceipt(false);
|
||||||
@@ -251,7 +251,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loading fullScreen text="Đang tải..." />;
|
return <Loading fullScreen text="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !booking) {
|
if (error || !booking) {
|
||||||
@@ -267,7 +267,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
mx-auto mb-3"
|
mx-auto mb-3"
|
||||||
/>
|
/>
|
||||||
<p className="text-red-700 font-medium mb-4">
|
<p className="text-red-700 font-medium mb-4">
|
||||||
{error || 'Không tìm thấy đặt phòng'}
|
{error || 'Booking not found'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/rooms')}
|
onClick={() => navigate('/rooms')}
|
||||||
@@ -275,7 +275,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
text-white px-3 py-2 rounded-md hover:bg-indigo-700
|
text-white px-3 py-2 rounded-md hover:bg-indigo-700
|
||||||
disabled:bg-gray-400 mb-6 transition-colors"
|
disabled:bg-gray-400 mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
Quay lại danh sách phòng
|
Back to room list
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,11 +307,10 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
className="text-3xl font-bold text-gray-900
|
className="text-3xl font-bold text-gray-900
|
||||||
mb-2"
|
mb-2"
|
||||||
>
|
>
|
||||||
Đặt phòng thành công!
|
Booking Successful!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
Cảm ơn bạn đã đặt phòng tại khách sạn của chúng
|
Thank you for booking with our hotel
|
||||||
tôi
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Booking Number */}
|
{/* Booking Number */}
|
||||||
@@ -322,7 +321,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<span className="text-sm text-indigo-600
|
<span className="text-sm text-indigo-600
|
||||||
font-medium"
|
font-medium"
|
||||||
>
|
>
|
||||||
Mã đặt phòng:
|
Booking Number:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-bold
|
<span className="text-lg font-bold
|
||||||
text-indigo-900"
|
text-indigo-900"
|
||||||
@@ -333,7 +332,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
onClick={copyBookingNumber}
|
onClick={copyBookingNumber}
|
||||||
className="ml-2 p-1 hover:bg-indigo-100
|
className="ml-2 p-1 hover:bg-indigo-100
|
||||||
rounded transition-colors"
|
rounded transition-colors"
|
||||||
title="Sao chép mã"
|
title="Copy booking number"
|
||||||
>
|
>
|
||||||
{copiedBookingNumber ? (
|
{copiedBookingNumber ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
@@ -362,7 +361,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-bold text-gray-900
|
<h2 className="text-xl font-bold text-gray-900
|
||||||
mb-4"
|
mb-4"
|
||||||
>
|
>
|
||||||
Chi tiết đặt phòng
|
Booking Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -389,14 +388,14 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<MapPin className="w-4 h-4
|
<MapPin className="w-4 h-4
|
||||||
inline mr-1"
|
inline mr-1"
|
||||||
/>
|
/>
|
||||||
Phòng {room.room_number} -
|
Room {room.room_number} -
|
||||||
Tầng {room.floor}
|
Floor {room.floor}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-indigo-600
|
<p className="text-indigo-600
|
||||||
font-semibold mt-1"
|
font-semibold mt-1"
|
||||||
>
|
>
|
||||||
{formatPrice(roomType.base_price)}/đêm
|
{formatPrice(roomType.base_price)}/night
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,7 +409,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Calendar className="w-4 h-4 inline mr-1" />
|
<Calendar className="w-4 h-4 inline mr-1" />
|
||||||
Ngày nhận phòng
|
Check-in Date
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{formatDate(booking.check_in_date)}
|
{formatDate(booking.check_in_date)}
|
||||||
@@ -419,7 +418,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Calendar className="w-4 h-4 inline mr-1" />
|
<Calendar className="w-4 h-4 inline mr-1" />
|
||||||
Ngày trả phòng
|
Check-out Date
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{formatDate(booking.check_out_date)}
|
{formatDate(booking.check_out_date)}
|
||||||
@@ -431,10 +430,10 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<Users className="w-4 h-4 inline mr-1" />
|
<Users className="w-4 h-4 inline mr-1" />
|
||||||
Số người
|
Number of Guests
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_count} người
|
{booking.guest_count} guest(s)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -443,7 +442,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<FileText className="w-4 h-4 inline mr-1" />
|
<FileText className="w-4 h-4 inline mr-1" />
|
||||||
Ghi chú
|
Notes
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.notes}
|
{booking.notes}
|
||||||
@@ -455,12 +454,12 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
<CreditCard className="w-4 h-4 inline mr-1" />
|
<CreditCard className="w-4 h-4 inline mr-1" />
|
||||||
Phương thức thanh toán
|
Payment Method
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.payment_method === 'cash'
|
{booking.payment_method === 'cash'
|
||||||
? '💵 Thanh toán tại chỗ'
|
? '💵 Pay at hotel'
|
||||||
: '🏦 Chuyển khoản ngân hàng'}
|
: '🏦 Bank transfer'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -472,7 +471,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<span className="text-lg font-semibold
|
<span className="text-lg font-semibold
|
||||||
text-gray-900"
|
text-gray-900"
|
||||||
>
|
>
|
||||||
Tổng thanh toán
|
Total Payment
|
||||||
</span>
|
</span>
|
||||||
<span className="text-2xl font-bold
|
<span className="text-2xl font-bold
|
||||||
text-indigo-600"
|
text-indigo-600"
|
||||||
@@ -492,13 +491,13 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-bold text-gray-900
|
<h2 className="text-xl font-bold text-gray-900
|
||||||
mb-4"
|
mb-4"
|
||||||
>
|
>
|
||||||
Thông tin khách hàng
|
Customer Information
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
<User className="w-4 h-4 inline mr-1" />
|
<User className="w-4 h-4 inline mr-1" />
|
||||||
Họ và tên
|
Full Name
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_info.full_name}
|
{booking.guest_info.full_name}
|
||||||
@@ -516,7 +515,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
<Phone className="w-4 h-4 inline mr-1" />
|
<Phone className="w-4 h-4 inline mr-1" />
|
||||||
Số điện thoại
|
Phone Number
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{booking.guest_info.phone}
|
{booking.guest_info.phone}
|
||||||
@@ -539,13 +538,13 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-bold text-blue-900 mb-2">
|
<h3 className="font-bold text-blue-900 mb-2">
|
||||||
Hướng dẫn chuyển khoản
|
Bank Transfer Instructions
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2 text-sm
|
<div className="space-y-2 text-sm
|
||||||
text-blue-800"
|
text-blue-800"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Vui lòng chuyển khoản theo thông tin sau:
|
Please transfer according to the following information:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1
|
<div className="grid grid-cols-1
|
||||||
@@ -556,19 +555,19 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
p-4 space-y-2"
|
p-4 space-y-2"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<strong>Ngân hàng:</strong>
|
<strong>Bank:</strong>
|
||||||
Vietcombank (VCB)
|
Vietcombank (VCB)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Số tài khoản:</strong>
|
<strong>Account Number:</strong>
|
||||||
0123456789
|
0123456789
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Chủ tài khoản:</strong>
|
<strong>Account Holder:</strong>
|
||||||
KHACH SAN ABC
|
KHACH SAN ABC
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Số tiền:</strong>{' '}
|
<strong>Amount:</strong>{' '}
|
||||||
<span className="text-indigo-600
|
<span className="text-indigo-600
|
||||||
font-bold"
|
font-bold"
|
||||||
>
|
>
|
||||||
@@ -576,7 +575,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Nội dung:</strong>{' '}
|
<strong>Content:</strong>{' '}
|
||||||
<span className="font-mono
|
<span className="font-mono
|
||||||
text-indigo-600"
|
text-indigo-600"
|
||||||
>
|
>
|
||||||
@@ -594,7 +593,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<p className="text-sm font-medium
|
<p className="text-sm font-medium
|
||||||
text-gray-700 mb-2"
|
text-gray-700 mb-2"
|
||||||
>
|
>
|
||||||
Quét mã QR để chuyển khoản
|
Scan QR code to transfer
|
||||||
</p>
|
</p>
|
||||||
<img
|
<img
|
||||||
src={qrCodeUrl}
|
src={qrCodeUrl}
|
||||||
@@ -605,16 +604,15 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<p className="text-xs text-gray-500
|
<p className="text-xs text-gray-500
|
||||||
mt-2 text-center"
|
mt-2 text-center"
|
||||||
>
|
>
|
||||||
Mã QR đã bao gồm đầy đủ thông tin
|
QR code includes all information
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs italic mt-2">
|
<p className="text-xs italic mt-2">
|
||||||
💡 Lưu ý: Vui lòng ghi đúng mã đặt phòng
|
💡 Note: Please enter the correct booking number
|
||||||
vào nội dung chuyển khoản để chúng tôi
|
in the transfer content so we can confirm your payment.
|
||||||
có thể xác nhận thanh toán của bạn.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,12 +626,11 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<h4 className="font-semibold text-blue-900
|
<h4 className="font-semibold text-blue-900
|
||||||
mb-3"
|
mb-3"
|
||||||
>
|
>
|
||||||
📎 Xác nhận thanh toán
|
📎 Payment Confirmation
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-blue-700 mb-3">
|
<p className="text-sm text-blue-700 mb-3">
|
||||||
Sau khi chuyển khoản, vui lòng tải lên
|
After transferring, please upload
|
||||||
ảnh biên lai để chúng tôi xác nhận nhanh
|
the receipt image so we can confirm faster.
|
||||||
hơn.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -675,7 +672,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<p className="text-xs
|
<p className="text-xs
|
||||||
text-gray-500"
|
text-gray-500"
|
||||||
>
|
>
|
||||||
Click để chọn ảnh khác
|
Click to select another image
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -687,12 +684,12 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
<p className="text-sm
|
<p className="text-sm
|
||||||
text-blue-600 font-medium"
|
text-blue-600 font-medium"
|
||||||
>
|
>
|
||||||
Chọn ảnh biên lai
|
Select receipt image
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs
|
<p className="text-xs
|
||||||
text-gray-500"
|
text-gray-500"
|
||||||
>
|
>
|
||||||
PNG, JPG, JPEG (Tối đa 5MB)
|
PNG, JPG, JPEG (Max 5MB)
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -720,14 +717,14 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
className="w-5 h-5
|
className="w-5 h-5
|
||||||
animate-spin"
|
animate-spin"
|
||||||
/>
|
/>
|
||||||
Đang gửi...
|
Sending...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CheckCircle
|
<CheckCircle
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
/>
|
/>
|
||||||
Xác nhận đã thanh toán
|
Confirm payment completed
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -804,7 +801,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
<ListOrdered className="w-5 h-5" />
|
<ListOrdered className="w-5 h-5" />
|
||||||
Xem đơn của tôi
|
View My Bookings
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
@@ -815,7 +812,7 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
<Home className="w-5 h-5" />
|
<Home className="w-5 h-5" />
|
||||||
Về trang chủ
|
Go to Home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
Dashboard
|
Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Tổng quan hoạt động của bạn
|
Overview of your activity
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h3 className="text-gray-500 text-sm
|
<h3 className="text-gray-500 text-sm
|
||||||
font-medium mb-1"
|
font-medium mb-1"
|
||||||
>
|
>
|
||||||
Tổng đặt phòng
|
Total Bookings
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
45
|
45
|
||||||
@@ -70,7 +70,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h3 className="text-gray-500 text-sm
|
<h3 className="text-gray-500 text-sm
|
||||||
font-medium mb-1"
|
font-medium mb-1"
|
||||||
>
|
>
|
||||||
Tổng chi tiêu
|
Total Spending
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
$12,450
|
$12,450
|
||||||
@@ -95,7 +95,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h3 className="text-gray-500 text-sm
|
<h3 className="text-gray-500 text-sm
|
||||||
font-medium mb-1"
|
font-medium mb-1"
|
||||||
>
|
>
|
||||||
Đang ở
|
Currently Staying
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
2
|
2
|
||||||
@@ -122,7 +122,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h3 className="text-gray-500 text-sm
|
<h3 className="text-gray-500 text-sm
|
||||||
font-medium mb-1"
|
font-medium mb-1"
|
||||||
>
|
>
|
||||||
Điểm thưởng
|
Reward Points
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
1,250
|
1,250
|
||||||
@@ -140,24 +140,24 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-semibold
|
<h2 className="text-xl font-semibold
|
||||||
text-gray-800 mb-4"
|
text-gray-800 mb-4"
|
||||||
>
|
>
|
||||||
Hoạt động gần đây
|
Recent Activity
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
action: 'Đặt phòng',
|
action: 'Booking',
|
||||||
room: 'Phòng 201',
|
room: 'Room 201',
|
||||||
time: '2 giờ trước'
|
time: '2 hours ago'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Check-in',
|
action: 'Check-in',
|
||||||
room: 'Phòng 105',
|
room: 'Room 105',
|
||||||
time: '1 ngày trước'
|
time: '1 day ago'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'Check-out',
|
action: 'Check-out',
|
||||||
room: 'Phòng 302',
|
room: 'Room 302',
|
||||||
time: '3 ngày trước'
|
time: '3 days ago'
|
||||||
},
|
},
|
||||||
].map((activity, index) => (
|
].map((activity, index) => (
|
||||||
<div key={index}
|
<div key={index}
|
||||||
@@ -194,19 +194,19 @@ const DashboardPage: React.FC = () => {
|
|||||||
<h2 className="text-xl font-semibold
|
<h2 className="text-xl font-semibold
|
||||||
text-gray-800 mb-4"
|
text-gray-800 mb-4"
|
||||||
>
|
>
|
||||||
Đặt phòng sắp tới
|
Upcoming Bookings
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
room: 'Phòng 401',
|
room: 'Room 401',
|
||||||
date: '20/11/2025',
|
date: '20/11/2025',
|
||||||
status: 'Đã xác nhận'
|
status: 'Confirmed'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
room: 'Phòng 203',
|
room: 'Room 203',
|
||||||
date: '25/11/2025',
|
date: '25/11/2025',
|
||||||
status: 'Chờ xác nhận'
|
status: 'Pending confirmation'
|
||||||
},
|
},
|
||||||
].map((booking, index) => (
|
].map((booking, index) => (
|
||||||
<div key={index}
|
<div key={index}
|
||||||
@@ -224,7 +224,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span className={`px-3 py-1 rounded-full
|
<span className={`px-3 py-1 rounded-full
|
||||||
text-xs font-medium
|
text-xs font-medium
|
||||||
${booking.status === 'Đã xác nhận'
|
${booking.status === 'Confirmed'
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
// Fetch booking details
|
// Fetch booking details
|
||||||
const bookingResponse = await getBookingById(id);
|
const bookingResponse = await getBookingById(id);
|
||||||
if (!bookingResponse.success || !bookingResponse.data?.booking) {
|
if (!bookingResponse.success || !bookingResponse.data?.booking) {
|
||||||
throw new Error('Không tìm thấy booking');
|
throw new Error('Booking not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingData = bookingResponse.data.booking;
|
const bookingData = bookingResponse.data.booking;
|
||||||
@@ -60,7 +60,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
// Check if booking requires deposit
|
// Check if booking requires deposit
|
||||||
if (!bookingData.requires_deposit) {
|
if (!bookingData.requires_deposit) {
|
||||||
toast.info('Booking này không yêu cầu đặt cọc');
|
toast.info('This booking does not require a deposit');
|
||||||
navigate(`/bookings/${id}`);
|
navigate(`/bookings/${id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error fetching data:', err);
|
console.error('Error fetching data:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message || 'Không thể tải thông tin thanh toán';
|
err.response?.data?.message || 'Unable to load payment information';
|
||||||
setError(message);
|
setError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -160,7 +160,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-3" />
|
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-3" />
|
||||||
<p className="text-red-700 font-medium mb-4">
|
<p className="text-red-700 font-medium mb-4">
|
||||||
{error || 'Không tìm thấy thông tin thanh toán'}
|
{error || 'Payment information not found'}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/bookings"
|
to="/bookings"
|
||||||
@@ -169,7 +169,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
transition-colors"
|
transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
Quay lại danh sách booking
|
Back to booking list
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +191,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
hover:text-gray-900 mb-6 transition-colors"
|
hover:text-gray-900 mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Quay lại chi tiết booking</span>
|
<span>Back to booking details</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Success Header (if paid) */}
|
{/* Success Header (if paid) */}
|
||||||
@@ -209,11 +209,11 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-2xl font-bold text-green-900 mb-1">
|
<h1 className="text-2xl font-bold text-green-900 mb-1">
|
||||||
Đã thanh toán đặt cọc thành công!
|
Deposit payment successful!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-green-700">
|
<p className="text-green-700">
|
||||||
Booking của bạn đã được xác nhận.
|
Your booking has been confirmed.
|
||||||
Phần còn lại thanh toán khi nhận phòng.
|
Remaining amount to be paid at check-in.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,11 +235,11 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-2xl font-bold text-orange-900 mb-1">
|
<h1 className="text-2xl font-bold text-orange-900 mb-1">
|
||||||
Thanh toán tiền đặt cọc
|
Deposit Payment
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-orange-700">
|
<p className="text-orange-700">
|
||||||
Vui lòng thanh toán <strong>20% tiền cọc</strong> để
|
Please pay <strong>20% deposit</strong> to
|
||||||
xác nhận đặt phòng
|
confirm your booking
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,12 +252,12 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
{/* Payment Summary */}
|
{/* Payment Summary */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
||||||
Thông tin thanh toán
|
Payment Information
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Tổng tiền phòng</span>
|
<span className="text-gray-600">Total Room Price</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{formatPrice(booking.total_price)}
|
{formatPrice(booking.total_price)}
|
||||||
</span>
|
</span>
|
||||||
@@ -268,7 +268,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
text-orange-600"
|
text-orange-600"
|
||||||
>
|
>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Tiền cọc cần thanh toán (20%)
|
Deposit Amount to Pay (20%)
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xl font-bold">
|
<span className="text-xl font-bold">
|
||||||
{formatPrice(depositAmount)}
|
{formatPrice(depositAmount)}
|
||||||
@@ -276,7 +276,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between text-sm text-gray-500">
|
<div className="flex justify-between text-sm text-gray-500">
|
||||||
<span>Phần còn lại thanh toán khi nhận phòng</span>
|
<span>Remaining amount to be paid at check-in</span>
|
||||||
<span>{formatPrice(remainingAmount)}</span>
|
<span>{formatPrice(remainingAmount)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,14 +284,14 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
{isDepositPaid && (
|
{isDepositPaid && (
|
||||||
<div className="mt-4 bg-green-50 border border-green-200 rounded p-3">
|
<div className="mt-4 bg-green-50 border border-green-200 rounded p-3">
|
||||||
<p className="text-sm text-green-800">
|
<p className="text-sm text-green-800">
|
||||||
✓ Đã thanh toán tiền cọc vào:{' '}
|
✓ Deposit paid on:{' '}
|
||||||
{depositPayment.payment_date
|
{depositPayment.payment_date
|
||||||
? new Date(depositPayment.payment_date).toLocaleString('en-US')
|
? new Date(depositPayment.payment_date).toLocaleString('en-US')
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
{depositPayment.transaction_id && (
|
{depositPayment.transaction_id && (
|
||||||
<p className="text-xs text-green-700 mt-1">
|
<p className="text-xs text-green-700 mt-1">
|
||||||
Mã giao dịch: {depositPayment.transaction_id}
|
Transaction ID: {depositPayment.transaction_id}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +302,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
{!isDepositPaid && (
|
{!isDepositPaid && (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-6">
|
<h2 className="text-xl font-bold text-gray-900 mb-6">
|
||||||
Chọn phương thức thanh toán
|
Select Payment Method
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Payment Method Buttons */}
|
{/* Payment Method Buttons */}
|
||||||
@@ -331,10 +331,10 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
: 'text-gray-700'
|
: 'text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Chuyển khoản
|
Bank Transfer
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
Chuyển khoản ngân hàng
|
Bank transfer
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
||||||
<Building2 className="w-5 h-5 inline mr-2" />
|
<Building2 className="w-5 h-5 inline mr-2" />
|
||||||
Thông tin chuyển khoản
|
Bank Transfer Information
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -359,16 +359,16 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">Ngân hàng</div>
|
<div className="text-xs text-gray-500">Bank</div>
|
||||||
<div className="font-medium">{bankInfo.bank_name}</div>
|
<div className="font-medium">{bankInfo.bank_name}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(bankInfo.bank_name, 'tên ngân hàng')
|
copyToClipboard(bankInfo.bank_name, 'bank name')
|
||||||
}
|
}
|
||||||
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
||||||
>
|
>
|
||||||
{copiedText === 'tên ngân hàng' ? (
|
{copiedText === 'bank name' ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-4 h-4 text-gray-600" />
|
<Copy className="w-4 h-4 text-gray-600" />
|
||||||
@@ -378,18 +378,18 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">Số tài khoản</div>
|
<div className="text-xs text-gray-500">Account Number</div>
|
||||||
<div className="font-medium font-mono">
|
<div className="font-medium font-mono">
|
||||||
{bankInfo.account_number}
|
{bankInfo.account_number}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(bankInfo.account_number, 'số tài khoản')
|
copyToClipboard(bankInfo.account_number, 'account number')
|
||||||
}
|
}
|
||||||
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
||||||
>
|
>
|
||||||
{copiedText === 'số tài khoản' ? (
|
{copiedText === 'account number' ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-4 h-4 text-gray-600" />
|
<Copy className="w-4 h-4 text-gray-600" />
|
||||||
@@ -399,16 +399,16 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">Chủ tài khoản</div>
|
<div className="text-xs text-gray-500">Account Holder</div>
|
||||||
<div className="font-medium">{bankInfo.account_name}</div>
|
<div className="font-medium">{bankInfo.account_name}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(bankInfo.account_name, 'chủ tài khoản')
|
copyToClipboard(bankInfo.account_name, 'account holder')
|
||||||
}
|
}
|
||||||
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
||||||
>
|
>
|
||||||
{copiedText === 'chủ tài khoản' ? (
|
{copiedText === 'account holder' ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-4 h-4 text-gray-600" />
|
<Copy className="w-4 h-4 text-gray-600" />
|
||||||
@@ -418,18 +418,18 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-orange-50 border border-orange-200 rounded">
|
<div className="flex justify-between items-center p-3 bg-orange-50 border border-orange-200 rounded">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-orange-700">Số tiền</div>
|
<div className="text-xs text-orange-700">Amount</div>
|
||||||
<div className="text-lg font-bold text-orange-600">
|
<div className="text-lg font-bold text-orange-600">
|
||||||
{formatPrice(bankInfo.amount)}
|
{formatPrice(bankInfo.amount)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(bankInfo.amount.toString(), 'số tiền')
|
copyToClipboard(bankInfo.amount.toString(), 'amount')
|
||||||
}
|
}
|
||||||
className="p-2 hover:bg-orange-100 rounded transition-colors"
|
className="p-2 hover:bg-orange-100 rounded transition-colors"
|
||||||
>
|
>
|
||||||
{copiedText === 'số tiền' ? (
|
{copiedText === 'amount' ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-4 h-4 text-orange-600" />
|
<Copy className="w-4 h-4 text-orange-600" />
|
||||||
@@ -439,18 +439,18 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">Nội dung chuyển khoản</div>
|
<div className="text-xs text-gray-500">Transfer Content</div>
|
||||||
<div className="font-medium font-mono text-red-600">
|
<div className="font-medium font-mono text-red-600">
|
||||||
{bankInfo.content}
|
{bankInfo.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(bankInfo.content, 'nội dung')
|
copyToClipboard(bankInfo.content, 'content')
|
||||||
}
|
}
|
||||||
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
className="p-2 hover:bg-gray-200 rounded transition-colors"
|
||||||
>
|
>
|
||||||
{copiedText === 'nội dung' ? (
|
{copiedText === 'content' ? (
|
||||||
<Check className="w-4 h-4 text-green-600" />
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-4 h-4 text-gray-600" />
|
<Copy className="w-4 h-4 text-gray-600" />
|
||||||
@@ -461,8 +461,8 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded p-3">
|
<div className="bg-yellow-50 border border-yellow-200 rounded p-3">
|
||||||
<p className="text-sm text-yellow-800">
|
<p className="text-sm text-yellow-800">
|
||||||
<strong>⚠️ Lưu ý:</strong> Vui lòng nhập đúng nội dung chuyển khoản để
|
<strong>⚠️ Note:</strong> Please enter the correct transfer content so
|
||||||
hệ thống tự động xác nhận thanh toán.
|
the system can automatically confirm the payment.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -478,17 +478,17 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
{notifying ? (
|
{notifying ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
Đang gửi...
|
Sending...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
Tôi đã chuyển khoản
|
I have transferred
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-center text-gray-500 mt-2">
|
<p className="text-xs text-center text-gray-500 mt-2">
|
||||||
Sau khi chuyển khoản, nhấn nút trên để thông báo cho chúng tôi
|
After transferring, click the button above to notify us
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,7 +504,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-8">
|
<div className="bg-white rounded-lg shadow-md p-6 sticky top-8">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4 text-center">
|
<h3 className="text-lg font-bold text-gray-900 mb-4 text-center">
|
||||||
Quét mã QR để thanh toán
|
Scan QR Code to Pay
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg mb-4">
|
<div className="bg-gray-50 p-4 rounded-lg mb-4">
|
||||||
@@ -517,10 +517,10 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Quét mã QR bằng app ngân hàng
|
Scan QR code with your bank app
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Thông tin chuyển khoản đã được điền tự động
|
Transfer information has been automatically filled
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
text-gray-700 rounded-lg transition-colors"
|
text-gray-700 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Tải mã QR
|
Download QR Code
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ const FavoritesPage: React.FC = () => {
|
|||||||
className="text-xl font-bold
|
className="text-xl font-bold
|
||||||
text-gray-900 mb-2"
|
text-gray-900 mb-2"
|
||||||
>
|
>
|
||||||
Vui lòng đăng nhập
|
Please Login
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
Bạn cần đăng nhập để xem danh sách yêu thích
|
You need to login to view your favorites list
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/login"
|
to="/login"
|
||||||
@@ -51,7 +51,7 @@ const FavoritesPage: React.FC = () => {
|
|||||||
hover:bg-indigo-700 transition-colors
|
hover:bg-indigo-700 transition-colors
|
||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
Đăng nhập
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +71,7 @@ const FavoritesPage: React.FC = () => {
|
|||||||
mb-4 transition-colors"
|
mb-4 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Quay lại trang chủ</span>
|
<span>Back to home</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -84,12 +84,12 @@ const FavoritesPage: React.FC = () => {
|
|||||||
className="text-3xl font-bold
|
className="text-3xl font-bold
|
||||||
text-gray-900"
|
text-gray-900"
|
||||||
>
|
>
|
||||||
Danh sách yêu thích
|
Favorites List
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mt-1">
|
<p className="text-gray-600 mt-1">
|
||||||
{favorites.length > 0
|
{favorites.length > 0
|
||||||
? `${favorites.length} phòng`
|
? `${favorites.length} room${favorites.length !== 1 ? 's' : ''}`
|
||||||
: 'Chưa có phòng yêu thích'}
|
: 'No favorite rooms yet'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +126,7 @@ const FavoritesPage: React.FC = () => {
|
|||||||
text-white rounded-lg
|
text-white rounded-lg
|
||||||
hover:bg-red-700 transition-colors"
|
hover:bg-red-700 transition-colors"
|
||||||
>
|
>
|
||||||
Thử lại
|
Try again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -153,16 +153,14 @@ const FavoritesPage: React.FC = () => {
|
|||||||
className="text-2xl font-bold
|
className="text-2xl font-bold
|
||||||
text-gray-900 mb-3"
|
text-gray-900 mb-3"
|
||||||
>
|
>
|
||||||
Chưa có phòng yêu thích
|
No favorite rooms yet
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className="text-gray-600 mb-6
|
className="text-gray-600 mb-6
|
||||||
max-w-md mx-auto"
|
max-w-md mx-auto"
|
||||||
>
|
>
|
||||||
Bạn chưa thêm phòng nào vào danh sách
|
You haven't added any rooms to your favorites list yet. Explore and save the rooms you like!
|
||||||
yêu thích. Hãy khám phá và lưu những
|
|
||||||
phòng bạn thích!
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@@ -172,7 +170,7 @@ const FavoritesPage: React.FC = () => {
|
|||||||
hover:bg-indigo-700 transition-colors
|
hover:bg-indigo-700 transition-colors
|
||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
Khám phá phòng
|
Explore rooms
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const MyBookingsPage: React.FC = () => {
|
|||||||
console.error('Error fetching bookings:', err);
|
console.error('Error fetching bookings:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể tải danh sách đặt phòng';
|
'Unable to load bookings list';
|
||||||
setError(message);
|
setError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
console.error('Error fetching booking:', err);
|
console.error('Error fetching booking:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể tải thông tin đặt phòng';
|
'Unable to load booking information';
|
||||||
setError(message);
|
setError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ const PaymentResultPage: React.FC = () => {
|
|||||||
font-medium"
|
font-medium"
|
||||||
>
|
>
|
||||||
<Home className="w-5 h-5" />
|
<Home className="w-5 h-5" />
|
||||||
Về trang chủ
|
Go to home
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
console.error('Error fetching room:', err);
|
console.error('Error fetching room:', err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
'Không thể tải thông tin phòng';
|
'Unable to load room information';
|
||||||
setError(message);
|
setError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -109,7 +109,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
disabled:bg-gray-400 mb-6 transition-colors"
|
disabled:bg-gray-400 mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Quay lại danh sách phòng</span>
|
<span>Back to room list</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Image Gallery */}
|
{/* Image Gallery */}
|
||||||
@@ -138,14 +138,14 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MapPin className="w-5 h-5" />
|
<MapPin className="w-5 h-5" />
|
||||||
<span>
|
<span>
|
||||||
Phòng {room.room_number} - Tầng {room.floor}
|
Room {room.room_number} - Floor {room.floor}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="w-5 h-5" />
|
<Users className="w-5 h-5" />
|
||||||
<span>
|
<span>
|
||||||
{roomType?.capacity || 0} người
|
{roomType?.capacity || 0} guests
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,10 +176,10 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{room.status === 'available'
|
{room.status === 'available'
|
||||||
? 'Còn phòng'
|
? 'Available'
|
||||||
: room.status === 'occupied'
|
: room.status === 'occupied'
|
||||||
? 'Đã đặt'
|
? 'Booked'
|
||||||
: 'Bảo trì'}
|
: 'Maintenance'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
<h2 className="text-2xl font-bold
|
<h2 className="text-2xl font-bold
|
||||||
text-gray-900 mb-4"
|
text-gray-900 mb-4"
|
||||||
>
|
>
|
||||||
Mô tả phòng
|
Room Description
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p className="text-gray-700 leading-relaxed">
|
||||||
{roomType.description}
|
{roomType.description}
|
||||||
@@ -202,7 +202,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
<h2 className="text-2xl font-bold
|
<h2 className="text-2xl font-bold
|
||||||
text-gray-900 mb-4"
|
text-gray-900 mb-4"
|
||||||
>
|
>
|
||||||
Tiện ích
|
Amenities
|
||||||
</h2>
|
</h2>
|
||||||
<RoomAmenities
|
<RoomAmenities
|
||||||
amenities={
|
amenities={
|
||||||
@@ -223,7 +223,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
<div className="text-3xl font-extrabold text-indigo-600">
|
<div className="text-3xl font-extrabold text-indigo-600">
|
||||||
{formattedPrice}
|
{formattedPrice}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">/ đêm</div>
|
<div className="text-sm text-gray-500">/ night</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,13 +239,13 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
if (room.status !== 'available') e.preventDefault();
|
if (room.status !== 'available') e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{room.status === 'available' ? 'Đặt ngay' : 'Không khả dụng'}
|
{room.status === 'available' ? 'Book Now' : 'Not Available'}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{room.status === 'available' && (
|
{room.status === 'available' && (
|
||||||
<p className="text-sm text-gray-500 text-center mt-3">
|
<p className="text-sm text-gray-500 text-center mt-3">
|
||||||
Không bị tính phí ngay — thanh toán tại khách sạn
|
No immediate charge — pay at the hotel
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -253,15 +253,15 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="text-sm text-gray-700 space-y-2">
|
<div className="text-sm text-gray-700 space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Loại phòng</span>
|
<span>Room Type</span>
|
||||||
<strong>{roomType?.name}</strong>
|
<strong>{roomType?.name}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Số khách</span>
|
<span>Guests</span>
|
||||||
<span>{roomType?.capacity} người</span>
|
<span>{roomType?.capacity} guests</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Số phòng</span>
|
<span>Rooms</span>
|
||||||
<span>1</span>
|
<span>1</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const RoomListPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching rooms:', err);
|
console.error('Error fetching rooms:', err);
|
||||||
setError('Không thể tải danh sách phòng. Vui lòng thử lại.');
|
setError('Unable to load room list. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -76,12 +76,12 @@ const RoomListPage: React.FC = () => {
|
|||||||
disabled:bg-gray-400 mb-6 transition-colors"
|
disabled:bg-gray-400 mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Quay lại trang chủ</span>
|
<span>Back to home</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<h1 className="text-3xl text-center font-bold text-gray-900">
|
<h1 className="text-3xl text-center font-bold text-gray-900">
|
||||||
Danh sách phòng
|
Room List
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ const RoomListPage: React.FC = () => {
|
|||||||
text-white rounded-lg hover:bg-red-700
|
text-white rounded-lg hover:bg-red-700
|
||||||
transition-colors"
|
transition-colors"
|
||||||
>
|
>
|
||||||
Thử lại
|
Try Again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -153,17 +153,17 @@ const RoomListPage: React.FC = () => {
|
|||||||
<h3 className="text-xl font-semibold
|
<h3 className="text-xl font-semibold
|
||||||
text-gray-800 mb-2"
|
text-gray-800 mb-2"
|
||||||
>
|
>
|
||||||
Không tìm thấy phòng phù hợp
|
No matching rooms found
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 mb-6">
|
||||||
Vui lòng thử điều chỉnh bộ lọc hoặc tìm kiếm khác
|
Please try adjusting the filters or search differently
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = '/rooms'}
|
onClick={() => window.location.href = '/rooms'}
|
||||||
className="px-6 py-2 bg-blue-600 text-white
|
className="px-6 py-2 bg-blue-600 text-white
|
||||||
rounded-lg hover:bg-blue-700 transition-colors"
|
rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Xóa bộ lọc
|
Clear Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// Base URL từ environment hoặc mặc định. Ensure it points to the
|
// Base URL from environment or default. Ensure it points to the
|
||||||
// server API root (append '/api' if not provided) so frontend calls
|
// server API root (append '/api' if not provided) so frontend calls
|
||||||
// like '/bookings/me' resolve to e.g. 'http://localhost:3000/api/bookings/me'.
|
// like '/bookings/me' resolve to e.g. 'http://localhost:3000/api/bookings/me'.
|
||||||
const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||||
@@ -12,7 +12,7 @@ const API_BASE_URL = /\/api(\/?$)/i.test(normalized)
|
|||||||
? normalized
|
? normalized
|
||||||
: normalized + '/api';
|
: normalized + '/api';
|
||||||
|
|
||||||
// Tạo axios instance
|
// Create axios instance
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -22,7 +22,7 @@ const apiClient = axios.create({
|
|||||||
withCredentials: true, // Enable sending cookies
|
withCredentials: true, // Enable sending cookies
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor - Thêm token vào header
|
// Request interceptor - Add token to header
|
||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
// Normalize request URL: if a request path accidentally begins
|
// Normalize request URL: if a request path accidentally begins
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ export interface ResetPasswordData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth Service - Xử lý các API calls liên quan
|
* Auth Service - Handles API calls related
|
||||||
* đến authentication
|
* to authentication
|
||||||
*/
|
*/
|
||||||
const authService = {
|
const authService = {
|
||||||
/**
|
/**
|
||||||
* Đăng nhập
|
* Login
|
||||||
*/
|
*/
|
||||||
login: async (
|
login: async (
|
||||||
credentials: LoginCredentials
|
credentials: LoginCredentials
|
||||||
@@ -63,7 +63,7 @@ const authService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Đăng ký tài khoản mới
|
* Register new account
|
||||||
*/
|
*/
|
||||||
register: async (
|
register: async (
|
||||||
data: RegisterData
|
data: RegisterData
|
||||||
@@ -76,7 +76,7 @@ const authService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Đăng xuất
|
* Logout
|
||||||
*/
|
*/
|
||||||
logout: async (): Promise<void> => {
|
logout: async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -87,7 +87,7 @@ const authService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lấy thông tin user hiện tại
|
* Get current user information
|
||||||
*/
|
*/
|
||||||
getProfile: async (): Promise<AuthResponse> => {
|
getProfile: async (): Promise<AuthResponse> => {
|
||||||
const response = await apiClient.get<AuthResponse>(
|
const response = await apiClient.get<AuthResponse>(
|
||||||
@@ -108,7 +108,7 @@ const authService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quên mật khẩu - Gửi email reset
|
* Forgot password - Send reset email
|
||||||
*/
|
*/
|
||||||
forgotPassword: async (
|
forgotPassword: async (
|
||||||
data: ForgotPasswordData
|
data: ForgotPasswordData
|
||||||
@@ -121,7 +121,7 @@ const authService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Đặt lại mật khẩu
|
* Reset password
|
||||||
*/
|
*/
|
||||||
resetPassword: async (
|
resetPassword: async (
|
||||||
data: ResetPasswordData
|
data: ResetPasswordData
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export const checkRoomAvailability = async (
|
|||||||
available: false,
|
available: false,
|
||||||
message:
|
message:
|
||||||
error.response.data.message ||
|
error.response.data.message ||
|
||||||
'Phòng đã được đặt trong thời gian này',
|
'Room already booked during this time',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,60 +3,60 @@ import * as yup from 'yup';
|
|||||||
export const bookingValidationSchema = yup.object().shape({
|
export const bookingValidationSchema = yup.object().shape({
|
||||||
checkInDate: yup
|
checkInDate: yup
|
||||||
.date()
|
.date()
|
||||||
.required('Vui lòng chọn ngày nhận phòng')
|
.required('Please select check-in date')
|
||||||
.min(
|
.min(
|
||||||
new Date(new Date().setHours(0, 0, 0, 0)),
|
new Date(new Date().setHours(0, 0, 0, 0)),
|
||||||
'Ngày nhận phòng không thể là ngày trong quá khứ'
|
'Check-in date cannot be in the past'
|
||||||
)
|
)
|
||||||
.typeError('Ngày nhận phòng không hợp lệ'),
|
.typeError('Invalid check-in date'),
|
||||||
|
|
||||||
checkOutDate: yup
|
checkOutDate: yup
|
||||||
.date()
|
.date()
|
||||||
.required('Vui lòng chọn ngày trả phòng')
|
.required('Please select check-out date')
|
||||||
.min(
|
.min(
|
||||||
yup.ref('checkInDate'),
|
yup.ref('checkInDate'),
|
||||||
'Ngày trả phòng phải sau ngày nhận phòng'
|
'Check-out date must be after check-in date'
|
||||||
)
|
)
|
||||||
.typeError('Ngày trả phòng không hợp lệ'),
|
.typeError('Invalid check-out date'),
|
||||||
|
|
||||||
guestCount: yup
|
guestCount: yup
|
||||||
.number()
|
.number()
|
||||||
.required('Vui lòng nhập số người')
|
.required('Please enter number of guests')
|
||||||
.min(1, 'Số người tối thiểu là 1')
|
.min(1, 'Minimum number of guests is 1')
|
||||||
.max(10, 'Số người tối đa là 10')
|
.max(10, 'Maximum number of guests is 10')
|
||||||
.integer('Số người phải là số nguyên')
|
.integer('Number of guests must be an integer')
|
||||||
.typeError('Số người phải là số'),
|
.typeError('Number of guests must be a number'),
|
||||||
|
|
||||||
notes: yup
|
notes: yup
|
||||||
.string()
|
.string()
|
||||||
.max(500, 'Ghi chú không được quá 500 ký tự')
|
.max(500, 'Notes cannot exceed 500 characters')
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
paymentMethod: yup
|
paymentMethod: yup
|
||||||
.mixed<'cash' | 'bank_transfer'>()
|
.mixed<'cash' | 'bank_transfer'>()
|
||||||
.required('Vui lòng chọn phương thức thanh toán')
|
.required('Please select payment method')
|
||||||
.oneOf(
|
.oneOf(
|
||||||
['cash', 'bank_transfer'],
|
['cash', 'bank_transfer'],
|
||||||
'Phương thức thanh toán không hợp lệ'
|
'Invalid payment method'
|
||||||
),
|
),
|
||||||
|
|
||||||
fullName: yup
|
fullName: yup
|
||||||
.string()
|
.string()
|
||||||
.required('Vui lòng nhập họ tên')
|
.required('Please enter full name')
|
||||||
.min(2, 'Họ tên phải có ít nhất 2 ký tự')
|
.min(2, 'Full name must be at least 2 characters')
|
||||||
.max(100, 'Họ tên không được quá 100 ký tự'),
|
.max(100, 'Full name cannot exceed 100 characters'),
|
||||||
|
|
||||||
email: yup
|
email: yup
|
||||||
.string()
|
.string()
|
||||||
.required('Vui lòng nhập email')
|
.required('Please enter email')
|
||||||
.email('Email không hợp lệ'),
|
.email('Invalid email'),
|
||||||
|
|
||||||
phone: yup
|
phone: yup
|
||||||
.string()
|
.string()
|
||||||
.required('Vui lòng nhập số điện thoại')
|
.required('Please enter phone number')
|
||||||
.matches(
|
.matches(
|
||||||
/^[0-9]{10,11}$/,
|
/^[0-9]{10,11}$/,
|
||||||
'Số điện thoại phải có 10-11 chữ số'
|
'Phone number must have 10-11 digits'
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
# Chức năng 6: Quên Mật Khẩu (Forgot Password) - Hoàn Thành ✅
|
# Function 6: Forgot Password - Completed ✅
|
||||||
|
|
||||||
## 📦 Files Đã Tạo/Cập Nhật
|
## 📦 Files Created/Updated
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
1. **`client/src/pages/auth/ForgotPasswordPage.tsx`** - Component form quên mật khẩu
|
1. **`client/src/pages/auth/ForgotPasswordPage.tsx`** - Forgot password form component
|
||||||
2. **`client/src/pages/auth/index.ts`** - Export ForgotPasswordPage
|
2. **`client/src/pages/auth/index.ts`** - Export ForgotPasswordPage
|
||||||
3. **`client/src/App.tsx`** - Route `/forgot-password`
|
3. **`client/src/App.tsx`** - Route `/forgot-password`
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
4. **`server/src/controllers/authController.js`** - forgotPassword() & resetPassword()
|
4. **`server/src/controllers/authController.js`** - forgotPassword() & resetPassword()
|
||||||
5. **`server/src/routes/authRoutes.js`** - Routes cho forgot/reset password
|
5. **`server/src/routes/authRoutes.js`** - Routes for forgot/reset password
|
||||||
|
|
||||||
## ✨ Tính Năng Chính
|
## ✨ Main Features
|
||||||
|
|
||||||
### 1. Form State (Initial)
|
### 1. Form State (Initial)
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ 🏨 Hotel Icon (Blue) │
|
│ 🏨 Hotel Icon (Blue) │
|
||||||
│ Quên mật khẩu? │
|
│ Forgot password? │
|
||||||
│ Nhập email để nhận link... │
|
│ Enter email to receive link... │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ ┌───────────────────────────────┐ │
|
│ ┌───────────────────────────────┐ │
|
||||||
│ │ Email │ │
|
│ │ Email │ │
|
||||||
│ │ [📧 email@example.com ] │ │
|
│ │ [📧 email@example.com ] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ [📤 Gửi link đặt lại MK] │ │
|
│ │ [📤 Send reset link] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ ← Quay lại đăng nhập │ │
|
│ │ ← Back to login │ │
|
||||||
│ └───────────────────────────────┘ │
|
│ └───────────────────────────────┘ │
|
||||||
│ Chưa có tài khoản? Đăng ký ngay │
|
│ Don't have an account? Sign up now │
|
||||||
└─────────────────────────────────────┘
|
└─────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,23 +37,23 @@
|
|||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ ✅ Success Icon │
|
│ ✅ Success Icon │
|
||||||
│ │
|
│ │
|
||||||
│ Email đã được gửi! │
|
│ Email has been sent! │
|
||||||
│ Chúng tôi đã gửi link đến │
|
│ We have sent a link to │
|
||||||
│ user@example.com │
|
│ user@example.com │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ ℹ️ Lưu ý: │
|
│ ℹ️ Note: │
|
||||||
│ • Link có hiệu lực trong 1 giờ │
|
│ • Link is valid for 1 hour │
|
||||||
│ • Kiểm tra cả thư mục Spam/Junk │
|
│ • Check Spam/Junk folder │
|
||||||
│ • Nếu không nhận được, thử lại │
|
│ • If not received, try again │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ [📧 Gửi lại email] │
|
│ [📧 Resend email] │
|
||||||
│ [← Quay lại đăng nhập] │
|
│ [← Back to login] │
|
||||||
└─────────────────────────────────────┘
|
└─────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Two-State Design Pattern
|
### 3. Two-State Design Pattern
|
||||||
✅ **Form State** - Nhập email
|
✅ **Form State** - Enter email
|
||||||
✅ **Success State** - Hiển thị xác nhận & hướng dẫn
|
✅ **Success State** - Display confirmation & instructions
|
||||||
|
|
||||||
State management:
|
State management:
|
||||||
```typescript
|
```typescript
|
||||||
@@ -61,19 +61,19 @@ const [isSuccess, setIsSuccess] = useState(false);
|
|||||||
const [submittedEmail, setSubmittedEmail] = useState('');
|
const [submittedEmail, setSubmittedEmail] = useState('');
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Features Chi Tiết
|
## 🔧 Detailed Features
|
||||||
|
|
||||||
### 1. Validation (Yup Schema)
|
### 1. Validation (Yup Schema)
|
||||||
```typescript
|
```typescript
|
||||||
email:
|
email:
|
||||||
- Required: "Email là bắt buộc"
|
- Required: "Email is required"
|
||||||
- Valid format: "Email không hợp lệ"
|
- Valid format: "Invalid email format"
|
||||||
- Trim whitespace
|
- Trim whitespace
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Form Field
|
### 2. Form Field
|
||||||
- **Email input** với Mail icon
|
- **Email input** with Mail icon
|
||||||
- Auto-focus khi load page
|
- Auto-focus when page loads
|
||||||
- Validation real-time
|
- Validation real-time
|
||||||
- Error message inline
|
- Error message inline
|
||||||
|
|
||||||
@@ -82,12 +82,12 @@ email:
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
Đang xử lý...
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send />
|
<Send />
|
||||||
Gửi link đặt lại mật khẩu
|
Send reset link
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
```
|
```
|
||||||
@@ -104,15 +104,15 @@ email:
|
|||||||
- Can resend email
|
- Can resend email
|
||||||
|
|
||||||
✅ **Action Buttons**
|
✅ **Action Buttons**
|
||||||
- "Gửi lại email" - Reset to form state
|
- "Resend email" - Reset to form state
|
||||||
- "Quay lại đăng nhập" - Navigate to /login
|
- "Back to login" - Navigate to /login
|
||||||
|
|
||||||
### 5. Help Section
|
### 5. Help Section
|
||||||
```tsx
|
```tsx
|
||||||
<div className="bg-white rounded-lg shadow-sm border">
|
<div className="bg-white rounded-lg shadow-sm border">
|
||||||
<h3>Cần trợ giúp?</h3>
|
<h3>Need help?</h3>
|
||||||
<p>
|
<p>
|
||||||
Liên hệ: support@hotel.com hoặc 1900-xxxx
|
Contact: support@hotel.com or 1900-xxxx
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
@@ -267,7 +267,7 @@ CREATE TABLE password_reset_tokens (
|
|||||||
↓
|
↓
|
||||||
2. Enter email address
|
2. Enter email address
|
||||||
↓
|
↓
|
||||||
3. Click "Gửi link đặt lại mật khẩu"
|
3. Click "Send reset link"
|
||||||
↓
|
↓
|
||||||
4. Frontend validation (Yup)
|
4. Frontend validation (Yup)
|
||||||
↓
|
↓
|
||||||
@@ -286,7 +286,7 @@ CREATE TABLE password_reset_tokens (
|
|||||||
↓
|
↓
|
||||||
10. Click link → /reset-password/:token
|
10. Click link → /reset-password/:token
|
||||||
↓
|
↓
|
||||||
11. Enter new password (Chức năng 7)
|
11. Enter new password (Function 7)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Test Scenarios
|
## 🧪 Test Scenarios
|
||||||
@@ -305,7 +305,7 @@ Expected:
|
|||||||
```
|
```
|
||||||
Input: email = "notanemail"
|
Input: email = "notanemail"
|
||||||
Expected:
|
Expected:
|
||||||
- Validation error: "Email không hợp lệ"
|
- Validation error: "Invalid email format"
|
||||||
- Form not submitted
|
- Form not submitted
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ Expected:
|
|||||||
```
|
```
|
||||||
Input: email = ""
|
Input: email = ""
|
||||||
Expected:
|
Expected:
|
||||||
- Validation error: "Email là bắt buộc"
|
- Validation error: "Email is required"
|
||||||
- Form not submitted
|
- Form not submitted
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -323,12 +323,12 @@ Action: Submit form
|
|||||||
Expected:
|
Expected:
|
||||||
- Button disabled
|
- Button disabled
|
||||||
- Spinner shows
|
- Spinner shows
|
||||||
- Text: "Đang xử lý..."
|
- Text: "Processing..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Case 5: Resend email
|
### Test Case 5: Resend email
|
||||||
```
|
```
|
||||||
Action: Click "Gửi lại email" in success state
|
Action: Click "Resend email" in success state
|
||||||
Expected:
|
Expected:
|
||||||
- Return to form state
|
- Return to form state
|
||||||
- Email field cleared
|
- Email field cleared
|
||||||
@@ -337,7 +337,7 @@ Expected:
|
|||||||
|
|
||||||
### Test Case 6: Back to login
|
### Test Case 6: Back to login
|
||||||
```
|
```
|
||||||
Action: Click "Quay lại đăng nhập"
|
Action: Click "Back to login"
|
||||||
Expected:
|
Expected:
|
||||||
- Navigate to /login
|
- Navigate to /login
|
||||||
```
|
```
|
||||||
@@ -482,7 +482,7 @@ never expose raw reset tokens in logs. To enable email sending:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** ✅ Chức năng 6 hoàn thành
|
**Status:** ✅ Function 6 completed
|
||||||
**Next:** Chức năng 7 - Reset Password (form to change password with token)
|
**Next:** Function 7 - Reset Password (form to change password with token)
|
||||||
**Test URL:** http://localhost:5173/forgot-password
|
**Test URL:** http://localhost:5173/forgot-password
|
||||||
**API:** POST /api/auth/forgot-password
|
**API:** POST /api/auth/forgot-password
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
# Layout Components - Chức năng 1
|
# Layout Components - Function 1
|
||||||
|
|
||||||
## Tổng quan
|
## Overview
|
||||||
Đã triển khai thành công **Chức năng 1: Layout cơ bản** bao gồm:
|
Successfully implemented **Function 1: Basic Layout** including:
|
||||||
|
|
||||||
### Components đã tạo
|
### Components Created
|
||||||
|
|
||||||
#### 1. **Header** (`src/components/layout/Header.tsx`)
|
#### 1. **Header** (`src/components/layout/Header.tsx`)
|
||||||
- Logo và tên ứng dụng
|
- Logo and application name
|
||||||
- Sticky header với shadow
|
- Sticky header with shadow
|
||||||
- Responsive design
|
- Responsive design
|
||||||
- Links cơ bản (Trang chủ, Phòng, Đặt phòng)
|
- Basic links (Home, Rooms, Bookings)
|
||||||
|
|
||||||
#### 2. **Footer** (`src/components/layout/Footer.tsx`)
|
#### 2. **Footer** (`src/components/layout/Footer.tsx`)
|
||||||
- Thông tin công ty
|
- Company information
|
||||||
- Quick links (Liên kết nhanh)
|
- Quick links
|
||||||
- Support links (Hỗ trợ)
|
- Support links
|
||||||
- Contact info (Thông tin liên hệ)
|
- Contact information
|
||||||
- Social media icons
|
- Social media icons
|
||||||
- Copyright info
|
- Copyright information
|
||||||
- Fully responsive (4 columns → 2 → 1)
|
- Fully responsive (4 columns → 2 → 1)
|
||||||
|
|
||||||
#### 3. **Navbar** (`src/components/layout/Navbar.tsx`)
|
#### 3. **Navbar** (`src/components/layout/Navbar.tsx`)
|
||||||
- **Trạng thái chưa đăng nhập**:
|
- **Not logged in state**:
|
||||||
- Hiển thị nút "Đăng nhập" và "Đăng ký"
|
- Display "Login" and "Register" buttons
|
||||||
- **Trạng thái đã đăng nhập**:
|
- **Logged in state**:
|
||||||
- Hiển thị avatar/tên user
|
- Display avatar/user name
|
||||||
- Dropdown menu với "Hồ sơ", "Quản trị" (admin), "Đăng xuất"
|
- Dropdown menu with "Profile", "Admin" (admin), "Logout"
|
||||||
- Mobile menu với hamburger icon
|
- Mobile menu with hamburger icon
|
||||||
- Responsive cho desktop và mobile
|
- Responsive for desktop and mobile
|
||||||
|
|
||||||
#### 4. **SidebarAdmin** (`src/components/layout/SidebarAdmin.tsx`)
|
#### 4. **SidebarAdmin** (`src/components/layout/SidebarAdmin.tsx`)
|
||||||
- Chỉ hiển thị cho role = "admin"
|
- Only displays for role = "admin"
|
||||||
- Collapsible sidebar (mở/đóng)
|
- Collapsible sidebar (open/close)
|
||||||
- Menu items: Dashboard, Users, Rooms, Bookings, Payments, Services, Promotions, Banners, Reports, Settings
|
- Menu items: Dashboard, Users, Rooms, Bookings, Payments, Services, Promotions, Banners, Reports, Settings
|
||||||
- Active state highlighting
|
- Active state highlighting
|
||||||
- Responsive design
|
- Responsive design
|
||||||
|
|
||||||
#### 5. **LayoutMain** (`src/components/layout/LayoutMain.tsx`)
|
#### 5. **LayoutMain** (`src/components/layout/LayoutMain.tsx`)
|
||||||
- Tích hợp Header, Navbar, Footer
|
- Integrates Header, Navbar, Footer
|
||||||
- Sử dụng `<Outlet />` để render nội dung động
|
- Uses `<Outlet />` to render dynamic content
|
||||||
- Props: `isAuthenticated`, `userInfo`, `onLogout`
|
- Props: `isAuthenticated`, `userInfo`, `onLogout`
|
||||||
- Min-height 100vh với flex layout
|
- Min-height 100vh with flex layout
|
||||||
|
|
||||||
### Cấu trúc thư mục
|
### Directory Structure
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── components/
|
├── components/
|
||||||
@@ -62,13 +62,13 @@ src/
|
|||||||
└── main.tsx
|
└── main.tsx
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cách sử dụng
|
### Usage
|
||||||
|
|
||||||
#### 1. Import Layout vào App
|
#### 1. Import Layout into App
|
||||||
```tsx
|
```tsx
|
||||||
import LayoutMain from './components/layout/LayoutMain';
|
import LayoutMain from './components/layout/LayoutMain';
|
||||||
|
|
||||||
// Trong Routes
|
// In Routes
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -80,11 +80,11 @@ import LayoutMain from './components/layout/LayoutMain';
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
{/* Các route con khác */}
|
{/* Other child routes */}
|
||||||
</Route>
|
</Route>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Sử dụng SidebarAdmin cho trang Admin
|
#### 2. Use SidebarAdmin for Admin Pages
|
||||||
```tsx
|
```tsx
|
||||||
import SidebarAdmin from '../components/layout/SidebarAdmin';
|
import SidebarAdmin from '../components/layout/SidebarAdmin';
|
||||||
|
|
||||||
@@ -98,78 +98,78 @@ const AdminLayout = () => (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tính năng đã hoàn thành ✅
|
### Completed Features ✅
|
||||||
|
|
||||||
- [x] Tạo thư mục `src/components/layout/`
|
- [x] Create `src/components/layout/` directory
|
||||||
- [x] Header.tsx với logo và navigation
|
- [x] Header.tsx with logo and navigation
|
||||||
- [x] Footer.tsx với thông tin đầy đủ
|
- [x] Footer.tsx with complete information
|
||||||
- [x] Navbar.tsx với logic đăng nhập/đăng xuất động
|
- [x] Navbar.tsx with dynamic login/logout logic
|
||||||
- [x] SidebarAdmin.tsx chỉ hiển thị với role admin
|
- [x] SidebarAdmin.tsx only displays with admin role
|
||||||
- [x] LayoutMain.tsx sử dụng `<Outlet />`
|
- [x] LayoutMain.tsx uses `<Outlet />`
|
||||||
- [x] Navbar thay đổi theo trạng thái đăng nhập
|
- [x] Navbar changes based on login state
|
||||||
- [x] Giao diện responsive, tương thích desktop/mobile
|
- [x] Responsive interface, compatible with desktop/mobile
|
||||||
- [x] Tích hợp TailwindCSS cho styling
|
- [x] TailwindCSS integration for styling
|
||||||
- [x] Export tất cả components qua index.ts
|
- [x] Export all components via index.ts
|
||||||
|
|
||||||
### Demo Routes đã tạo
|
### Demo Routes Created
|
||||||
|
|
||||||
**Public Routes** (với LayoutMain):
|
**Public Routes** (with LayoutMain):
|
||||||
- `/` - Trang chủ
|
- `/` - Home
|
||||||
- `/rooms` - Danh sách phòng
|
- `/rooms` - Room list
|
||||||
- `/bookings` - Đặt phòng
|
- `/bookings` - Bookings
|
||||||
- `/about` - Giới thiệu
|
- `/about` - About
|
||||||
|
|
||||||
**Auth Routes** (không có layout):
|
**Auth Routes** (no layout):
|
||||||
- `/login` - Đăng nhập
|
- `/login` - Login
|
||||||
- `/register` - Đăng ký
|
- `/register` - Register
|
||||||
- `/forgot-password` - Quên mật khẩu
|
- `/forgot-password` - Forgot password
|
||||||
|
|
||||||
**Admin Routes** (với SidebarAdmin):
|
**Admin Routes** (with SidebarAdmin):
|
||||||
- `/admin/dashboard` - Dashboard
|
- `/admin/dashboard` - Dashboard
|
||||||
- `/admin/users` - Quản lý người dùng
|
- `/admin/users` - User Management
|
||||||
- `/admin/rooms` - Quản lý phòng
|
- `/admin/rooms` - Room Management
|
||||||
- `/admin/bookings` - Quản lý đặt phòng
|
- `/admin/bookings` - Booking Management
|
||||||
- `/admin/payments` - Quản lý thanh toán
|
- `/admin/payments` - Payment Management
|
||||||
- `/admin/services` - Quản lý dịch vụ
|
- `/admin/services` - Service Management
|
||||||
- `/admin/promotions` - Quản lý khuyến mãi
|
- `/admin/promotions` - Promotion Management
|
||||||
- `/admin/banners` - Quản lý banner
|
- `/admin/banners` - Banner Management
|
||||||
|
|
||||||
### Chạy ứng dụng
|
### Run Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Di chuyển vào thư mục client
|
# Navigate to client directory
|
||||||
cd client
|
cd client
|
||||||
|
|
||||||
# Cài đặt dependencies (nếu chưa cài)
|
# Install dependencies (if not installed)
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Chạy development server
|
# Run development server
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Mở trình duyệt tại: http://localhost:5173
|
# Open browser at: http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
### Các bước tiếp theo
|
### Next Steps
|
||||||
|
|
||||||
**Chức năng 2**: Cấu hình Routing (react-router-dom)
|
**Function 2**: Routing Configuration (react-router-dom)
|
||||||
- ProtectedRoute component
|
- ProtectedRoute component
|
||||||
- AdminRoute component
|
- AdminRoute component
|
||||||
- Redirect logic
|
- Redirect logic
|
||||||
|
|
||||||
**Chức năng 3**: useAuthStore (Zustand Store)
|
**Function 3**: useAuthStore (Zustand Store)
|
||||||
- Quản lý authentication state
|
- Manage authentication state
|
||||||
- Login/Logout functions
|
- Login/Logout functions
|
||||||
- Persist state trong localStorage
|
- Persist state in localStorage
|
||||||
|
|
||||||
**Chức năng 4-8**: Auth Forms
|
**Function 4-8**: Auth Forms
|
||||||
- LoginPage
|
- LoginPage
|
||||||
- RegisterPage
|
- RegisterPage
|
||||||
- ForgotPasswordPage
|
- ForgotPasswordPage
|
||||||
- ResetPasswordPage
|
- ResetPasswordPage
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- Layout components được thiết kế để tái sử dụng
|
- Layout components designed for reusability
|
||||||
- Props-based design cho flexibility
|
- Props-based design for flexibility
|
||||||
- Sẵn sàng tích hợp với Zustand store
|
- Ready to integrate with Zustand store
|
||||||
- Tailwind classes tuân thủ 80 ký tự/dòng
|
- Tailwind classes follow 80 characters/line
|
||||||
- Icons sử dụng lucide-react (đã có trong dependencies)
|
- Icons use lucide-react (already in dependencies)
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
# Chức năng 4: Form Đăng Nhập - Hướng Dẫn Sử Dụng
|
# Function 4: Login Form - Usage Guide
|
||||||
|
|
||||||
## 📋 Tổng Quan
|
## 📋 Overview
|
||||||
|
|
||||||
Form đăng nhập đã được triển khai đầy đủ với:
|
Login form has been fully implemented with:
|
||||||
- ✅ Validation form bằng React Hook Form + Yup
|
- ✅ Form validation with React Hook Form + Yup
|
||||||
- ✅ Hiển thị/ẩn mật khẩu
|
- ✅ Show/hide password
|
||||||
- ✅ Checkbox "Nhớ đăng nhập" (7 ngày)
|
- ✅ "Remember me" checkbox (7 days)
|
||||||
- ✅ Loading state trong quá trình đăng nhập
|
- ✅ Loading state during login process
|
||||||
- ✅ Hiển thị lỗi từ server
|
- ✅ Display errors from server
|
||||||
- ✅ Redirect sau khi đăng nhập thành công
|
- ✅ Redirect after successful login
|
||||||
- ✅ UI đẹp với Tailwind CSS và Lucide Icons
|
- ✅ Beautiful UI with Tailwind CSS and Lucide Icons
|
||||||
- ✅ Responsive design
|
- ✅ Responsive design
|
||||||
|
|
||||||
## 🗂️ Các File Đã Tạo/Cập Nhật
|
## 🗂️ Files Created/Updated
|
||||||
|
|
||||||
### 1. **LoginPage.tsx** - Component form đăng nhập
|
### 1. **LoginPage.tsx** - Login form component
|
||||||
**Đường dẫn:** `client/src/pages/auth/LoginPage.tsx`
|
**Path:** `client/src/pages/auth/LoginPage.tsx`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Các tính năng chính:
|
// Main features:
|
||||||
- React Hook Form với Yup validation
|
- React Hook Form with Yup validation
|
||||||
- Show/hide password toggle
|
- Show/hide password toggle
|
||||||
- Remember me checkbox
|
- Remember me checkbox
|
||||||
- Loading state với spinner
|
- Loading state with spinner
|
||||||
- Error handling
|
- Error handling
|
||||||
- Redirect với location state
|
- Redirect with location state
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. **index.ts** - Export module
|
### 2. **index.ts** - Export module
|
||||||
**Đường dẫn:** `client/src/pages/auth/index.ts`
|
**Path:** `client/src/pages/auth/index.ts`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export { default as LoginPage } from './LoginPage';
|
export { default as LoginPage } from './LoginPage';
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. **App.tsx** - Đã cập nhật routing
|
### 3. **App.tsx** - Routing updated
|
||||||
**Đường dẫn:** `client/src/App.tsx`
|
**Path:** `client/src/App.tsx`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Đã thêm:
|
// Added:
|
||||||
import { LoginPage } from './pages/auth';
|
import { LoginPage } from './pages/auth';
|
||||||
|
|
||||||
// Route:
|
// Route:
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Cấu Trúc UI
|
## 🎨 UI Structure
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ 🏨 Hotel Icon │
|
│ 🏨 Hotel Icon │
|
||||||
│ Đăng nhập │
|
│ Login │
|
||||||
│ Chào mừng bạn trở lại... │
|
│ Welcome back... │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ ┌───────────────────────────────┐ │
|
│ ┌───────────────────────────────┐ │
|
||||||
│ │ [Error message if any] │ │
|
│ │ [Error message if any] │ │
|
||||||
@@ -60,72 +60,72 @@ import { LoginPage } from './pages/auth';
|
|||||||
│ │ Email │ │
|
│ │ Email │ │
|
||||||
│ │ [📧 email@example.com ] │ │
|
│ │ [📧 email@example.com ] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Mật khẩu │ │
|
│ │ Password │ │
|
||||||
│ │ [🔒 •••••••• 👁️] │ │
|
│ │ [🔒 •••••••• 👁️] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ ☑️ Nhớ đăng nhập │ │
|
│ │ ☑️ Remember me │ │
|
||||||
│ │ Quên mật khẩu? → │ │
|
│ │ Forgot password? → │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ [🔐 Đăng nhập] │ │
|
│ │ [🔐 Login] │ │
|
||||||
│ └───────────────────────────────┘ │
|
│ └───────────────────────────────┘ │
|
||||||
│ Chưa có tài khoản? Đăng ký ngay │
|
│ Don't have an account? Sign up now │
|
||||||
│ │
|
│ │
|
||||||
│ Điều khoản & Chính sách bảo mật │
|
│ Terms & Privacy Policy │
|
||||||
└─────────────────────────────────────┘
|
└─────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Cách Sử Dụng
|
## 🔧 Usage
|
||||||
|
|
||||||
### 1. Truy Cập Form
|
### 1. Access Form
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# URL
|
# URL
|
||||||
http://localhost:5173/login
|
http://localhost:5173/login
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Các Trường Trong Form
|
### 2. Form Fields
|
||||||
|
|
||||||
| Trường | Type | Bắt buộc | Validation |
|
| Field | Type | Required | Validation |
|
||||||
|--------|------|----------|------------|
|
|--------|------|----------|------------|
|
||||||
| Email | text | ✅ | Email hợp lệ |
|
| Email | text | ✅ | Valid email |
|
||||||
| Password | password | ✅ | Min 8 ký tự |
|
| Password | password | ✅ | Min 8 characters |
|
||||||
| Remember Me | checkbox | ❌ | Boolean |
|
| Remember Me | checkbox | ❌ | Boolean |
|
||||||
|
|
||||||
### 3. Validation Rules
|
### 3. Validation Rules
|
||||||
|
|
||||||
**Email:**
|
**Email:**
|
||||||
```typescript
|
```typescript
|
||||||
- Required: "Email là bắt buộc"
|
- Required: "Email is required"
|
||||||
- Valid email format: "Email không hợp lệ"
|
- Valid email format: "Invalid email format"
|
||||||
- Trim whitespace
|
- Trim whitespace
|
||||||
```
|
```
|
||||||
|
|
||||||
**Password:**
|
**Password:**
|
||||||
```typescript
|
```typescript
|
||||||
- Required: "Mật khẩu là bắt buộc"
|
- Required: "Password is required"
|
||||||
- Min 8 characters: "Mật khẩu phải có ít nhất 8 ký tự"
|
- Min 8 characters: "Password must be at least 8 characters"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Luồng Đăng Nhập
|
### 4. Login Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
1. User nhập email + password
|
1. User enters email + password
|
||||||
2. Click "Đăng nhập"
|
2. Click "Login"
|
||||||
3. Validation form (client-side)
|
3. Form validation (client-side)
|
||||||
4. Nếu valid:
|
4. If valid:
|
||||||
- Button disabled + hiển thị loading
|
- Button disabled + show loading
|
||||||
- Gọi useAuthStore.login()
|
- Call useAuthStore.login()
|
||||||
- API POST /api/auth/login
|
- API POST /api/auth/login
|
||||||
5. Nếu thành công:
|
5. If successful:
|
||||||
- Lưu token vào localStorage
|
- Save token to localStorage
|
||||||
- Update Zustand state
|
- Update Zustand state
|
||||||
- Redirect đến /dashboard
|
- Redirect to /dashboard
|
||||||
6. Nếu lỗi:
|
6. If error:
|
||||||
- Hiển thị error message
|
- Display error message
|
||||||
- Button enabled lại
|
- Button enabled again
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Tính Năng Chính
|
## 🎯 Main Features
|
||||||
|
|
||||||
### 1. Show/Hide Password
|
### 1. Show/Hide Password
|
||||||
|
|
||||||
@@ -141,33 +141,33 @@ const [showPassword, setShowPassword] = useState(false);
|
|||||||
<input type={showPassword ? 'text' : 'password'} />
|
<input type={showPassword ? 'text' : 'password'} />
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Remember Me (7 ngày)
|
### 2. Remember Me (7 days)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Checkbox
|
// Checkbox
|
||||||
<input {...register('rememberMe')} type="checkbox" />
|
<input {...register('rememberMe')} type="checkbox" />
|
||||||
|
|
||||||
// Logic trong authService.login()
|
// Logic in authService.login()
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
// Token sẽ được lưu trong localStorage
|
// Token will be saved in localStorage
|
||||||
// và không bị xóa khi đóng trình duyệt
|
// and won't be deleted when closing browser
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Loading State
|
### 3. Loading State
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Button disabled khi đang loading
|
// Button disabled when loading
|
||||||
<button disabled={isLoading}>
|
<button disabled={isLoading}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
Đang xử lý...
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LogIn />
|
<LogIn />
|
||||||
Đăng nhập
|
Login
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -176,10 +176,10 @@ if (rememberMe) {
|
|||||||
### 4. Error Handling
|
### 4. Error Handling
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Error từ Zustand store
|
// Error from Zustand store
|
||||||
const { error } = useAuthStore();
|
const { error } = useAuthStore();
|
||||||
|
|
||||||
// Hiển thị error message
|
// Display error message
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200">
|
<div className="bg-red-50 border border-red-200">
|
||||||
{error}
|
{error}
|
||||||
@@ -190,20 +190,20 @@ const { error } = useAuthStore();
|
|||||||
### 5. Redirect Logic
|
### 5. Redirect Logic
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Lấy location state từ ProtectedRoute
|
// Get location state from ProtectedRoute
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Redirect về trang trước đó hoặc dashboard
|
// Redirect to previous page or dashboard
|
||||||
const from = location.state?.from?.pathname || '/dashboard';
|
const from = location.state?.from?.pathname || '/dashboard';
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔗 Integration với Zustand Store
|
## 🔗 Integration with Zustand Store
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Hook usage
|
// Hook usage
|
||||||
const {
|
const {
|
||||||
login, // Function để login
|
login, // Function to login
|
||||||
isLoading, // Loading state
|
isLoading, // Loading state
|
||||||
error, // Error message
|
error, // Error message
|
||||||
clearError // Clear error
|
clearError // Clear error
|
||||||
@@ -217,7 +217,7 @@ await login({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Styling với Tailwind
|
## 🎨 Styling with Tailwind
|
||||||
|
|
||||||
### Color Scheme
|
### Color Scheme
|
||||||
```
|
```
|
||||||
@@ -232,7 +232,7 @@ await login({
|
|||||||
// Container
|
// Container
|
||||||
className="max-w-md w-full" // Max width 28rem
|
className="max-w-md w-full" // Max width 28rem
|
||||||
|
|
||||||
// Grid (nếu có)
|
// Grid (if any)
|
||||||
className="grid grid-cols-1 md:grid-cols-2"
|
className="grid grid-cols-1 md:grid-cols-2"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -242,20 +242,20 @@ className="grid grid-cols-1 md:grid-cols-2"
|
|||||||
|
|
||||||
**Test Case 1: Empty form**
|
**Test Case 1: Empty form**
|
||||||
```
|
```
|
||||||
- Input: Submit form trống
|
- Input: Submit empty form
|
||||||
- Expected: Hiển thị lỗi "Email là bắt buộc"
|
- Expected: Display error "Email is required"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 2: Invalid email**
|
**Test Case 2: Invalid email**
|
||||||
```
|
```
|
||||||
- Input: Email = "notanemail"
|
- Input: Email = "notanemail"
|
||||||
- Expected: "Email không hợp lệ"
|
- Expected: "Invalid email format"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 3: Short password**
|
**Test Case 3: Short password**
|
||||||
```
|
```
|
||||||
- Input: Password = "123"
|
- Input: Password = "123"
|
||||||
- Expected: "Mật khẩu phải có ít nhất 8 ký tự"
|
- Expected: "Password must be at least 8 characters"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Authentication Testing
|
### 2. Authentication Testing
|
||||||
@@ -269,13 +269,13 @@ className="grid grid-cols-1 md:grid-cols-2"
|
|||||||
**Test Case 5: Invalid credentials**
|
**Test Case 5: Invalid credentials**
|
||||||
```
|
```
|
||||||
- Input: Wrong password
|
- Input: Wrong password
|
||||||
- Expected: Error message từ server
|
- Expected: Error message from server
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 6: Network error**
|
**Test Case 6: Network error**
|
||||||
```
|
```
|
||||||
- Input: Server offline
|
- Input: Server offline
|
||||||
- Expected: Error message "Có lỗi xảy ra"
|
- Expected: Error message "An error occurred"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. UX Testing
|
### 3. UX Testing
|
||||||
@@ -288,14 +288,14 @@ className="grid grid-cols-1 md:grid-cols-2"
|
|||||||
|
|
||||||
**Test Case 8: Remember me**
|
**Test Case 8: Remember me**
|
||||||
```
|
```
|
||||||
- Action: Check "Nhớ đăng nhập"
|
- Action: Check "Remember me"
|
||||||
- Expected: Token persist sau khi reload
|
- Expected: Token persists after reload
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 9: Loading state**
|
**Test Case 9: Loading state**
|
||||||
```
|
```
|
||||||
- Action: Submit form
|
- Action: Submit form
|
||||||
- Expected: Button disabled, spinner hiển thị
|
- Expected: Button disabled, spinner displayed
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔐 Security Features
|
## 🔐 Security Features
|
||||||
@@ -311,18 +311,18 @@ onClick={() => setShowPassword(!showPassword)}
|
|||||||
|
|
||||||
### 2. HTTPS Only (Production)
|
### 2. HTTPS Only (Production)
|
||||||
```typescript
|
```typescript
|
||||||
// Trong .env
|
// In .env
|
||||||
VITE_API_URL=https://api.yourdomain.com
|
VITE_API_URL=https://api.yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Token Storage
|
### 3. Token Storage
|
||||||
```typescript
|
```typescript
|
||||||
// LocalStorage cho remember me
|
// LocalStorage for remember me
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionStorage cho session only
|
// SessionStorage for session only
|
||||||
else {
|
else {
|
||||||
sessionStorage.setItem('token', token);
|
sessionStorage.setItem('token', token);
|
||||||
}
|
}
|
||||||
@@ -355,22 +355,22 @@ else {
|
|||||||
✅ Remember form state
|
✅ Remember form state
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Next Steps (Chức năng 5-7)
|
## 🚀 Next Steps (Function 5-7)
|
||||||
|
|
||||||
1. **Chức năng 5: Form Register**
|
1. **Function 5: Register Form**
|
||||||
- Copy structure từ LoginPage
|
- Copy structure from LoginPage
|
||||||
- Thêm fields: name, phone, confirmPassword
|
- Add fields: name, phone, confirmPassword
|
||||||
- Use registerSchema
|
- Use registerSchema
|
||||||
- Redirect to /login after success
|
- Redirect to /login after success
|
||||||
|
|
||||||
2. **Chức năng 6: Forgot Password**
|
2. **Function 6: Forgot Password**
|
||||||
- Simple form với email only
|
- Simple form with email only
|
||||||
- Send reset link
|
- Send reset link
|
||||||
- Success message
|
- Success message
|
||||||
|
|
||||||
3. **Chức năng 7: Reset Password**
|
3. **Function 7: Reset Password**
|
||||||
- Form với password + confirmPassword
|
- Form with password + confirmPassword
|
||||||
- Token từ URL params
|
- Token from URL params
|
||||||
- Redirect to /login after success
|
- Redirect to /login after success
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
@@ -382,24 +382,24 @@ Solution: Check token expiry time
|
|||||||
- Refresh token: 7 days
|
- Refresh token: 7 days
|
||||||
```
|
```
|
||||||
|
|
||||||
### Issue 2: Form không submit
|
### Issue 2: Form doesn't submit
|
||||||
```typescript
|
```typescript
|
||||||
Solution: Check console for validation errors
|
Solution: Check console for validation errors
|
||||||
- Open DevTools > Console
|
- Open DevTools > Console
|
||||||
- Look for Yup validation errors
|
- Look for Yup validation errors
|
||||||
```
|
```
|
||||||
|
|
||||||
### Issue 3: Redirect không hoạt động
|
### Issue 3: Redirect doesn't work
|
||||||
```typescript
|
```typescript
|
||||||
Solution: Check location state
|
Solution: Check location state
|
||||||
console.log(location.state?.from);
|
console.log(location.state?.from);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Issue 4: Remember me không work
|
### Issue 4: Remember me doesn't work
|
||||||
```typescript
|
```typescript
|
||||||
Solution: Check localStorage
|
Solution: Check localStorage
|
||||||
- Open DevTools > Application > Local Storage
|
- Open DevTools > Application > Local Storage
|
||||||
- Check "token" và "refreshToken" keys
|
- Check "token" and "refreshToken" keys
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 Resources
|
## 📚 Resources
|
||||||
@@ -428,5 +428,5 @@ Solution: Check localStorage
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** ✅ Chức năng 4 hoàn thành
|
**Status:** ✅ Function 4 completed
|
||||||
**Next:** Chức năng 5 - Form Register
|
**Next:** Function 5 - Register Form
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
# Chức năng 5: Form Đăng Ký - Hoàn Thành ✅
|
# Function 5: Register Form - Completed ✅
|
||||||
|
|
||||||
## 📦 Files Đã Tạo/Cập Nhật
|
## 📦 Files Created/Updated
|
||||||
|
|
||||||
### 1. **RegisterPage.tsx** - Component form đăng ký
|
### 1. **RegisterPage.tsx** - Register form component
|
||||||
**Đường dẫn:** `client/src/pages/auth/RegisterPage.tsx`
|
**Path:** `client/src/pages/auth/RegisterPage.tsx`
|
||||||
|
|
||||||
### 2. **index.ts** - Export module
|
### 2. **index.ts** - Export module
|
||||||
**Đường dẫn:** `client/src/pages/auth/index.ts`
|
**Path:** `client/src/pages/auth/index.ts`
|
||||||
- Đã thêm export RegisterPage
|
- Added export RegisterPage
|
||||||
|
|
||||||
### 3. **App.tsx** - Cập nhật routing
|
### 3. **App.tsx** - Routing updated
|
||||||
**Đường dẫn:** `client/src/App.tsx`
|
**Path:** `client/src/App.tsx`
|
||||||
- Đã thêm route `/register`
|
- Added route `/register`
|
||||||
|
|
||||||
## ✨ Tính Năng Chính
|
## ✨ Main Features
|
||||||
|
|
||||||
### 1. Form Fields (5 fields)
|
### 1. Form Fields (5 fields)
|
||||||
✅ **Họ và tên** (name)
|
✅ **Full Name** (name)
|
||||||
- Required, 2-50 ký tự
|
- Required, 2-50 characters
|
||||||
- Icon: User
|
- Icon: User
|
||||||
- Placeholder: "Nguyễn Văn A"
|
- Placeholder: "John Doe"
|
||||||
|
|
||||||
✅ **Email**
|
✅ **Email**
|
||||||
- Required, valid email format
|
- Required, valid email format
|
||||||
- Icon: Mail
|
- Icon: Mail
|
||||||
- Placeholder: "email@example.com"
|
- Placeholder: "email@example.com"
|
||||||
|
|
||||||
✅ **Số điện thoại** (phone) - Optional
|
✅ **Phone Number** (phone) - Optional
|
||||||
- 10-11 chữ số
|
- 10-11 digits
|
||||||
- Icon: Phone
|
- Icon: Phone
|
||||||
- Placeholder: "0123456789"
|
- Placeholder: "0123456789"
|
||||||
|
|
||||||
✅ **Mật khẩu** (password)
|
✅ **Password** (password)
|
||||||
- Required, min 8 chars
|
- Required, min 8 chars
|
||||||
- Must contain: uppercase, lowercase, number, special char
|
- Must contain: uppercase, lowercase, number, special char
|
||||||
- Show/hide toggle với Eye icon
|
- Show/hide toggle with Eye icon
|
||||||
- Icon: Lock
|
- Icon: Lock
|
||||||
|
|
||||||
✅ **Xác nhận mật khẩu** (confirmPassword)
|
✅ **Confirm Password** (confirmPassword)
|
||||||
- Must match password
|
- Must match password
|
||||||
- Show/hide toggle với Eye icon
|
- Show/hide toggle with Eye icon
|
||||||
- Icon: Lock
|
- Icon: Lock
|
||||||
|
|
||||||
### 2. Password Strength Indicator
|
### 2. Password Strength Indicator
|
||||||
✅ **Visual Progress Bar** với 5 levels:
|
✅ **Visual Progress Bar** with 5 levels:
|
||||||
1. 🔴 Rất yếu (0/5)
|
1. 🔴 Very weak (0/5)
|
||||||
2. 🟠 Yếu (1/5)
|
2. 🟠 Weak (1/5)
|
||||||
3. 🟡 Trung bình (2/5)
|
3. 🟡 Medium (2/5)
|
||||||
4. 🔵 Mạnh (3/5)
|
4. 🔵 Strong (3/5)
|
||||||
5. 🟢 Rất mạnh (5/5)
|
5. 🟢 Very strong (5/5)
|
||||||
|
|
||||||
✅ **Real-time Requirements Checker:**
|
✅ **Real-time Requirements Checker:**
|
||||||
- ✅/❌ Ít nhất 8 ký tự
|
- ✅/❌ At least 8 characters
|
||||||
- ✅/❌ Chữ thường (a-z)
|
- ✅/❌ Lowercase (a-z)
|
||||||
- ✅/❌ Chữ hoa (A-Z)
|
- ✅/❌ Uppercase (A-Z)
|
||||||
- ✅/❌ Số (0-9)
|
- ✅/❌ Number (0-9)
|
||||||
- ✅/❌ Ký tự đặc biệt (@$!%*?&)
|
- ✅/❌ Special character (@$!%*?&)
|
||||||
|
|
||||||
### 3. Validation Rules (Yup Schema)
|
### 3. Validation Rules (Yup Schema)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
name:
|
name:
|
||||||
- Required: "Họ tên là bắt buộc"
|
- Required: "Full name is required"
|
||||||
- Min 2 chars: "Họ tên phải có ít nhất 2 ký tự"
|
- Min 2 chars: "Full name must be at least 2 characters"
|
||||||
- Max 50 chars: "Họ tên không được quá 50 ký tự"
|
- Max 50 chars: "Full name must not exceed 50 characters"
|
||||||
- Trim whitespace
|
- Trim whitespace
|
||||||
|
|
||||||
email:
|
email:
|
||||||
- Required: "Email là bắt buộc"
|
- Required: "Email is required"
|
||||||
- Valid format: "Email không hợp lệ"
|
- Valid format: "Invalid email format"
|
||||||
- Trim whitespace
|
- Trim whitespace
|
||||||
|
|
||||||
phone (optional):
|
phone (optional):
|
||||||
- Pattern /^[0-9]{10,11}$/
|
- Pattern /^[0-9]{10,11}$/
|
||||||
- Error: "Số điện thoại không hợp lệ"
|
- Error: "Invalid phone number"
|
||||||
|
|
||||||
password:
|
password:
|
||||||
- Required: "Mật khẩu là bắt buộc"
|
- Required: "Password is required"
|
||||||
- Min 8 chars
|
- Min 8 chars
|
||||||
- Pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/
|
- Pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/
|
||||||
- Error: "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
|
- Error: "Password must contain uppercase, lowercase, number and special characters"
|
||||||
|
|
||||||
confirmPassword:
|
confirmPassword:
|
||||||
- Required: "Vui lòng xác nhận mật khẩu"
|
- Required: "Please confirm password"
|
||||||
- Must match password: "Mật khẩu không khớp"
|
- Must match password: "Passwords do not match"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. UX Features
|
### 4. UX Features
|
||||||
@@ -93,32 +93,32 @@ confirmPassword:
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
Đang xử lý...
|
Processing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<UserPlus />
|
<UserPlus />
|
||||||
Đăng ký
|
Register
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
```
|
```
|
||||||
|
|
||||||
✅ **Show/Hide Password** (2 toggles)
|
✅ **Show/Hide Password** (2 toggles)
|
||||||
- Eye/EyeOff icons
|
- Eye/EyeOff icons
|
||||||
- Separate toggle cho password và confirmPassword
|
- Separate toggle for password and confirmPassword
|
||||||
- Visual feedback khi hover
|
- Visual feedback on hover
|
||||||
|
|
||||||
✅ **Error Display**
|
✅ **Error Display**
|
||||||
- Inline validation errors dưới mỗi field
|
- Inline validation errors under each field
|
||||||
- Global error message ở top của form
|
- Global error message at top of form
|
||||||
- Red border cho fields có lỗi
|
- Red border for fields with errors
|
||||||
|
|
||||||
✅ **Success Flow**
|
✅ **Success Flow**
|
||||||
```typescript
|
```typescript
|
||||||
1. Submit form
|
1. Submit form
|
||||||
2. Validation passes
|
2. Validation passes
|
||||||
3. Call useAuthStore.register()
|
3. Call useAuthStore.register()
|
||||||
4. Show toast: "Đăng ký thành công! Vui lòng đăng nhập."
|
4. Show toast: "Registration successful! Please login."
|
||||||
5. Navigate to /login
|
5. Navigate to /login
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,38 +135,38 @@ confirmPassword:
|
|||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ 🏨 Hotel Icon (Purple) │
|
│ 🏨 Hotel Icon (Purple) │
|
||||||
│ Đăng ký tài khoản │
|
│ Register Account │
|
||||||
│ Tạo tài khoản mới để đặt phòng... │
|
│ Create a new account to book... │
|
||||||
├─────────────────────────────────────┤
|
├─────────────────────────────────────┤
|
||||||
│ ┌───────────────────────────────┐ │
|
│ ┌───────────────────────────────┐ │
|
||||||
│ │ [Error message if any] │ │
|
│ │ [Error message if any] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Họ và tên │ │
|
│ │ Full Name │ │
|
||||||
│ │ [👤 Nguyễn Văn A ] │ │
|
│ │ [👤 John Doe ] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Email │ │
|
│ │ Email │ │
|
||||||
│ │ [📧 email@example.com ] │ │
|
│ │ [📧 email@example.com ] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Số điện thoại (Tùy chọn) │ │
|
│ │ Phone Number (Optional) │ │
|
||||||
│ │ [📱 0123456789 ] │ │
|
│ │ [📱 0123456789 ] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Mật khẩu │ │
|
│ │ Password │ │
|
||||||
│ │ [🔒 •••••••• 👁️] │ │
|
│ │ [🔒 •••••••• 👁️] │ │
|
||||||
│ │ ▓▓▓▓▓░░░░░ Rất mạnh │ │
|
│ │ ▓▓▓▓▓░░░░░ Very strong │ │
|
||||||
│ │ ✅ Ít nhất 8 ký tự │ │
|
│ │ ✅ At least 8 characters │ │
|
||||||
│ │ ✅ Chữ thường (a-z) │ │
|
│ │ ✅ Lowercase (a-z) │ │
|
||||||
│ │ ✅ Chữ hoa (A-Z) │ │
|
│ │ ✅ Uppercase (A-Z) │ │
|
||||||
│ │ ✅ Số (0-9) │ │
|
│ │ ✅ Number (0-9) │ │
|
||||||
│ │ ✅ Ký tự đặc biệt │ │
|
│ │ ✅ Special character │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ Xác nhận mật khẩu │ │
|
│ │ Confirm Password │ │
|
||||||
│ │ [🔒 •••••••• 👁️] │ │
|
│ │ [🔒 •••••••• 👁️] │ │
|
||||||
│ ├───────────────────────────────┤ │
|
│ ├───────────────────────────────┤ │
|
||||||
│ │ [👤 Đăng ký] │ │
|
│ │ [👤 Register] │ │
|
||||||
│ └───────────────────────────────┘ │
|
│ └───────────────────────────────┘ │
|
||||||
│ Đã có tài khoản? Đăng nhập ngay │
|
│ Already have an account? Login now │
|
||||||
│ │
|
│ │
|
||||||
│ Điều khoản & Chính sách bảo mật │
|
│ Terms & Privacy Policy │
|
||||||
└─────────────────────────────────────┘
|
└─────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ POST /api/auth/register
|
|||||||
### Request Body
|
### Request Body
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
name: string; // "Nguyễn Văn A"
|
name: string; // "John Doe"
|
||||||
email: string; // "user@example.com"
|
email: string; // "user@example.com"
|
||||||
password: string; // "Password123@"
|
password: string; // "Password123@"
|
||||||
phone?: string; // "0123456789" (optional)
|
phone?: string; // "0123456789" (optional)
|
||||||
@@ -195,7 +195,7 @@ POST /api/auth/register
|
|||||||
"data": {
|
"data": {
|
||||||
"user": {
|
"user": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Nguyễn Văn A",
|
"name": "John Doe",
|
||||||
"email": "user@example.com",
|
"email": "user@example.com",
|
||||||
"phone": "0123456789",
|
"phone": "0123456789",
|
||||||
"role": "customer"
|
"role": "customer"
|
||||||
@@ -235,20 +235,20 @@ Expected: Show validation errors for name, email, password
|
|||||||
**Test Case 2: Invalid email**
|
**Test Case 2: Invalid email**
|
||||||
```
|
```
|
||||||
Input: email = "notanemail"
|
Input: email = "notanemail"
|
||||||
Expected: "Email không hợp lệ"
|
Expected: "Invalid email format"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 3: Short name**
|
**Test Case 3: Short name**
|
||||||
```
|
```
|
||||||
Input: name = "A"
|
Input: name = "A"
|
||||||
Expected: "Họ tên phải có ít nhất 2 ký tự"
|
Expected: "Full name must be at least 2 characters"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 4: Weak password**
|
**Test Case 4: Weak password**
|
||||||
```
|
```
|
||||||
Input: password = "abc123"
|
Input: password = "abc123"
|
||||||
Expected: "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
|
Expected: "Password must contain uppercase, lowercase, number and special characters"
|
||||||
Password strength: Yếu/Trung bình
|
Password strength: Weak/Medium
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 5: Password mismatch**
|
**Test Case 5: Password mismatch**
|
||||||
@@ -256,13 +256,13 @@ Password strength: Yếu/Trung bình
|
|||||||
Input:
|
Input:
|
||||||
password = "Password123@"
|
password = "Password123@"
|
||||||
confirmPassword = "Password456@"
|
confirmPassword = "Password456@"
|
||||||
Expected: "Mật khẩu không khớp"
|
Expected: "Passwords do not match"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Case 6: Invalid phone**
|
**Test Case 6: Invalid phone**
|
||||||
```
|
```
|
||||||
Input: phone = "123"
|
Input: phone = "123"
|
||||||
Expected: "Số điện thoại không hợp lệ"
|
Expected: "Invalid phone number"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. UX Tests
|
### 2. UX Tests
|
||||||
@@ -290,7 +290,7 @@ Action: Submit valid form
|
|||||||
Expected:
|
Expected:
|
||||||
- Button disabled
|
- Button disabled
|
||||||
- Spinner shows
|
- Spinner shows
|
||||||
- Text changes to "Đang xử lý..."
|
- Text changes to "Processing..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Integration Tests
|
### 3. Integration Tests
|
||||||
@@ -300,7 +300,7 @@ Expected:
|
|||||||
Input: All valid data
|
Input: All valid data
|
||||||
Expected:
|
Expected:
|
||||||
1. API POST /api/auth/register called
|
1. API POST /api/auth/register called
|
||||||
2. Toast: "Đăng ký thành công! Vui lòng đăng nhập."
|
2. Toast: "Registration successful! Please login."
|
||||||
3. Redirect to /login
|
3. Redirect to /login
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ Expected:
|
|||||||
```
|
```
|
||||||
Scenario: Server offline
|
Scenario: Server offline
|
||||||
Expected:
|
Expected:
|
||||||
- Error message: "Đăng ký thất bại. Vui lòng thử lại."
|
- Error message: "Registration failed. Please try again."
|
||||||
- Toast error displayed
|
- Toast error displayed
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ function getPasswordStrength(pwd: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
strength: 0-5,
|
strength: 0-5,
|
||||||
label: ['Rất yếu', 'Yếu', 'Trung bình', 'Mạnh', 'Rất mạnh'][strength],
|
label: ['Very weak', 'Weak', 'Medium', 'Strong', 'Very strong'][strength],
|
||||||
color: ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'][strength]
|
color: ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'][strength]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ function getPasswordStrength(pwd: string) {
|
|||||||
RegisterPage/
|
RegisterPage/
|
||||||
├── Header Section
|
├── Header Section
|
||||||
│ ├── Hotel Icon (purple)
|
│ ├── Hotel Icon (purple)
|
||||||
│ ├── Title: "Đăng ký tài khoản"
|
│ ├── Title: "Register Account"
|
||||||
│ └── Subtitle
|
│ └── Subtitle
|
||||||
│
|
│
|
||||||
├── Form Container (white card)
|
├── Form Container (white card)
|
||||||
@@ -364,7 +364,7 @@ RegisterPage/
|
|||||||
│ └── Submit Button (with loading)
|
│ └── Submit Button (with loading)
|
||||||
│
|
│
|
||||||
├── Login Link
|
├── Login Link
|
||||||
│ └── "Đã có tài khoản? Đăng nhập ngay"
|
│ └── "Already have an account? Login now"
|
||||||
│
|
│
|
||||||
└── Footer Links
|
└── Footer Links
|
||||||
├── Terms of Service
|
├── Terms of Service
|
||||||
@@ -413,7 +413,7 @@ http://localhost:5173/register
|
|||||||
|
|
||||||
### Example Registration
|
### Example Registration
|
||||||
```typescript
|
```typescript
|
||||||
Name: "Nguyễn Văn A"
|
Name: "John Doe"
|
||||||
Email: "nguyenvana@example.com"
|
Email: "nguyenvana@example.com"
|
||||||
Phone: "0123456789"
|
Phone: "0123456789"
|
||||||
Password: "Password123@"
|
Password: "Password123@"
|
||||||
@@ -481,6 +481,6 @@ to /login Show errors
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** ✅ Chức năng 5 hoàn thành
|
**Status:** ✅ Function 5 completed
|
||||||
**Next:** Chức năng 6 - Forgot Password
|
**Next:** Function 6 - Forgot Password
|
||||||
**Test URL:** http://localhost:5173/register
|
**Test URL:** http://localhost:5173/register
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# Route Protection Documentation
|
# Route Protection Documentation
|
||||||
|
|
||||||
## Chức năng 8: Phân quyền & Bảo vệ Route
|
## Function 8: Authorization & Route Protection
|
||||||
|
|
||||||
Hệ thống sử dụng 2 component để bảo vệ các route:
|
The system uses 2 components to protect routes:
|
||||||
- **ProtectedRoute**: Yêu cầu user phải đăng nhập
|
- **ProtectedRoute**: Requires user to be logged in
|
||||||
- **AdminRoute**: Yêu cầu user phải là Admin
|
- **AdminRoute**: Requires user to be Admin
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. ProtectedRoute
|
## 1. ProtectedRoute
|
||||||
|
|
||||||
### Mục đích
|
### Purpose
|
||||||
Bảo vệ các route yêu cầu authentication (đăng nhập).
|
Protects routes requiring authentication (login).
|
||||||
|
|
||||||
### Cách hoạt động
|
### How It Works
|
||||||
```tsx
|
```tsx
|
||||||
// File: client/src/components/auth/ProtectedRoute.tsx
|
// File: client/src/components/auth/ProtectedRoute.tsx
|
||||||
|
|
||||||
@@ -23,32 +23,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAuthenticated, isLoading } = useAuthStore();
|
const { isAuthenticated, isLoading } = useAuthStore();
|
||||||
|
|
||||||
// 1. Nếu đang loading → hiển thị spinner
|
// 1. If loading → display spinner
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Nếu chưa đăng nhập → redirect /login
|
// 2. If not logged in → redirect /login
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to="/login"
|
to="/login"
|
||||||
state={{ from: location }} // Lưu location để quay lại
|
state={{ from: location }} // Save location to return later
|
||||||
replace
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Đã đăng nhập → cho phép truy cập
|
// 3. Logged in → allow access
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sử dụng trong App.tsx
|
### Usage in App.tsx
|
||||||
```tsx
|
```tsx
|
||||||
import { ProtectedRoute } from './components/auth';
|
import { ProtectedRoute } from './components/auth';
|
||||||
|
|
||||||
// Route yêu cầu đăng nhập
|
// Route requiring login
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
@@ -77,20 +77,20 @@ import { ProtectedRoute } from './components/auth';
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Luồng hoạt động
|
### Flow
|
||||||
1. User chưa đăng nhập truy cập `/dashboard`
|
1. User not logged in accesses `/dashboard`
|
||||||
2. ProtectedRoute kiểm tra `isAuthenticated === false`
|
2. ProtectedRoute checks `isAuthenticated === false`
|
||||||
3. Redirect về `/login` và lưu `state={{ from: '/dashboard' }}`
|
3. Redirect to `/login` and save `state={{ from: '/dashboard' }}`
|
||||||
4. Sau khi login thành công, redirect về `/dashboard`
|
4. After successful login, redirect to `/dashboard`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. AdminRoute
|
## 2. AdminRoute
|
||||||
|
|
||||||
### Mục đích
|
### Purpose
|
||||||
Bảo vệ các route chỉ dành cho Admin (role-based access).
|
Protects routes for Admin only (role-based access).
|
||||||
|
|
||||||
### Cách hoạt động
|
### How It Works
|
||||||
```tsx
|
```tsx
|
||||||
// File: client/src/components/auth/AdminRoute.tsx
|
// File: client/src/components/auth/AdminRoute.tsx
|
||||||
|
|
||||||
@@ -100,12 +100,12 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||||
|
|
||||||
// 1. Nếu đang loading → hiển thị spinner
|
// 1. If loading → display spinner
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Nếu chưa đăng nhập → redirect /login
|
// 2. If not logged in → redirect /login
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
@@ -116,22 +116,22 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Nếu không phải admin → redirect /
|
// 3. If not admin → redirect /
|
||||||
const isAdmin = userInfo?.role === 'admin';
|
const isAdmin = userInfo?.role === 'admin';
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Là admin → cho phép truy cập
|
// 4. Is admin → allow access
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sử dụng trong App.tsx
|
### Usage in App.tsx
|
||||||
```tsx
|
```tsx
|
||||||
import { AdminRoute } from './components/auth';
|
import { AdminRoute } from './components/auth';
|
||||||
|
|
||||||
// Route chỉ dành cho Admin
|
// Route for Admin only
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
@@ -148,50 +148,50 @@ import { AdminRoute } from './components/auth';
|
|||||||
</Route>
|
</Route>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Luồng hoạt động
|
### Flow
|
||||||
|
|
||||||
#### Case 1: User chưa đăng nhập
|
#### Case 1: User not logged in
|
||||||
1. Truy cập `/admin`
|
1. Access `/admin`
|
||||||
2. AdminRoute kiểm tra `isAuthenticated === false`
|
2. AdminRoute checks `isAuthenticated === false`
|
||||||
3. Redirect về `/login` với `state={{ from: '/admin' }}`
|
3. Redirect to `/login` with `state={{ from: '/admin' }}`
|
||||||
4. Sau login thành công → quay lại `/admin`
|
4. After successful login → return to `/admin`
|
||||||
5. AdminRoute kiểm tra lại role
|
5. AdminRoute checks role again
|
||||||
|
|
||||||
#### Case 2: User đã đăng nhập nhưng không phải Admin
|
#### Case 2: User logged in but not Admin
|
||||||
1. Customer (role='customer') truy cập `/admin`
|
1. Customer (role='customer') accesses `/admin`
|
||||||
2. AdminRoute kiểm tra `isAuthenticated === true`
|
2. AdminRoute checks `isAuthenticated === true`
|
||||||
3. AdminRoute kiểm tra `userInfo.role === 'customer'` (không phải 'admin')
|
3. AdminRoute checks `userInfo.role === 'customer'` (not 'admin')
|
||||||
4. Redirect về `/` (trang chủ)
|
4. Redirect to `/` (homepage)
|
||||||
|
|
||||||
#### Case 3: User là Admin
|
#### Case 3: User is Admin
|
||||||
1. Admin (role='admin') truy cập `/admin`
|
1. Admin (role='admin') accesses `/admin`
|
||||||
2. AdminRoute kiểm tra `isAuthenticated === true`
|
2. AdminRoute checks `isAuthenticated === true`
|
||||||
3. AdminRoute kiểm tra `userInfo.role === 'admin'` ✅
|
3. AdminRoute checks `userInfo.role === 'admin'` ✅
|
||||||
4. Cho phép truy cập
|
4. Allow access
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Cấu trúc Route trong App.tsx
|
## 3. Route Structure in App.tsx
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public Routes - Không cần đăng nhập */}
|
{/* Public Routes - No login required */}
|
||||||
<Route path="/" element={<LayoutMain />}>
|
<Route path="/" element={<LayoutMain />}>
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
<Route path="rooms" element={<RoomListPage />} />
|
<Route path="rooms" element={<RoomListPage />} />
|
||||||
<Route path="about" element={<AboutPage />} />
|
<Route path="about" element={<AboutPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Auth Routes - Không cần layout */}
|
{/* Auth Routes - No layout */}
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||||
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
|
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
|
||||||
|
|
||||||
{/* Protected Routes - Yêu cầu đăng nhập */}
|
{/* Protected Routes - Login required */}
|
||||||
<Route path="/" element={<LayoutMain />}>
|
<Route path="/" element={<LayoutMain />}>
|
||||||
<Route
|
<Route
|
||||||
path="dashboard"
|
path="dashboard"
|
||||||
@@ -219,7 +219,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin Routes - Chỉ Admin */}
|
{/* Admin Routes - Admin only */}
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
@@ -251,7 +251,7 @@ function App() {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Tích hợp với Zustand Store
|
## 4. Integration with Zustand Store
|
||||||
|
|
||||||
### useAuthStore State
|
### useAuthStore State
|
||||||
```tsx
|
```tsx
|
||||||
@@ -286,17 +286,17 @@ const useAuthStore = create<AuthStore>((set) => ({
|
|||||||
```
|
```
|
||||||
|
|
||||||
### User Roles
|
### User Roles
|
||||||
- **admin**: Quản trị viên (full access)
|
- **admin**: Administrator (full access)
|
||||||
- **staff**: Nhân viên (limited access)
|
- **staff**: Staff (limited access)
|
||||||
- **customer**: Khách hàng (customer features only)
|
- **customer**: Customer (customer features only)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Loading State
|
## 5. Loading State
|
||||||
|
|
||||||
Cả 2 component đều xử lý loading state để tránh:
|
Both components handle loading state to avoid:
|
||||||
- Flash of redirect (nhấp nháy khi chuyển trang)
|
- Flash of redirect (flickering when changing pages)
|
||||||
- Race condition (auth state chưa load xong)
|
- Race condition (auth state not loaded yet)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -306,7 +306,7 @@ if (isLoading) {
|
|||||||
<div className="animate-spin rounded-full h-12 w-12
|
<div className="animate-spin rounded-full h-12 w-12
|
||||||
border-b-2 border-indigo-600 mx-auto"
|
border-b-2 border-indigo-600 mx-auto"
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-gray-600">Đang xác thực...</p>
|
<p className="mt-4 text-gray-600">Authenticating...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -330,12 +330,12 @@ const LoginPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await login(data);
|
await login(data);
|
||||||
|
|
||||||
// Redirect về page ban đầu hoặc /dashboard
|
// Redirect to original page or /dashboard
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
|
|
||||||
toast.success('Đăng nhập thành công!');
|
toast.success('Login successful!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Đăng nhập thất bại!');
|
toast.error('Login failed!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,58 +348,58 @@ const LoginPage: React.FC = () => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Flow
|
### Flow
|
||||||
1. User truy cập `/bookings` (protected)
|
1. User accesses `/bookings` (protected)
|
||||||
2. Redirect `/login?from=/bookings`
|
2. Redirect `/login?from=/bookings`
|
||||||
3. Login thành công
|
3. Login successful
|
||||||
4. Redirect về `/bookings` (page ban đầu)
|
4. Redirect to `/bookings` (original page)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Testing Route Protection
|
## 7. Testing Route Protection
|
||||||
|
|
||||||
### Test Case 1: ProtectedRoute - Unauthenticated
|
### Test Case 1: ProtectedRoute - Unauthenticated
|
||||||
**Given**: User chưa đăng nhập
|
**Given**: User not logged in
|
||||||
**When**: Truy cập `/dashboard`
|
**When**: Access `/dashboard`
|
||||||
**Then**: Redirect về `/login`
|
**Then**: Redirect to `/login`
|
||||||
**And**: Lưu `from=/dashboard` trong location state
|
**And**: Save `from=/dashboard` in location state
|
||||||
|
|
||||||
### Test Case 2: ProtectedRoute - Authenticated
|
### Test Case 2: ProtectedRoute - Authenticated
|
||||||
**Given**: User đã đăng nhập
|
**Given**: User logged in
|
||||||
**When**: Truy cập `/dashboard`
|
**When**: Access `/dashboard`
|
||||||
**Then**: Hiển thị DashboardPage thành công
|
**Then**: Display DashboardPage successfully
|
||||||
|
|
||||||
### Test Case 3: AdminRoute - Not Admin
|
### Test Case 3: AdminRoute - Not Admin
|
||||||
**Given**: User có role='customer'
|
**Given**: User has role='customer'
|
||||||
**When**: Truy cập `/admin`
|
**When**: Access `/admin`
|
||||||
**Then**: Redirect về `/` (trang chủ)
|
**Then**: Redirect to `/` (homepage)
|
||||||
|
|
||||||
### Test Case 4: AdminRoute - Is Admin
|
### Test Case 4: AdminRoute - Is Admin
|
||||||
**Given**: User có role='admin'
|
**Given**: User has role='admin'
|
||||||
**When**: Truy cập `/admin`
|
**When**: Access `/admin`
|
||||||
**Then**: Hiển thị AdminLayout thành công
|
**Then**: Display AdminLayout successfully
|
||||||
|
|
||||||
### Test Case 5: Loading State
|
### Test Case 5: Loading State
|
||||||
**Given**: Auth đang initialize
|
**Given**: Auth is initializing
|
||||||
**When**: isLoading === true
|
**When**: isLoading === true
|
||||||
**Then**: Hiển thị loading spinner
|
**Then**: Display loading spinner
|
||||||
**And**: Không redirect
|
**And**: No redirect
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Security Best Practices
|
## 8. Security Best Practices
|
||||||
|
|
||||||
### ✅ Đã Implement
|
### ✅ Implemented
|
||||||
1. **Client-side protection**: ProtectedRoute & AdminRoute
|
1. **Client-side protection**: ProtectedRoute & AdminRoute
|
||||||
2. **Token persistence**: localStorage
|
2. **Token persistence**: localStorage
|
||||||
3. **Role-based access**: Kiểm tra userInfo.role
|
3. **Role-based access**: Check userInfo.role
|
||||||
4. **Location state**: Lưu "from" để redirect về đúng page
|
4. **Location state**: Save "from" to redirect to correct page
|
||||||
5. **Loading state**: Tránh flash của redirect
|
5. **Loading state**: Avoid flash of redirect
|
||||||
6. **Replace navigation**: Không lưu lịch sử redirect
|
6. **Replace navigation**: Don't save redirect history
|
||||||
|
|
||||||
### ⚠️ Lưu Ý
|
### ⚠️ Note
|
||||||
- Client-side protection **không đủ** → Phải có backend validation
|
- Client-side protection **is not enough** → Must have backend validation
|
||||||
- API endpoints phải kiểm tra JWT + role
|
- API endpoints must check JWT + role
|
||||||
- Middleware backend: `auth`, `adminOnly`
|
- Backend middleware: `auth`, `adminOnly`
|
||||||
- Never trust client-side role → Always verify on server
|
- Never trust client-side role → Always verify on server
|
||||||
|
|
||||||
### Backend Middleware Example
|
### Backend Middleware Example
|
||||||
@@ -440,45 +440,45 @@ router.get('/admin/users', auth, adminOnly, getUsers);
|
|||||||
|
|
||||||
## 9. Troubleshooting
|
## 9. Troubleshooting
|
||||||
|
|
||||||
### Vấn đề 1: Infinite redirect loop
|
### Issue 1: Infinite redirect loop
|
||||||
**Nguyên nhân**: ProtectedRoute check sai logic
|
**Cause**: ProtectedRoute check logic error
|
||||||
**Giải pháp**: Đảm bảo `replace={true}` trong Navigate
|
**Solution**: Ensure `replace={true}` in Navigate
|
||||||
|
|
||||||
### Vấn đề 2: Flash of redirect
|
### Issue 2: Flash of redirect
|
||||||
**Nguyên nhân**: Không handle loading state
|
**Cause**: Not handling loading state
|
||||||
**Giải pháp**: Thêm check `if (isLoading)` trước check auth
|
**Solution**: Add check `if (isLoading)` before auth check
|
||||||
|
|
||||||
### Vấn đề 3: Lost location state
|
### Issue 3: Lost location state
|
||||||
**Nguyên nhân**: Không pass `state={{ from: location }}`
|
**Cause**: Not passing `state={{ from: location }}`
|
||||||
**Giải pháp**: Luôn lưu location khi redirect
|
**Solution**: Always save location when redirecting
|
||||||
|
|
||||||
### Vấn đề 4: Admin có thể truy cập nhưng API fail
|
### Issue 4: Admin can access but API fails
|
||||||
**Nguyên nhân**: Backend không verify role
|
**Cause**: Backend doesn't verify role
|
||||||
**Giải pháp**: Thêm middleware `adminOnly` trên API routes
|
**Solution**: Add `adminOnly` middleware on API routes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Summary
|
## 10. Summary
|
||||||
|
|
||||||
### ProtectedRoute
|
### ProtectedRoute
|
||||||
- ✅ Kiểm tra `isAuthenticated`
|
- ✅ Check `isAuthenticated`
|
||||||
- ✅ Redirect `/login` nếu chưa đăng nhập
|
- ✅ Redirect `/login` if not logged in
|
||||||
- ✅ Lưu location state để quay lại
|
- ✅ Save location state to return
|
||||||
- ✅ Handle loading state
|
- ✅ Handle loading state
|
||||||
|
|
||||||
### AdminRoute
|
### AdminRoute
|
||||||
- ✅ Kiểm tra `isAuthenticated` trước
|
- ✅ Check `isAuthenticated` first
|
||||||
- ✅ Kiểm tra `userInfo.role === 'admin'`
|
- ✅ Check `userInfo.role === 'admin'`
|
||||||
- ✅ Redirect `/login` nếu chưa đăng nhập
|
- ✅ Redirect `/login` if not logged in
|
||||||
- ✅ Redirect `/` nếu không phải admin
|
- ✅ Redirect `/` if not admin
|
||||||
- ✅ Handle loading state
|
- ✅ Handle loading state
|
||||||
|
|
||||||
### Kết quả
|
### Results
|
||||||
- Bảo vệ toàn bộ protected routes
|
- Protect all protected routes
|
||||||
- UX mượt mà, không flash
|
- Smooth UX, no flash
|
||||||
- Role-based access hoạt động chính xác
|
- Role-based access works correctly
|
||||||
- Security tốt (kết hợp backend validation)
|
- Good security (combined with backend validation)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Chức năng 8 hoàn thành! ✅**
|
**Function 8 completed! ✅**
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
## 📦 Files Created
|
## 📦 Files Created
|
||||||
|
|
||||||
### Core Server Files
|
### Core Server Files
|
||||||
1. **`.env`** - Environment configuration (với mật khẩu và secrets)
|
1. **`.env`** - Environment configuration (with passwords and secrets)
|
||||||
2. **`src/server.js`** - Server entry point với database connection
|
2. **`src/server.js`** - Server entry point with database connection
|
||||||
3. **`src/app.js`** - Express application setup với middleware
|
3. **`src/app.js`** - Express application setup with middleware
|
||||||
|
|
||||||
### Controllers
|
### Controllers
|
||||||
4. **`src/controllers/authController.js`** - Authentication logic
|
4. **`src/controllers/authController.js`** - Authentication logic
|
||||||
@@ -230,11 +230,11 @@ CLIENT_URL=http://localhost:5173
|
|||||||
|
|
||||||
### 1. Database Setup
|
### 1. Database Setup
|
||||||
```bash
|
```bash
|
||||||
# Tạo database
|
# Create database
|
||||||
mysql -u root -p
|
mysql -u root -p
|
||||||
CREATE DATABASE hotel_db;
|
CREATE DATABASE hotel_db;
|
||||||
|
|
||||||
# Chạy migrations
|
# Run migrations
|
||||||
cd d:/hotel-booking/server
|
cd d:/hotel-booking/server
|
||||||
npm run migrate
|
npm run migrate
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Test Scenarios - Route Protection (Chức năng 8)
|
# Test Scenarios - Route Protection (Function 8)
|
||||||
|
|
||||||
## Test Setup
|
## Test Setup
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Verify that loading state is displayed during auth check.
|
|||||||
|
|
||||||
### Expected Result
|
### Expected Result
|
||||||
- ✅ Loading spinner displayed
|
- ✅ Loading spinner displayed
|
||||||
- ✅ Text "Đang tải..." or "Đang xác thực..." visible
|
- ✅ Text "Loading..." or "Authenticating..." visible
|
||||||
- ✅ No flash of redirect
|
- ✅ No flash of redirect
|
||||||
- ✅ Smooth transition after loading
|
- ✅ Smooth transition after loading
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ Verify that logout clears auth and redirects properly.
|
|||||||
- ✅ Token removed from localStorage
|
- ✅ Token removed from localStorage
|
||||||
- ✅ userInfo removed from localStorage
|
- ✅ userInfo removed from localStorage
|
||||||
- ✅ Redirected to `/` or `/login`
|
- ✅ Redirected to `/` or `/login`
|
||||||
- ✅ Navbar shows "Đăng nhập" button
|
- ✅ Navbar shows "Login" button
|
||||||
- ✅ Cannot access protected routes anymore
|
- ✅ Cannot access protected routes anymore
|
||||||
|
|
||||||
### Actual Result
|
### Actual Result
|
||||||
@@ -430,7 +430,7 @@ Verify that non-existent routes show 404 page.
|
|||||||
|
|
||||||
### Expected Result
|
### Expected Result
|
||||||
- ✅ 404 page displayed
|
- ✅ 404 page displayed
|
||||||
- ✅ "404 - Không tìm thấy trang" message
|
- ✅ "404 - Page not found" message
|
||||||
- ✅ URL shows `/non-existent-route`
|
- ✅ URL shows `/non-existent-route`
|
||||||
- ✅ No errors in console
|
- ✅ No errors in console
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
# 🏨 Hotel Management & Booking System
|
# 🏨 Hotel Management & Booking System
|
||||||
## Bản Phân Tích Dành Cho Admin (SRS Admin Analysis)
|
## Admin Analysis Document (SRS Admin Analysis)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Giới thiệu
|
## 1. Introduction
|
||||||
Tài liệu này phân tích các yêu cầu từ SRS của hệ thống **Hotel Management & Booking Online (e-Hotel)**, tập trung hoàn toàn vào phần **Admin / Manager / Staff** (không bao gồm khách hàng).
|
This document analyzes the requirements from the SRS of the **Hotel Management & Booking Online (e-Hotel)** system, focusing entirely on the **Admin / Manager / Staff** section (excluding customers).
|
||||||
Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo mật của hệ thống.
|
The goal is to understand the administration, operation, and security functions of the system.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 2. Phân tích chức năng dành cho Admin
|
# 2. Admin Functionality Analysis
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.1 Setup Module (Thiết lập hệ thống)
|
## 2.1 Setup Module (System Setup)
|
||||||
|
|
||||||
### 2.1.1 Setup Rooms (Quản lý phòng)
|
### 2.1.1 Setup Rooms (Room Management)
|
||||||
**Vai trò sử dụng:** Manager, Admin
|
**User Roles:** Manager, Admin
|
||||||
|
|
||||||
**Các chức năng:**
|
**Functions:**
|
||||||
- Thêm mới phòng
|
- Add new room
|
||||||
- Chỉnh sửa thông tin phòng
|
- Edit room information
|
||||||
- Xoá phòng *(chỉ khi phòng chưa có booking)*
|
- Delete room *(only when room has no bookings)*
|
||||||
- Upload hình ảnh phòng
|
- Upload room images
|
||||||
|
|
||||||
**Thông tin phòng gồm:**
|
**Room Information:**
|
||||||
- RoomID
|
- RoomID
|
||||||
- Description
|
- Description
|
||||||
- Type (VIP, DELUX, SUITE, …)
|
- Type (VIP, DELUX, SUITE, …)
|
||||||
@@ -32,114 +32,114 @@ Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo
|
|||||||
- Price
|
- Price
|
||||||
- Pictures
|
- Pictures
|
||||||
|
|
||||||
**Quy tắc:**
|
**Rules:**
|
||||||
- Validate toàn bộ dữ liệu khi thêm/sửa
|
- Validate all data when adding/editing
|
||||||
- Không cho xoá phòng đã phát sinh booking
|
- Do not allow deletion of rooms that have bookings
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.1.2 Setup Services (Quản lý dịch vụ)
|
### 2.1.2 Setup Services (Service Management)
|
||||||
**Vai trò:** Manager, Admin
|
**Roles:** Manager, Admin
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Thêm dịch vụ
|
- Add service
|
||||||
- Chỉnh sửa
|
- Edit
|
||||||
- Xoá dịch vụ
|
- Delete service
|
||||||
|
|
||||||
**Thông tin dịch vụ:**
|
**Service Information:**
|
||||||
- Service ID
|
- Service ID
|
||||||
- Service Name
|
- Service Name
|
||||||
- Description
|
- Description
|
||||||
- Unit (giờ, suất, lần,…)
|
- Unit (hour, portion, time, …)
|
||||||
- Price
|
- Price
|
||||||
|
|
||||||
**Quy tắc:**
|
**Rules:**
|
||||||
- Validate tất cả dữ liệu nhập
|
- Validate all input data
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.1.3 Promotion Management (Quản lý khuyến mãi)
|
### 2.1.3 Promotion Management
|
||||||
**Vai trò:** Manager, Admin
|
**Roles:** Manager, Admin
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Add promotion
|
- Add promotion
|
||||||
- Edit promotion
|
- Edit promotion
|
||||||
- Delete promotion
|
- Delete promotion
|
||||||
- Promotion có thể áp dụng bằng code hoặc tự động trong booking
|
- Promotion can be applied by code or automatically in booking
|
||||||
|
|
||||||
**Thông tin:**
|
**Information:**
|
||||||
- ID
|
- ID
|
||||||
- Name
|
- Name
|
||||||
- Description
|
- Description
|
||||||
- Value (phần trăm hoặc số tiền)
|
- Value (percentage or fixed amount)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 2.2 Operation Module (Vận hành khách sạn)
|
# 2.2 Operation Module (Hotel Operations)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2.1 Booking Management
|
## 2.2.1 Booking Management
|
||||||
**Vai trò:** Staff, Manager, Admin
|
**Roles:** Staff, Manager, Admin
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Tìm booking theo tên khách, số booking, ngày đặt
|
- Search booking by guest name, booking number, booking date
|
||||||
- Xem chi tiết booking
|
- View booking details
|
||||||
- Xem bill dịch vụ
|
- View service bill
|
||||||
- Xử lý yêu cầu:
|
- Process requests:
|
||||||
- Hủy booking
|
- Cancel booking
|
||||||
- Checkout
|
- Checkout
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2.2 Check-in
|
## 2.2.2 Check-in
|
||||||
**Vai trò:** Staff, Manager
|
**Roles:** Staff, Manager
|
||||||
|
|
||||||
**Quy trình check-in:**
|
**Check-in Process:**
|
||||||
- Khách xuất trình Booking Number
|
- Guest presents Booking Number
|
||||||
- Nhân viên kiểm tra thông tin booking
|
- Staff verifies booking information
|
||||||
- Nhập thông tin từng khách trong phòng
|
- Enter information for each guest in the room
|
||||||
- Gán số phòng thực tế
|
- Assign actual room number
|
||||||
- Thu thêm phí nếu có trẻ em hoặc extra person
|
- Collect additional fees if there are children or extra persons
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2.3 Use Services (Khách đăng ký sử dụng dịch vụ)
|
## 2.2.3 Use Services (Guest Service Registration)
|
||||||
**Vai trò:** Staff
|
**Roles:** Staff
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Đăng ký dịch vụ cho khách dựa trên Room Number
|
- Register services for guests based on Room Number
|
||||||
- In ticket nếu có yêu cầu
|
- Print ticket if requested
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2.4 Check-out
|
## 2.2.4 Check-out
|
||||||
**Vai trò:** Staff, Manager
|
**Roles:** Staff, Manager
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Tính toán:
|
- Calculate:
|
||||||
- Phí phòng
|
- Room fee
|
||||||
- Phí dịch vụ
|
- Service fee
|
||||||
- Phụ phí khác
|
- Other surcharges
|
||||||
- Tạo hóa đơn (Invoice)
|
- Create invoice
|
||||||
- Khấu trừ tiền đã đặt cọc (booking value)
|
- Deduct deposit amount (booking value)
|
||||||
- Khách thanh toán phần còn lại
|
- Guest pays remaining amount
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 2.3 Report Module (Báo cáo)
|
# 2.3 Report Module
|
||||||
|
|
||||||
**Vai trò:** Manager, Admin
|
**Roles:** Manager, Admin
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Nhập khoảng thời gian From → To
|
- Enter time range From → To
|
||||||
- Liệt kê toàn bộ booking trong khoảng thời gian
|
- List all bookings within the time range
|
||||||
- Tính tổng doanh thu
|
- Calculate total revenue
|
||||||
- Xuất báo cáo:
|
- Export reports:
|
||||||
- Excel
|
- Excel
|
||||||
- PDF
|
- PDF
|
||||||
|
|
||||||
**Nội dung báo cáo:**
|
**Report Content:**
|
||||||
- Booking ID
|
- Booking ID
|
||||||
- Customer Name
|
- Customer Name
|
||||||
- Room
|
- Room
|
||||||
@@ -150,58 +150,58 @@ Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 2.4 System Administration Module (Quản trị hệ thống)
|
# 2.4 System Administration Module
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.4.1 User Management
|
## 2.4.1 User Management
|
||||||
**Vai trò:** Admin
|
**Roles:** Admin
|
||||||
|
|
||||||
**Chức năng:**
|
**Functions:**
|
||||||
- Add user
|
- Add user
|
||||||
- Edit user
|
- Edit user
|
||||||
- Delete user
|
- Delete user
|
||||||
- View user detail
|
- View user detail
|
||||||
- List tất cả user
|
- List all users
|
||||||
- Gán role (Admin, Manager, Staff)
|
- Assign role (Admin, Manager, Staff)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.4.2 Security
|
## 2.4.2 Security
|
||||||
**Chức năng bảo mật của hệ thống:**
|
**System Security Functions:**
|
||||||
|
|
||||||
### Roles được định nghĩa:
|
### Defined Roles:
|
||||||
| Role | Quyền |
|
| Role | Permissions |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| **Customer** | Không cần login |
|
| **Customer** | No login required |
|
||||||
| **Staff (Sale)** | Truy cập Operation Module |
|
| **Staff (Sale)** | Access Operation Module |
|
||||||
| **Manager** | Truy cập Setup Module |
|
| **Manager** | Access Setup Module |
|
||||||
| **Admin** | Toàn quyền, bao gồm User & Security |
|
| **Admin** | Full access, including User & Security |
|
||||||
|
|
||||||
### Quy tắc bảo mật:
|
### Security Rules:
|
||||||
- Nhân viên & admin bắt buộc phải login
|
- Staff & admin must login
|
||||||
- Quyền thao tác phụ thuộc vào role
|
- Operation permissions depend on role
|
||||||
- Session timeout sau 30 phút không hoạt động
|
- Session timeout after 30 minutes of inactivity
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 3. Tóm tắt theo góc nhìn Admin
|
# 3. Summary from Admin Perspective
|
||||||
|
|
||||||
| Module | Quyền Admin | Nội dung |
|
| Module | Admin Permissions | Content |
|
||||||
|--------|-------------|----------|
|
|--------|-------------|----------|
|
||||||
| Room Setup | Full | CRUD phòng |
|
| Room Setup | Full | CRUD rooms |
|
||||||
| Service Setup | Full | CRUD dịch vụ |
|
| Service Setup | Full | CRUD services |
|
||||||
| Promotion Setup | Full | CRUD khuyến mãi |
|
| Promotion Setup | Full | CRUD promotions |
|
||||||
| Booking Management | Full | Xem, duyệt, hủy booking |
|
| Booking Management | Full | View, approve, cancel bookings |
|
||||||
| Check-in / Check-out | Full | Quản lý vận hành |
|
| Check-in / Check-out | Full | Operations management |
|
||||||
| Service Usage | Full | Ghi log dịch vụ |
|
| Service Usage | Full | Service logging |
|
||||||
| Reports | Full | Thống kê, xuất file |
|
| Reports | Full | Statistics, export files |
|
||||||
| User Management | Full | Quản lý nhân viên |
|
| User Management | Full | Staff management |
|
||||||
| Security | Full | Role, phân quyền |
|
| Security | Full | Roles, permissions |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 4. Kết luận
|
# 4. Conclusion
|
||||||
Phân tích trên giúp xác định đầy đủ các chức năng cần triển khai cho **Admin / Manager / Staff** trong hệ thống quản lý khách sạn.
|
The above analysis helps identify all the functions that need to be implemented for **Admin / Manager / Staff** in the hotel management system.
|
||||||
Tài liệu có thể được sử dụng để xây dựng database, API, UI/UX, và phân quyền hệ thống.
|
This document can be used to build the database, API, UI/UX, and system permissions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
## Chức năng 1: Layout cơ bản (Header, Footer, Navbar, SidebarAdmin)
|
## Function 1: Basic Layout (Header, Footer, Navbar, SidebarAdmin)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Tạo layout nền tảng cho toàn bộ hệ thống và cấu trúc render nội dung theo route.
|
Create a foundational layout for the entire system and structure content rendering by route.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo thư mục:
|
- Create directory:
|
||||||
```
|
```
|
||||||
src/components/layouts/
|
src/components/layouts/
|
||||||
```
|
```
|
||||||
- Bao gồm:
|
- Include:
|
||||||
+ Header.jsx
|
+ Header.jsx
|
||||||
+ Footer.jsx
|
+ Footer.jsx
|
||||||
+ Navbar.jsx
|
+ Navbar.jsx
|
||||||
+ SidebarAdmin.jsx
|
+ SidebarAdmin.jsx
|
||||||
+ LayoutMain.jsx
|
+ LayoutMain.jsx
|
||||||
- Dùng <Outlet /> trong LayoutMain để render nội dung động.
|
- Use <Outlet /> in LayoutMain to render dynamic content.
|
||||||
- Navbar thay đổi tùy trạng thái đăng nhập:
|
- Navbar changes based on login status:
|
||||||
+ Nếu chưa login → hiển thị nút “Đăng nhập / Đăng ký”.
|
+ If not logged in → display "Login / Register" button.
|
||||||
+ Nếu đã login → hiển thị avatar, tên user và nút “Đăng xuất”.
|
+ If logged in → display avatar, user name and "Logout" button.
|
||||||
- SidebarAdmin chỉ hiển thị với role = admin.
|
- SidebarAdmin only displays with role = admin.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Layout tổng thể hiển thị ổn định.
|
1. Overall layout displays stably.
|
||||||
2. Navbar hiển thị nội dung động theo trạng thái người dùng.
|
2. Navbar displays dynamic content based on user status.
|
||||||
3. Giao diện responsive, tương thích desktop/mobile.
|
3. Responsive interface, compatible with desktop/mobile.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 2: Cấu hình Routing (react-router-dom)
|
## Function 2: Routing Configuration (react-router-dom)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Thiết lập hệ thống định tuyến chuẩn, có bảo vệ route theo role.
|
Set up a standard routing system with role-based route protection.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Cấu trúc route chính:
|
- Main route structure:
|
||||||
```
|
```
|
||||||
<Route path="/" element={<LayoutMain />}>
|
<Route path="/" element={<LayoutMain />}>
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
@@ -51,25 +51,25 @@
|
|||||||
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||||
<Route path="/admin/*" element={<AdminRoute><AdminModule /></AdminRoute>} />
|
<Route path="/admin/*" element={<AdminRoute><AdminModule /></AdminRoute>} />
|
||||||
```
|
```
|
||||||
- Dùng ProtectedRoute và AdminRoute để kiểm tra:
|
- Use ProtectedRoute and AdminRoute to check:
|
||||||
+ isAuthenticated
|
+ isAuthenticated
|
||||||
+ role === "admin"
|
+ role === "admin"
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Người dùng không đăng nhập bị redirect về /login.
|
1. Unauthenticated users are redirected to /login.
|
||||||
2. AdminRoute chỉ cho phép admin truy cập.
|
2. AdminRoute only allows admin access.
|
||||||
3. Tất cả route hoạt động mượt, không lỗi vòng lặp redirect.
|
3. All routes work smoothly, no redirect loop errors.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 3: useAuthStore (Zustand Store)
|
## Function 3: useAuthStore (Zustand Store)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Quản lý trạng thái xác thực toàn cục (token, userInfo, role).
|
Manage global authentication state (token, userInfo, role).
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo src/stores/useAuthStore.js
|
- Create src/stores/useAuthStore.js
|
||||||
- Cấu trúc:
|
- Structure:
|
||||||
```
|
```
|
||||||
const useAuthStore = create((set) => ({
|
const useAuthStore = create((set) => ({
|
||||||
token: localStorage.getItem("token") || null,
|
token: localStorage.getItem("token") || null,
|
||||||
@@ -82,132 +82,132 @@
|
|||||||
resetPassword: async (payload) => { ... },
|
resetPassword: async (payload) => { ... },
|
||||||
}));
|
}));
|
||||||
```
|
```
|
||||||
- Khi đăng nhập thành công:
|
- When login succeeds:
|
||||||
+ Lưu token + userInfo vào localStorage.
|
+ Save token + userInfo to localStorage.
|
||||||
- Khi logout:
|
- When logout:
|
||||||
+ Xóa localStorage và reset state.
|
+ Clear localStorage and reset state.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Toàn bộ thông tin user được quản lý tập trung.
|
1. All user information is managed centrally.
|
||||||
2. Duy trì đăng nhập sau khi reload trang.
|
2. Maintain login after page reload.
|
||||||
3. Dễ dàng truy cập userInfo trong mọi component.
|
3. Easy access to userInfo in any component.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 4: Form Login
|
## Function 4: Login Form
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng đăng nhập hệ thống.
|
Allow users to log into the system.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo LoginPage.jsx
|
- Create LoginPage.jsx
|
||||||
- Dùng React Hook Form + Yup validate:
|
- Use React Hook Form + Yup validation:
|
||||||
+ Email hợp lệ
|
+ Valid email
|
||||||
+ Mật khẩu ≥ 8 ký tự
|
+ Password ≥ 8 characters
|
||||||
- API:
|
- API:
|
||||||
```
|
```
|
||||||
POST /api/auth/login
|
POST /api/auth/login
|
||||||
```
|
```
|
||||||
- Sau khi đăng nhập thành công:
|
- After successful login:
|
||||||
+ Lưu token vào localStorage.
|
+ Save token to localStorage.
|
||||||
+ Gọi setUser() để cập nhật Zustand.
|
+ Call setUser() to update Zustand.
|
||||||
+ Redirect về /dashboard.
|
+ Redirect to /dashboard.
|
||||||
+ Gửi email POST /api/notify/login-success.
|
+ Send email POST /api/notify/login-success.
|
||||||
- UX nâng cao:
|
- Enhanced UX:
|
||||||
+ Nút loading khi đang gửi form.
|
+ Loading button when submitting form.
|
||||||
+ “Hiện/Ẩn mật khẩu”.
|
+ "Show/Hide password".
|
||||||
+ “Nhớ đăng nhập” → lưu 7 ngày.
|
+ "Remember me" → save for 7 days.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Đăng nhập hoạt động mượt, hiển thị thông báo lỗi rõ ràng.
|
1. Login works smoothly, displays clear error messages.
|
||||||
2. Email được gửi khi login thành công.
|
2. Email is sent when login succeeds.
|
||||||
3. Chuyển hướng đúng theo vai trò user.
|
3. Redirect correctly based on user role.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 5: Form Register
|
## Function 5: Register Form
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng đăng ký tài khoản mới.
|
Allow users to register a new account.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo RegisterPage.jsx
|
- Create RegisterPage.jsx
|
||||||
- Dùng React Hook Form + Yup validate:
|
- Use React Hook Form + Yup validation:
|
||||||
+ Họ tên không rỗng
|
+ Full name not empty
|
||||||
+ Email hợp lệ
|
+ Valid email
|
||||||
+ Mật khẩu ≥ 8 ký tự, có ký tự đặc biệt
|
+ Password ≥ 8 characters, contains special characters
|
||||||
- API:
|
- API:
|
||||||
```
|
```
|
||||||
POST /api/auth/register
|
POST /api/auth/register
|
||||||
```
|
```
|
||||||
- Sau khi đăng ký thành công:
|
- After successful registration:
|
||||||
+ Hiển thị toast “Đăng ký thành công, vui lòng đăng nhập”.
|
+ Display toast "Registration successful, please login".
|
||||||
+ Redirect về /login.
|
+ Redirect to /login.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Người dùng tạo tài khoản mới thành công.
|
1. Users can create new accounts successfully.
|
||||||
2. Validate chặt chẽ, UX mượt mà.
|
2. Strict validation, smooth UX.
|
||||||
3. Giao diện thống nhất với form login.
|
3. Interface consistent with login form.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 6: Quên mật khẩu (Forgot Password)
|
## Function 6: Forgot Password
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cung cấp chức năng gửi email reset mật khẩu.
|
Provide functionality to send password reset email.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo ForgotPasswordPage.jsx
|
- Create ForgotPasswordPage.jsx
|
||||||
- API:
|
- API:
|
||||||
```
|
```
|
||||||
POST /api/auth/forgot-password
|
POST /api/auth/forgot-password
|
||||||
```
|
```
|
||||||
- Sau khi gửi thành công:
|
- After successful send:
|
||||||
+ Hiển thị thông báo “Vui lòng kiểm tra email để đặt lại mật khẩu.”
|
+ Display message "Please check your email to reset password."
|
||||||
+ Backend gửi link reset có token dạng:
|
+ Backend sends reset link with token:
|
||||||
```
|
```
|
||||||
https://domain.com/reset-password/:token
|
https://domain.com/reset-password/:token
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Gửi email thành công.
|
1. Email sent successfully.
|
||||||
2. UX rõ ràng, có loading và thông báo lỗi.
|
2. Clear UX, with loading and error messages.
|
||||||
3. Giao diện thân thiện.
|
3. User-friendly interface.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 7: Đặt lại mật khẩu (Reset Password)
|
## Function 7: Reset Password
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng đổi mật khẩu thông qua link email.
|
Allow users to change password through email link.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo ResetPasswordPage.jsx
|
- Create ResetPasswordPage.jsx
|
||||||
- Validate:
|
- Validation:
|
||||||
+ Mật khẩu mới ≥ 8 ký tự, chứa ký tự đặc biệt
|
+ New password ≥ 8 characters, contains special characters
|
||||||
+ Nhập lại mật khẩu trùng khớp
|
+ Confirm password matches
|
||||||
- API:
|
- API:
|
||||||
```
|
```
|
||||||
POST /api/auth/reset-password
|
POST /api/auth/reset-password
|
||||||
```
|
```
|
||||||
- Sau khi đổi mật khẩu thành công:
|
- After successful password change:
|
||||||
+ Gửi email xác nhận POST /api/notify/reset-success.
|
+ Send confirmation email POST /api/notify/reset-success.
|
||||||
+ Redirect về /login.
|
+ Redirect to /login.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Mật khẩu được cập nhật thành công.
|
1. Password updated successfully.
|
||||||
2. Gửi email thông báo thành công.
|
2. Success notification email sent.
|
||||||
3. Bảo vệ token hết hạn (invalid token → redirect về forgot-password).
|
3. Protect expired token (invalid token → redirect to forgot-password).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 8: Phân quyền & Bảo vệ route (ProtectedRoute / AdminRoute)
|
## Function 8: Permissions & Route Protection (ProtectedRoute / AdminRoute)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Chặn truy cập trái phép và bảo vệ các route quan trọng.
|
Block unauthorized access and protect important routes.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Tạo component ProtectedRoute.jsx:
|
- Create ProtectedRoute.jsx component:
|
||||||
```
|
```
|
||||||
const ProtectedRoute = ({ children }) => {
|
const ProtectedRoute = ({ children }) => {
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
@@ -216,13 +216,13 @@
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
- Tạo AdminRoute.jsx:
|
- Create AdminRoute.jsx:
|
||||||
```
|
```
|
||||||
const AdminRoute = ({ children }) => {
|
const AdminRoute = ({ children }) => {
|
||||||
const { userInfo } = useAuthStore();
|
const { userInfo } = useAuthStore();
|
||||||
return userInfo?.role === "admin" ? children : <Navigate to="/" replace />;
|
return userInfo?.role === "admin" ? children : <Navigate to="/" replace />;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Chỉ người dùng hợp lệ mới truy cập được route quan trọng.
|
1. Only valid users can access important routes.
|
||||||
2. AdminRoute đảm bảo bảo mật cho module quản trị.
|
2. AdminRoute ensures security for admin module.
|
||||||
|
|||||||
@@ -1,182 +1,182 @@
|
|||||||
# Review System
|
# Review System
|
||||||
|
|
||||||
## Chức năng 1: HomePage – Trang chủ hiển thị phòng nổi bật
|
## Function 1: HomePage – Homepage displaying featured rooms
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Tạo giao diện trang chủ giới thiệu phòng nổi bật, banner và điều hướng đến danh sách phòng.
|
Create a homepage interface introducing featured rooms, banner and navigation to room list.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /
|
1. Route: /
|
||||||
2. Banner:
|
2. Banner:
|
||||||
```
|
```
|
||||||
GET /api/banners?position=home
|
GET /api/banners?position=home
|
||||||
```
|
```
|
||||||
- Nếu không có banner → hiển thị ảnh mặc định.
|
- If no banner → display default image.
|
||||||
- Có thể dùng Carousel hoặc ảnh tĩnh.
|
- Can use Carousel or static image.
|
||||||
3. Phòng nổi bật:
|
3. Featured rooms:
|
||||||
```
|
```
|
||||||
GET /api/rooms?featured=true
|
GET /api/rooms?featured=true
|
||||||
```
|
```
|
||||||
- Hiển thị 4–6 phòng bằng component RoomCard.
|
- Display 4–6 rooms using RoomCard component.
|
||||||
- Nút “Xem tất cả phòng” → điều hướng /rooms.
|
- "View all rooms" button → navigate to /rooms.
|
||||||
4. Loading skeleton trong khi chờ dữ liệu.
|
4. Loading skeleton while waiting for data.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Trang chủ hiển thị banner và danh sách phòng nổi bật rõ ràng.
|
1. Homepage displays banner and featured room list clearly.
|
||||||
2. Khi không có banner → ảnh fallback được hiển thị.
|
2. When no banner → fallback image is displayed.
|
||||||
3. Phòng nổi bật load từ API, giới hạn 4–6 phòng.
|
3. Featured rooms load from API, limited to 4–6 rooms.
|
||||||
4. UX mượt, có skeleton khi load.
|
4. Smooth UX, with skeleton when loading.
|
||||||
5. Nút “Xem tất cả phòng” điều hướng chính xác đến /rooms.
|
5. "View all rooms" button navigates correctly to /rooms.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 2: RoomListPage – Danh sách & Bộ lọc phòng
|
## Function 2: RoomListPage – Room List & Filters
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Hiển thị danh sách phòng, cho phép người dùng lọc theo loại, giá, số người và phân trang.
|
Display room list, allow users to filter by type, price, number of guests and pagination.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /rooms
|
1. Route: /rooms
|
||||||
2. Bộ lọc (component RoomFilter):
|
2. Filters (RoomFilter component):
|
||||||
- Trường lọc: loại phòng, giá min–max, số người.
|
- Filter fields: room type, min–max price, number of guests.
|
||||||
- Khi submit → gọi API:
|
- On submit → call API:
|
||||||
```
|
```
|
||||||
GET /api/rooms?type=&minPrice=&maxPrice=&capacity=&page=
|
GET /api/rooms?type=&minPrice=&maxPrice=&capacity=&page=
|
||||||
```
|
```
|
||||||
- Lưu bộ lọc vào URL query.
|
- Save filters to URL query.
|
||||||
- Nút “Reset” để xóa toàn bộ bộ lọc.
|
- "Reset" button to clear all filters.
|
||||||
3. Phân trang (Pagination component).
|
3. Pagination (Pagination component).
|
||||||
4. Hiển thị danh sách bằng RoomCard.
|
4. Display list using RoomCard.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Danh sách phòng hiển thị chính xác theo filter.
|
1. Room list displays accurately according to filter.
|
||||||
2. Bộ lọc hoạt động mượt, có thể reset dễ dàng.
|
2. Filters work smoothly, can reset easily.
|
||||||
3. Phân trang hiển thị chính xác số trang.
|
3. Pagination displays correct page numbers.
|
||||||
4. Filter được lưu trong URL (giúp reload không mất).
|
4. Filters saved in URL (helps reload without losing).
|
||||||
5. Giao diện responsive, dễ đọc, không bị vỡ.
|
5. Responsive interface, easy to read, no breakage.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 3: RoomDetailPage – Chi tiết phòng & Đánh giá
|
## Function 3: RoomDetailPage – Room Details & Reviews
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Tạo trang chi tiết phòng đầy đủ thông tin, hình ảnh, tiện ích và khu vực đánh giá.
|
Create a complete room detail page with information, images, amenities and review section.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /rooms/:id
|
1. Route: /rooms/:id
|
||||||
2. Phần nội dung:
|
2. Content section:
|
||||||
-Thông tin phòng (ảnh, mô tả, giá, tiện ích)
|
- Room information (images, description, price, amenities)
|
||||||
- RoomGallery: Carousel ảnh
|
- RoomGallery: Image carousel
|
||||||
- RoomAmenities: danh sách tiện ích
|
- RoomAmenities: amenities list
|
||||||
- Nút “Đặt ngay” → điều hướng /booking/:roomId
|
- "Book Now" button → navigate to /booking/:roomId
|
||||||
3. Review Section:
|
3. Review Section:
|
||||||
- Lấy danh sách review đã duyệt:
|
- Get approved review list:
|
||||||
```
|
```
|
||||||
GET /api/rooms/:id/reviews
|
GET /api/rooms/:id/reviews
|
||||||
```
|
```
|
||||||
- Nếu người dùng đã từng đặt phòng:
|
- If user has booked the room before:
|
||||||
```
|
```
|
||||||
POST /api/reviews
|
POST /api/reviews
|
||||||
```
|
```
|
||||||
4. Component RatingStars + ReviewForm.
|
4. RatingStars + ReviewForm component.
|
||||||
5. Nếu chưa đăng nhập → hiển thị “Vui lòng đăng nhập để đánh giá”.
|
5. If not logged in → display "Please login to review".
|
||||||
6. Tính trung bình điểm review.
|
6. Calculate average review rating.
|
||||||
7. Loading skeleton khi chờ review.
|
7. Loading skeleton when waiting for reviews.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Hiển thị đầy đủ ảnh, mô tả, tiện ích phòng.
|
1. Displays complete images, description, room amenities.
|
||||||
2. Carousel hoạt động mượt mà.
|
2. Carousel works smoothly.
|
||||||
3. Review hiển thị đúng, có trung bình số sao.
|
3. Reviews display correctly, with average star rating.
|
||||||
4. Người đã đặt có thể viết review (sau duyệt).
|
4. Users who have booked can write reviews (after approval).
|
||||||
5. Nút “Đặt ngay” điều hướng chính xác đến form booking.
|
5. "Book Now" button navigates correctly to booking form.
|
||||||
6. Skeleton hiển thị khi chờ dữ liệu.
|
6. Skeleton displays when waiting for data.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 4: SearchRoom – Tìm phòng trống
|
## Function 4: SearchRoom – Find available rooms
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng tìm phòng trống theo ngày và loại phòng.
|
Allow users to find available rooms by date and room type.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Form tìm kiếm (ở HomePage hoặc RoomListPage):
|
1. Search form (on HomePage or RoomListPage):
|
||||||
- Input: ngày đến (from), ngày đi (to), loại phòng.
|
- Input: arrival date (from), departure date (to), room type.
|
||||||
2. API:
|
2. API:
|
||||||
```
|
```
|
||||||
GET /api/rooms/available?from=&to=&type=
|
GET /api/rooms/available?from=&to=&type=
|
||||||
```
|
```
|
||||||
3. Validate:
|
3. Validation:
|
||||||
- from < to
|
- from < to
|
||||||
- from không nhỏ hơn hôm nay.
|
- from not less than today.
|
||||||
4. Kết quả:
|
4. Results:
|
||||||
- Hiển thị danh sách bằng RoomCard.
|
- Display list using RoomCard.
|
||||||
- Nếu không có kết quả → “Không tìm thấy phòng phù hợp”.
|
- If no results → "No matching rooms found".
|
||||||
5. Dùng react-datepicker hoặc react-day-picker.
|
5. Use react-datepicker or react-day-picker.
|
||||||
6. Loading spinner khi đang tìm.
|
6. Loading spinner while searching.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Form tìm phòng hoạt động, validate chính xác.
|
1. Room search form works, validates correctly.
|
||||||
2. Khi bấm tìm → hiển thị danh sách phòng trống.
|
2. When clicking search → displays available room list.
|
||||||
3. Nếu không có kết quả → thông báo thân thiện.
|
3. If no results → friendly message.
|
||||||
4. Loading hiển thị rõ trong lúc chờ.
|
4. Loading displays clearly while waiting.
|
||||||
5. Tìm theo ngày & loại phòng chính xác từ backend.
|
5. Search by date & room type accurately from backend.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 5: Wishlist – Danh sách yêu thích
|
## Function 5: Wishlist – Favorites list
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng thêm, bỏ hoặc xem danh sách phòng yêu thích.
|
Allow users to add, remove or view favorite rooms list.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. API:
|
1. API:
|
||||||
```
|
```
|
||||||
POST /api/favorites/:roomId # Thêm
|
POST /api/favorites/:roomId # Add
|
||||||
DELETE /api/favorites/:roomId # Xóa
|
DELETE /api/favorites/:roomId # Remove
|
||||||
GET /api/favorites # Lấy danh sách yêu thích
|
GET /api/favorites # Get favorites list
|
||||||
```
|
```
|
||||||
2. UI:
|
2. UI:
|
||||||
- FavoriteButton (icon ❤️):
|
- FavoriteButton (icon ❤️):
|
||||||
+ Nếu yêu thích → tô đỏ
|
+ If favorited → filled red
|
||||||
+ Nếu chưa → viền xám
|
+ If not → gray border
|
||||||
- Tooltip: “Thêm vào yêu thích” / “Bỏ yêu thích”
|
- Tooltip: "Add to favorites" / "Remove from favorites"
|
||||||
3. Nếu chưa đăng nhập:
|
3. If not logged in:
|
||||||
- Lưu tạm trong localStorage (guestFavorites)
|
- Save temporarily in localStorage (guestFavorites)
|
||||||
- Khi đăng nhập → đồng bộ với server.
|
- When logged in → sync with server.
|
||||||
4. Toast thông báo khi thêm/bỏ yêu thích.
|
4. Toast notification when adding/removing favorites.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Nút ❤️ hoạt động đúng trạng thái (đỏ / xám).
|
1. ❤️ button works correctly (red / gray).
|
||||||
2. Người chưa đăng nhập vẫn có thể lưu tạm yêu thích.
|
2. Unauthenticated users can still save favorites temporarily.
|
||||||
3. Khi đăng nhập → danh sách đồng bộ với backend.
|
3. When logged in → list syncs with backend.
|
||||||
4. Toast hiển thị “Đã thêm vào yêu thích” / “Đã bỏ yêu thích”.
|
4. Toast displays "Added to favorites" / "Removed from favorites".
|
||||||
5. API hoạt động đúng, không lỗi 401 khi đăng nhập hợp lệ.
|
5. API works correctly, no 401 error when logged in validly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 6: Tối ưu UI/UX & Performance
|
## Function 6: UI/UX & Performance Optimization
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cải thiện trải nghiệm người dùng, tối ưu tốc độ tải và khả năng hiển thị responsive.
|
Improve user experience, optimize loading speed and responsive display capability.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Loading skeleton khi fetch phòng hoặc review.
|
1. Loading skeleton when fetching rooms or reviews.
|
||||||
2. Debounce khi nhập giá để tránh gọi API liên tục.
|
2. Debounce when entering price to avoid continuous API calls.
|
||||||
3. Infinite scroll (tùy chọn) thay cho pagination.
|
3. Infinite scroll (optional) instead of pagination.
|
||||||
4. Responsive layout:
|
4. Responsive layout:
|
||||||
- Desktop: 3–4 cột
|
- Desktop: 3–4 columns
|
||||||
- Tablet: 2 cột
|
- Tablet: 2 columns
|
||||||
- Mobile: 1 cột
|
- Mobile: 1 column
|
||||||
5. Empty states:
|
5. Empty states:
|
||||||
- Không có phòng → hiển thị ảnh minh họa + dòng “Không tìm thấy phòng phù hợp”.
|
- No rooms → display illustration + "No matching rooms found" message.
|
||||||
- Không có review → “Hãy là người đầu tiên đánh giá!”.
|
- No reviews → "Be the first to review!".
|
||||||
6. Toast thông báo khi thêm yêu thích, gửi review, lỗi mạng.
|
6. Toast notifications when adding favorites, submitting reviews, network errors.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Trang hoạt động mượt, có skeleton khi chờ dữ liệu.
|
1. Page works smoothly, has skeleton when waiting for data.
|
||||||
2. Tốc độ phản hồi nhanh (debounce hoạt động).
|
2. Fast response speed (debounce works).
|
||||||
3. Responsive trên mọi kích thước màn hình.
|
3. Responsive on all screen sizes.
|
||||||
4. Các empty state hiển thị thân thiện.
|
4. Empty states display friendly.
|
||||||
5. Toast thông báo rõ ràng, UX thân thiện.
|
5. Toast notifications clear, friendly UX.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,144 +1,144 @@
|
|||||||
# Booking & Payment
|
# Booking & Payment
|
||||||
|
|
||||||
## Chức năng 1: BookingPage – Form Đặt phòng
|
## Function 1: BookingPage – Booking Form
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Xây dựng form đặt phòng đầy đủ thông tin, xác thực dữ liệu, tính tổng tiền theo số ngày, và gửi yêu cầu đặt.
|
Build a complete booking form with information, data validation, calculate total by number of days, and send booking request.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route:
|
1. Route:
|
||||||
```
|
```
|
||||||
/booking/:roomId
|
/booking/:roomId
|
||||||
```
|
```
|
||||||
2. Khi user click “Đặt ngay” ở RoomDetailPage → chuyển sang BookingPage.
|
2. When user clicks "Book Now" on RoomDetailPage → navigate to BookingPage.
|
||||||
3. Hiển thị:
|
3. Display:
|
||||||
- Ảnh phòng, tên phòng, giá/đêm
|
- Room image, room name, price/night
|
||||||
- Thông tin người dùng (tự động điền nếu đã login)
|
- User information (auto-fill if logged in)
|
||||||
- Form:
|
- Form:
|
||||||
+ Ngày check-in / check-out (DateRangePicker)
|
+ Check-in / check-out date (DateRangePicker)
|
||||||
+ Số người
|
+ Number of guests
|
||||||
+ Ghi chú
|
+ Notes
|
||||||
+ Phương thức thanh toán:
|
+ Payment method:
|
||||||
1. Thanh toán tại chỗ
|
1. Pay at hotel
|
||||||
2. Chuyển khoản (hiển thị QR + hướng dẫn)
|
2. Bank transfer (display QR + instructions)
|
||||||
4. Validate bằng Yup + React Hook Form:
|
4. Validate using Yup + React Hook Form:
|
||||||
- Check-in < Check-out
|
- Check-in < Check-out
|
||||||
- Không bỏ trống ngày
|
- Dates not empty
|
||||||
- Có chọn phương thức thanh toán
|
- Payment method selected
|
||||||
5. Tính tổng tiền:
|
5. Calculate total:
|
||||||
```
|
```
|
||||||
total = room.price * (số ngày ở)
|
total = room.price * (number of nights)
|
||||||
```
|
```
|
||||||
6. Nút “Đặt phòng”:
|
6. "Book" button:
|
||||||
- Loading spinner
|
- Loading spinner
|
||||||
- Disable khi đang submit
|
- Disable when submitting
|
||||||
|
|
||||||
7. Nếu chưa đăng nhập → redirect /login.
|
7. If not logged in → redirect to /login.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 2: Booking API (Giao tiếp backend)
|
## Function 2: Booking API (Backend communication)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Kết nối và xử lý API liên quan đến đặt phòng.
|
Connect and handle APIs related to booking.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
🔧 Endpoints:
|
🔧 Endpoints:
|
||||||
```
|
```
|
||||||
POST /api/bookings → Tạo booking
|
POST /api/bookings → Create booking
|
||||||
GET /api/bookings/me → Lấy danh sách booking của user
|
GET /api/bookings/me → Get user's booking list
|
||||||
PATCH /api/bookings/:id/cancel → Hủy booking
|
PATCH /api/bookings/:id/cancel → Cancel booking
|
||||||
GET /api/bookings/:id → Chi tiết booking
|
GET /api/bookings/:id → Booking details
|
||||||
GET /api/bookings/check/:bookingNumber → Tra cứu booking
|
GET /api/bookings/check/:bookingNumber → Look up booking
|
||||||
```
|
```
|
||||||
🔄 Luồng xử lý:
|
🔄 Processing flow:
|
||||||
1. Frontend gọi POST /api/bookings
|
1. Frontend calls POST /api/bookings
|
||||||
2. Backend kiểm tra phòng trống:
|
2. Backend checks room availability:
|
||||||
```
|
```
|
||||||
GET /api/rooms/available?roomId=...&from=...&to=...
|
GET /api/rooms/available?roomId=...&from=...&to=...
|
||||||
```
|
```
|
||||||
3. Nếu trống → tạo booking
|
3. If available → create booking
|
||||||
- Nếu trùng lịch → trả 409 “Phòng đã được đặt trong thời gian này”
|
- If schedule conflict → return 409 "Room already booked during this time"
|
||||||
4. Gửi email xác nhận booking (nếu cần)
|
4. Send booking confirmation email (if needed)
|
||||||
5. Trả về dữ liệu booking để hiển thị /booking-success/:id.
|
5. Return booking data to display /booking-success/:id.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 3: BookingSuccess – Trang kết quả sau đặt phòng
|
## Function 3: BookingSuccess – Page after booking
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Hiển thị kết quả đặt phòng thành công và các hành động tiếp theo.
|
Display successful booking result and next actions.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /booking-success/:id
|
1. Route: /booking-success/:id
|
||||||
2. Gọi GET /api/bookings/:id → hiển thị chi tiết
|
2. Call GET /api/bookings/:id → display details
|
||||||
3. Nút:
|
3. Buttons:
|
||||||
- “Xem đơn của tôi” → /my-bookings
|
- "View my bookings" → /my-bookings
|
||||||
- “Về trang chủ” → /
|
- "Go to home" → /
|
||||||
4. Nếu phương thức là Chuyển khoản:
|
4. If payment method is Bank transfer:
|
||||||
+ Hiển thị QR code ngân hàng
|
+ Display bank QR code
|
||||||
+ Cho phép upload ảnh xác nhận
|
+ Allow upload confirmation image
|
||||||
+ Gọi POST /api/notify/payment khi người dùng xác nhận đã chuyển khoản.
|
+ Call POST /api/notify/payment when user confirms transfer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 4: MyBookingsPage – Danh sách đơn đặt của người
|
## Function 4: MyBookingsPage – User's booking list
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Hiển thị toàn bộ các đơn đặt của user + cho phép hủy đơn.
|
Display all user's bookings + allow canceling bookings.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /my-bookings
|
1. Route: /my-bookings
|
||||||
2. API: GET /api/bookings/me
|
2. API: GET /api/bookings/me
|
||||||
3. Hiển thị danh sách booking:
|
3. Display booking list:
|
||||||
- Phòng, ngày nhận/trả, tổng tiền
|
- Room, check-in/check-out dates, total amount
|
||||||
- Trạng thái:
|
- Status:
|
||||||
🟡 pending
|
🟡 pending
|
||||||
🟢 confirmed
|
🟢 confirmed
|
||||||
🔴 cancelled
|
🔴 cancelled
|
||||||
4. Nút “Hủy đặt phòng”:
|
4. "Cancel booking" button:
|
||||||
1. window.confirm("Bạn có chắc muốn hủy không?")
|
1. window.confirm("Are you sure you want to cancel?")
|
||||||
2. Gọi PATCH /api/bookings/:id/cancel (hoặc DELETE /api/bookings/:id tùy implement)
|
2. Call PATCH /api/bookings/:id/cancel (or DELETE /api/bookings/:id depending on implementation)
|
||||||
3. Logic hủy:
|
3. Cancel logic:
|
||||||
- Giữ 20% giá trị đơn
|
- Keep 20% of order value
|
||||||
- Hoàn 80% còn lại cho user
|
- Refund remaining 80% to user
|
||||||
- Cập nhật trạng thái phòng về available
|
- Update room status to available
|
||||||
4. Hiển thị toast “Đơn đã được hủy thành công”
|
4. Display toast "Booking cancelled successfully"
|
||||||
5. Cho phép xem chi tiết booking:
|
5. Allow viewing booking details:
|
||||||
- Route: /bookings/:id
|
- Route: /bookings/:id
|
||||||
- Gọi GET /api/bookings/:id
|
- Call GET /api/bookings/:id
|
||||||
- Hiển thị chi tiết phòng, thông tin user, tổng tiền, status.
|
- Display room details, user information, total amount, status.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 5: Thanh toán (Giả lập Payment)
|
## Function 5: Payment (Simulated Payment)
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng chọn phương thức thanh toán và xác nhận thanh toán.
|
Allow users to select payment method and confirm payment.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
- Phương thức:
|
- Payment methods:
|
||||||
1. Thanh toán tại chỗ
|
1. Pay at hotel
|
||||||
- Booking được tạo với status = "pending"
|
- Booking created with status = "pending"
|
||||||
2. Chuyển khoản
|
2. Bank transfer
|
||||||
- Hiển thị mã QR ngân hàng (tĩnh hoặc từ API)
|
- Display bank QR code (static or from API)
|
||||||
- Upload ảnh biên lai (image upload)
|
- Upload receipt image (image upload)
|
||||||
- Sau khi upload → gọi POST /api/notify/payment gửi email xác nhận
|
- After upload → call POST /api/notify/payment send confirmation email
|
||||||
- Cập nhật status = "confirmed"
|
- Update status = "confirmed"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 6: UX & Hiệu năng
|
## Function 6: UX & Performance
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cải thiện trải nghiệm người dùng và tính trực quan.
|
Improve user experience and intuitiveness.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Toasts (react-hot-toast hoặc sonner)
|
1. Toasts (react-hot-toast or sonner)
|
||||||
2. Loading spinner rõ ràng
|
2. Clear loading spinner
|
||||||
3. DateRangePicker cho chọn ngày
|
3. DateRangePicker for date selection
|
||||||
4. Form được validate đầy đủ (và báo lỗi chi tiết)
|
4. Form fully validated (and detailed error messages)
|
||||||
5. Focus input đầu tiên
|
5. Focus first input
|
||||||
6. Tự động redirect khi đặt thành công / hủy đơn
|
6. Auto redirect when booking succeeds / canceling booking
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,72 +1,72 @@
|
|||||||
# Review System
|
# Review System
|
||||||
|
|
||||||
## Chức năng 1: ReviewPage – Trang người dùng đánh giá phòng
|
## Function 1: ReviewPage – User Room Review Page
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép người dùng viết đánh giá cho những phòng họ đã đặt thành công.
|
Allow users to write reviews for rooms they have successfully booked.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /reviews
|
1. Route: /reviews
|
||||||
2. Gọi API:
|
2. API Calls:
|
||||||
```
|
```
|
||||||
GET /api/bookings/me → Lấy danh sách phòng người dùng đã đặt.
|
GET /api/bookings/me → Get list of rooms user has booked.
|
||||||
POST /api/reviews → Gửi đánh giá.
|
POST /api/reviews → Submit review.
|
||||||
```
|
```
|
||||||
3. Giao diện:
|
3. Interface:
|
||||||
- Hiển thị danh sách phòng đã đặt (tên, ngày ở, trạng thái)
|
- Display list of booked rooms (name, stay dates, status)
|
||||||
- Nút “Đánh giá” (hiện nếu chưa đánh giá phòng đó)
|
- "Review" button (shown if room not yet reviewed)
|
||||||
4. Khi nhấn “Đánh giá” → mở Modal:
|
4. When clicking "Review" → open Modal:
|
||||||
- Input chọn số sao (⭐ 1–5)
|
- Input to select star rating (⭐ 1–5)
|
||||||
- Textarea nhập nội dung bình luận
|
- Textarea to enter comment content
|
||||||
- Nút “Gửi đánh giá”
|
- "Submit Review" button
|
||||||
5. Validate:
|
5. Validation:
|
||||||
- Rating bắt buộc (1–5)
|
- Rating required (1–5)
|
||||||
- Comment không để trống
|
- Comment cannot be empty
|
||||||
6. Sau khi gửi thành công → toast thông báo “Đánh giá của bạn đang chờ duyệt”.
|
6. After successful submission → toast notification "Your review is pending approval".
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Người dùng chỉ thấy nút “Đánh giá” với phòng đã từng đặt.
|
1. Users only see "Review" button for rooms they have booked.
|
||||||
2. Modal mở ra và validate chính xác.
|
2. Modal opens and validates correctly.
|
||||||
3. Gửi thành công → review có trạng thái "pending".
|
3. Successful submission → review has status "pending".
|
||||||
4. Toast hiển thị thông báo hợp lý.
|
4. Toast displays appropriate notification.
|
||||||
5. Giao diện gọn, trực quan, không lỗi khi chưa có phòng nào đặt.
|
5. Clean, intuitive interface, no errors when no rooms booked.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 2: RoomDetailPage – Hiển thị danh sách đánh giá
|
## Function 2: RoomDetailPage – Display Review List
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Hiển thị danh sách các đánh giá đã được admin duyệt cho từng phòng.
|
Display list of reviews that have been approved by admin for each room.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /rooms/:id
|
1. Route: /rooms/:id
|
||||||
2. API:
|
2. API:
|
||||||
```
|
```
|
||||||
GET /api/reviews?roomId={id}&status=approved
|
GET /api/reviews?roomId={id}&status=approved
|
||||||
```
|
```
|
||||||
3. Hiển thị danh sách review:
|
3. Display review list:
|
||||||
- Avatar + tên người dùng
|
- Avatar + user name
|
||||||
- Số sao (⭐)
|
- Star rating (⭐)
|
||||||
- Nội dung bình luận
|
- Comment content
|
||||||
- Ngày đăng (createdAt)
|
- Post date (createdAt)
|
||||||
4. Tính và hiển thị điểm trung bình rating (VD: ⭐ 4.2 / 5)
|
4. Calculate and display average rating (e.g.: ⭐ 4.2 / 5)
|
||||||
5. Nếu chưa có review → hiển thị: “Chưa có đánh giá nào.”
|
5. If no reviews → display: "No reviews yet."
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Danh sách review hiển thị đúng theo phòng.
|
1. Review list displays correctly by room.
|
||||||
2. Chỉ review có status = approved được render.
|
2. Only reviews with status = approved are rendered.
|
||||||
3. Tính điểm trung bình chính xác (làm tròn 1 chữ số thập phân).
|
3. Calculate average rating accurately (rounded to 1 decimal place).
|
||||||
4. Hiển thị avatar, tên, sao, và ngày đầy đủ.
|
4. Display avatar, name, stars, and date completely.
|
||||||
5. Có thông báo “Chưa có đánh giá” khi danh sách trống.
|
5. Show "No reviews yet" message when list is empty.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 3: AdminReviewPage – Trang quản trị đánh giá
|
## Function 3: AdminReviewPage – Review Management Page
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cho phép Admin xem, duyệt hoặc từ chối các đánh giá người dùng gửi lên.
|
Allow Admin to view, approve or reject reviews submitted by users.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Route: /admin/reviews
|
1. Route: /admin/reviews
|
||||||
2. API:
|
2. API:
|
||||||
```
|
```
|
||||||
@@ -74,67 +74,67 @@
|
|||||||
PATCH /api/reviews/:id/approve
|
PATCH /api/reviews/:id/approve
|
||||||
PATCH /api/reviews/:id/reject
|
PATCH /api/reviews/:id/reject
|
||||||
```
|
```
|
||||||
3. Hành động:
|
3. Actions:
|
||||||
✅ Duyệt → review chuyển sang approved
|
✅ Approve → review changes to approved
|
||||||
❌ Từ chối → review chuyển sang rejected
|
❌ Reject → review changes to rejected
|
||||||
4. Sau khi duyệt → cập nhật giao diện và hiển thị toast thông báo.
|
4. After approval → update interface and display toast notification.
|
||||||
5. Có filter theo trạng thái (pending, approved, rejected).
|
5. Filter by status (pending, approved, rejected).
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Admin thấy đầy đủ danh sách review.
|
1. Admin sees complete review list.
|
||||||
2. Duyệt hoặc từ chối hoạt động đúng API.
|
2. Approve or reject works correctly with API.
|
||||||
3. Bảng tự cập nhật khi thay đổi trạng thái.
|
3. Table automatically updates when status changes.
|
||||||
4. Toast hiển thị rõ “Đã duyệt” hoặc “Đã từ chối”.
|
4. Toast clearly displays "Approved" or "Rejected".
|
||||||
5. Chỉ review approved mới hiển thị công khai cho người dùng.
|
5. Only approved reviews are displayed publicly to users.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 4: Bảo mật & Logic hiển thị
|
## Function 4: Security & Display Logic
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Đảm bảo chỉ người hợp lệ mới có thể gửi đánh giá và hệ thống hiển thị đúng dữ liệu.
|
Ensure only valid users can submit reviews and system displays correct data.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Kiểm tra quyền:
|
1. Permission check:
|
||||||
- Người dùng chưa đăng nhập → redirect /login
|
- User not logged in → redirect /login
|
||||||
- Người dùng chưa từng đặt phòng → không hiển thị nút “Đánh giá”
|
- User has never booked room → don't display "Review" button
|
||||||
2. Kiểm tra logic:
|
2. Logic check:
|
||||||
- Mỗi người chỉ được đánh giá 1 lần / phòng
|
- Each person can only review once per room
|
||||||
- Review mặc định status = pending
|
- Review default status = pending
|
||||||
3. Phân quyền:
|
3. Authorization:
|
||||||
- User: chỉ gửi review
|
- User: can only submit review
|
||||||
- Admin: duyệt / từ chối
|
- Admin: approve / reject
|
||||||
- Staff: chỉ xem
|
- Staff: view only
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. Người chưa đăng nhập không thể gửi review.
|
1. Users not logged in cannot submit reviews.
|
||||||
2. Mỗi phòng chỉ được review 1 lần bởi 1 user.
|
2. Each room can only be reviewed once by one user.
|
||||||
3. Dữ liệu hiển thị chính xác theo phân quyền.
|
3. Data displays correctly according to permissions.
|
||||||
4. Review chỉ xuất hiện công khai khi được duyệt.
|
4. Reviews only appear publicly when approved.
|
||||||
5. Không có lỗi logic hoặc hiển thị sai trạng thái.
|
5. No logic errors or incorrect status display.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chức năng 5: UX & Hiển thị tổng quan
|
## Function 5: UX & Overall Display
|
||||||
|
|
||||||
### Mục tiêu
|
### Objective
|
||||||
Cải thiện trải nghiệm người dùng và giao diện hiển thị đánh giá.
|
Improve user experience and review display interface.
|
||||||
|
|
||||||
#### Nhiệm vụ chi tiết
|
#### Detailed Tasks
|
||||||
1. Dùng component đánh giá sao trực quan (ví dụ react-rating-stars-component).
|
1. Use intuitive star rating component (e.g., react-rating-stars-component).
|
||||||
2. Format ngày tạo bằng:
|
2. Format creation date using:
|
||||||
```
|
```
|
||||||
new Date(createdAt).toLocaleDateString('vi-VN')
|
new Date(createdAt).toLocaleDateString('en-US')
|
||||||
```
|
```
|
||||||
3. Thêm hiệu ứng hover nhẹ khi hiển thị danh sách review.
|
3. Add light hover effect when displaying review list.
|
||||||
4. Dùng toast (react-hot-toast) cho thông báo gửi / duyệt / từ chối.
|
4. Use toast (react-hot-toast) for submit / approve / reject notifications.
|
||||||
5. Loading spinner khi chờ API.
|
5. Loading spinner when waiting for API.
|
||||||
|
|
||||||
### Kết quả mong đợi
|
### Expected Results
|
||||||
1. UI mượt mà, dễ đọc và thân thiện.
|
1. Smooth, readable and user-friendly UI.
|
||||||
2. Loading / toast hiển thị đúng trạng thái.
|
2. Loading / toast displays correct status.
|
||||||
3. Ngày tháng, sao và bình luận được format đẹp.
|
3. Dates, stars and comments are formatted nicely.
|
||||||
4. Giao diện quản trị và người dùng thống nhất phong cách.
|
4. Admin and user interfaces have consistent styling.
|
||||||
5. Trải nghiệm người dùng mượt, không giật lag.
|
5. Smooth user experience, no lag or stuttering.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
# 🚀 QUICK START - Server Setup
|
# 🚀 QUICK START - Server Setup
|
||||||
|
|
||||||
## Bước 1: Copy file .env
|
## Step 1: Copy .env file
|
||||||
```bash
|
```bash
|
||||||
cd d:/hotel-booking/server
|
cd d:/hotel-booking/server
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
> File .env đã được tạo sẵn với cấu hình mặc định
|
> The .env file has been pre-created with default configuration
|
||||||
|
|
||||||
## Bước 2: Tạo Database (nếu chưa có)
|
## Step 2: Create Database (if not exists)
|
||||||
```bash
|
```bash
|
||||||
# Mở MySQL command line
|
# Open MySQL command line
|
||||||
mysql -u root -p
|
mysql -u root -p
|
||||||
|
|
||||||
# Tạo database
|
# Create database
|
||||||
CREATE DATABASE hotel_db;
|
CREATE DATABASE hotel_db;
|
||||||
|
|
||||||
# Thoát
|
# Exit
|
||||||
exit;
|
exit;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bước 3: Chạy Migrations
|
## Step 3: Run Migrations
|
||||||
```bash
|
```bash
|
||||||
cd d:/hotel-booking/server
|
cd d:/hotel-booking/server
|
||||||
npm run migrate
|
npm run migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
Lệnh này sẽ tạo các bảng:
|
This command will create the following tables:
|
||||||
- roles
|
- roles
|
||||||
- users
|
- users
|
||||||
- refresh_tokens
|
- refresh_tokens
|
||||||
@@ -41,23 +41,23 @@ Lệnh này sẽ tạo các bảng:
|
|||||||
- password_reset_tokens
|
- password_reset_tokens
|
||||||
- reviews
|
- reviews
|
||||||
|
|
||||||
## Bước 4: (Optional) Seed Data
|
## Step 4: (Optional) Seed Data
|
||||||
```bash
|
```bash
|
||||||
npm run seed
|
npm run seed
|
||||||
```
|
```
|
||||||
|
|
||||||
Lệnh này sẽ tạo:
|
This command will create:
|
||||||
- 3 roles: admin, staff, customer
|
- 3 roles: admin, staff, customer
|
||||||
- Demo users
|
- Demo users
|
||||||
- Demo rooms & room types
|
- Demo rooms & room types
|
||||||
- Demo bookings
|
- Demo bookings
|
||||||
|
|
||||||
## Bước 5: Start Server
|
## Step 5: Start Server
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Bạn sẽ thấy:
|
You will see:
|
||||||
```
|
```
|
||||||
✅ Database connection established successfully
|
✅ Database connection established successfully
|
||||||
📊 Database models synced
|
📊 Database models synced
|
||||||
@@ -67,12 +67,12 @@ Bạn sẽ thấy:
|
|||||||
🏥 Health: http://localhost:3000/health
|
🏥 Health: http://localhost:3000/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bước 6: Test API
|
## Step 6: Test API
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
Mở browser: http://localhost:3000/health
|
Open browser: http://localhost:3000/health
|
||||||
|
|
||||||
### Test Login (sau khi seed data)
|
### Test Login (after seeding data)
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3000/api/auth/login \
|
curl -X POST http://localhost:3000/api/auth/login \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@@ -81,34 +81,34 @@ curl -X POST http://localhost:3000/api/auth/login \
|
|||||||
|
|
||||||
## ⚠️ Troubleshooting
|
## ⚠️ Troubleshooting
|
||||||
|
|
||||||
### Lỗi: "Access denied for user 'root'"
|
### Error: "Access denied for user 'root'"
|
||||||
**Giải pháp:** Sửa DB_PASS trong file `.env`
|
**Solution:** Update DB_PASS in `.env` file
|
||||||
```bash
|
```bash
|
||||||
DB_PASS=your_mysql_password
|
DB_PASS=your_mysql_password
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lỗi: "Unknown database 'hotel_db'"
|
### Error: "Unknown database 'hotel_db'"
|
||||||
**Giải pháp:** Tạo database thủ công (Bước 2)
|
**Solution:** Create database manually (Step 2)
|
||||||
|
|
||||||
### Lỗi: "Port 3000 already in use"
|
### Error: "Port 3000 already in use"
|
||||||
**Giải pháp:** Đổi PORT trong `.env`
|
**Solution:** Change PORT in `.env`
|
||||||
```bash
|
```bash
|
||||||
PORT=3001
|
PORT=3001
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lỗi: "Cannot find module"
|
### Error: "Cannot find module"
|
||||||
**Giải pháp:** Cài lại dependencies
|
**Solution:** Reinstall dependencies
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 Next Steps
|
## 📝 Next Steps
|
||||||
|
|
||||||
1. ✅ Server đang chạy
|
1. ✅ Server is running
|
||||||
2. ✅ Database đã setup
|
2. ✅ Database is set up
|
||||||
3. ✅ API endpoints sẵn sàng
|
3. ✅ API endpoints are ready
|
||||||
4. 🔜 Test với frontend login form
|
4. 🔜 Test with frontend login form
|
||||||
5. 🔜 Implement các API còn lại
|
5. 🔜 Implement remaining APIs
|
||||||
|
|
||||||
## 🧪 Test với Postman
|
## 🧪 Test với Postman
|
||||||
|
|
||||||
@@ -146,14 +146,14 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
|
|||||||
|
|
||||||
## ✅ Checklist
|
## ✅ Checklist
|
||||||
|
|
||||||
- [ ] MySQL đang chạy
|
- [ ] MySQL is running
|
||||||
- [ ] File .env đã tạo và cấu hình đúng
|
- [ ] .env file has been created and configured correctly
|
||||||
- [ ] Database hotel_db đã tạo
|
- [ ] Database hotel_db has been created
|
||||||
- [ ] Migrations đã chạy thành công
|
- [ ] Migrations have run successfully
|
||||||
- [ ] Server đang chạy (port 3000)
|
- [ ] Server is running (port 3000)
|
||||||
- [ ] Health check trả về 200 OK
|
- [ ] Health check returns 200 OK
|
||||||
- [ ] Frontend .env đã có VITE_API_URL=http://localhost:3000
|
- [ ] Frontend .env has VITE_API_URL=http://localhost:3000
|
||||||
- [ ] Frontend đang chạy (port 5173)
|
- [ ] Frontend is running (port 5173)
|
||||||
|
|
||||||
## 🎯 Ready to Test Login!
|
## 🎯 Ready to Test Login!
|
||||||
|
|
||||||
@@ -162,4 +162,4 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
|
|||||||
3. Login page: http://localhost:5173/login ✅
|
3. Login page: http://localhost:5173/login ✅
|
||||||
4. API endpoint: http://localhost:3000/api/auth/login ✅
|
4. API endpoint: http://localhost:3000/api/auth/login ✅
|
||||||
|
|
||||||
**Tất cả sẵn sàng!** Giờ có thể test login form từ frontend! 🚀
|
**Everything is ready!** You can now test the login form from the frontend! 🚀
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ const resetPassword = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
error.message.includes('must be different') ||
|
error.message.includes('must be different') ||
|
||||||
error.message.includes('Mật khẩu mới')
|
error.message.includes('New password')
|
||||||
) {
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const addFavorite = async (req, res, next) => {
|
|||||||
if (!room) {
|
if (!room) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Không tìm thấy phòng',
|
message: 'Room not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const addFavorite = async (req, res, next) => {
|
|||||||
if (existingFavorite) {
|
if (existingFavorite) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Phòng đã có trong danh sách yêu thích',
|
message: 'Room already in favorites list',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const addFavorite = async (req, res, next) => {
|
|||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Đã thêm vào danh sách yêu thích',
|
message: 'Added to favorites list',
|
||||||
data: {
|
data: {
|
||||||
favorite,
|
favorite,
|
||||||
},
|
},
|
||||||
@@ -75,7 +75,7 @@ const removeFavorite = async (req, res, next) => {
|
|||||||
if (!favorite) {
|
if (!favorite) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Không tìm thấy phòng trong danh sách yêu thích',
|
message: 'Room not found in favorites list',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ const removeFavorite = async (req, res, next) => {
|
|||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Đã xóa khỏi danh sách yêu thích',
|
message: 'Removed from favorites list',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module.exports = {
|
|||||||
// booking_number, service_name, rest of fields
|
// booking_number, service_name, rest of fields
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010001',
|
booking_number: 'BK2025010001',
|
||||||
service_name: 'Dịch vụ phòng - Bữa sáng',
|
service_name: 'Room Service - Breakfast',
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
unit_price: 150000,
|
unit_price: 150000,
|
||||||
total_price: 300000,
|
total_price: 300000,
|
||||||
@@ -20,7 +20,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010001',
|
booking_number: 'BK2025010001',
|
||||||
service_name: 'Dịch vụ giặt ủi - Thông thường',
|
service_name: 'Laundry Service - Regular',
|
||||||
quantity: 3,
|
quantity: 3,
|
||||||
unit_price: 60000,
|
unit_price: 60000,
|
||||||
total_price: 180000,
|
total_price: 180000,
|
||||||
@@ -31,7 +31,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010002',
|
booking_number: 'BK2025010002',
|
||||||
service_name: 'Dịch vụ phòng - Bữa sáng',
|
service_name: 'Room Service - Breakfast',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 150000,
|
unit_price: 150000,
|
||||||
total_price: 150000,
|
total_price: 150000,
|
||||||
@@ -42,7 +42,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010002',
|
booking_number: 'BK2025010002',
|
||||||
service_name: 'Spa - Massage truyền thống',
|
service_name: 'Spa - Traditional Massage',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 500000,
|
unit_price: 500000,
|
||||||
total_price: 500000,
|
total_price: 500000,
|
||||||
@@ -53,7 +53,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010002',
|
booking_number: 'BK2025010002',
|
||||||
service_name: 'Trả phòng muộn',
|
service_name: 'Late Check-out',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 500000,
|
unit_price: 500000,
|
||||||
total_price: 500000,
|
total_price: 500000,
|
||||||
@@ -64,7 +64,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010003',
|
booking_number: 'BK2025010003',
|
||||||
service_name: 'Đón sân bay',
|
service_name: 'Airport Pickup',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 400000,
|
unit_price: 400000,
|
||||||
total_price: 400000,
|
total_price: 400000,
|
||||||
@@ -75,7 +75,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010005',
|
booking_number: 'BK2025010005',
|
||||||
service_name: 'Đón sân bay',
|
service_name: 'Airport Pickup',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 400000,
|
unit_price: 400000,
|
||||||
total_price: 400000,
|
total_price: 400000,
|
||||||
@@ -86,7 +86,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
booking_number: 'BK2025010005',
|
booking_number: 'BK2025010005',
|
||||||
service_name: 'Spa - Liệu pháp hương thơm',
|
service_name: 'Spa - Aromatherapy',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit_price: 700000,
|
unit_price: 700000,
|
||||||
total_price: 700000,
|
total_price: 700000,
|
||||||
|
|||||||
Reference in New Issue
Block a user