This commit is contained in:
Iliyan Angelov
2025-11-16 15:12:43 +02:00
parent 824eec6190
commit 93d4c1df80
54 changed files with 1606 additions and 1612 deletions

View File

@@ -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`
- Bảo vệ routes yêu cầu authentication
- Redirect về `/login` nếu chưa đăng nhập
- Lưu location để quay lại sau khi login
- Protects routes requiring authentication
- Redirects to `/login` if not logged in
- Saves location to return after login
2. **AdminRoute** - `src/components/auth/AdminRoute.tsx`
- Bảo vệ routes chỉ dành cho Admin
- Redirect về `/` nếu không phải admin
- Kiểm tra `userInfo.role === 'admin'`
- Protects routes for Admin only
- Redirects to `/` if not admin
- Checks `userInfo.role === 'admin'`
3. **Page Components**:
- `RoomListPage` - Danh sách phòng (public)
- `BookingListPage` - Lịch sử đặt phòng (protected)
- `DashboardPage` - Dashboard cá nhân (protected)
- `RoomListPage` - Room list (public)
- `BookingListPage` - Booking history (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
/rooms → RoomListPage
/about → About Page
/login → Login Page (chưa có)
/register → Register Page (chưa có)
/forgot-password → Forgot Password Page (chưa có)
/reset-password/:token → Reset Password Page (chưa có)
/login → Login Page (not yet)
/register → Register Page (not yet)
/forgot-password → Forgot Password Page (not yet)
/reset-password/:token → Reset Password Page (not yet)
```
#### Protected Routes (Cần đăng nhập):
#### Protected Routes (Login required):
```
/dashboard → DashboardPage (ProtectedRoute)
/bookings → BookingListPage (ProtectedRoute)
/profile → Profile Page (ProtectedRoute)
```
#### Admin Routes (Chỉ Admin):
#### Admin Routes (Admin only):
```
/admin → AdminLayout (AdminRoute)
/admin/dashboard → Admin Dashboard
/admin/users → Quản lý người dùng
/admin/rooms → Quản lý phòng
/admin/bookings → Quản lý đặt phòng
/admin/payments → Quản lý thanh toán
/admin/services → Quản lý dịch vụ
/admin/promotions → Quản lý khuyến mãi
/admin/banners → Quản lý banner
/admin/reports → Báo cáo
/admin/settings → Cài đặt
/admin/users → User Management
/admin/rooms → Room Management
/admin/bookings → Booking Management
/admin/payments → Payment Management
/admin/services → Service Management
/admin/promotions → Promotion Management
/admin/banners → Banner Management
/admin/reports → Reports
/admin/settings → Settings
```
## 🧪 Cách Test
## 🧪 How to Test
### 1. Khởi động Dev Server:
### 1. Start Dev Server:
```bash
cd /d/hotel-booking/client
npm run dev
```
Mở `http://localhost:5173`
Open `http://localhost:5173`
### 2. Test Public Routes:
- Truy cập `/`Hiển thị HomePage ✅
- Truy cập `/rooms`Hiển thị RoomListPage ✅
- Truy cập `/about`Hiển thị About Page ✅
- Access `/`Display HomePage ✅
- Access `/rooms`Display RoomListPage ✅
- Access `/about`Display About Page ✅
### 3. Test Protected Routes (Chưa login):
- Truy cập `/dashboard` → Redirect về `/login`
- Truy cập `/bookings` → Redirect về `/login`
- Truy cập `/profile` → Redirect về `/login`
### 3. Test Protected Routes (Not logged in):
- Access `/dashboard` → Redirect to `/login`
- Access `/bookings` → Redirect to `/login`
- Access `/profile` → Redirect to `/login`
### 4. Test Protected Routes (Đã login):
- Click nút **"🔒 Demo Login"** ở góc dưới phải
- Truy cập `/dashboard`Hiển thị Dashboard ✅
- Truy cập `/bookings`Hiển thị Booking List ✅
- Truy cập `/profile`Hiển thị Profile ✅
### 4. Test Protected Routes (Logged in):
- Click **"🔒 Demo Login"** button at bottom right
- Access `/dashboard`Display Dashboard ✅
- Access `/bookings`Display Booking List ✅
- Access `/profile`Display Profile ✅
### 5. Test Admin Routes (Role = Customer):
- Đảm bảo đã login (role = customer)
- Truy cập `/admin` → Redirect về `/`
- Truy cập `/admin/dashboard` → Redirect về `/`
- Ensure logged in (role = customer)
- Access `/admin` → Redirect to `/`
- Access `/admin/dashboard` → Redirect to `/`
### 6. Test Admin Routes (Role = Admin):
- Click nút **"👑 Switch to Admin"**
- Truy cập `/admin` → Redirect về `/admin/dashboard`
- Truy cập `/admin/users`Hiển thị User Management ✅
- Truy cập `/admin/rooms`Hiển thị Room Management ✅
- Click các menu trong SidebarAdmin → Hoạt động bình thường
- Click **"👑 Switch to Admin"** button
- Access `/admin` → Redirect to `/admin/dashboard`
- Access `/admin/users`Display User Management ✅
- Access `/admin/rooms`Display Room Management ✅
- Click menu items in SidebarAdmin → Works normally
### 7. Test Logout:
- Click nút **"🔓 Demo Logout"**
- Truy cập `/dashboard` → Redirect về `/login`
- Truy cập `/admin` → Redirect về `/`
- Click **"🔓 Demo Logout"** button
- Access `/dashboard` → Redirect to `/login`
- Access `/admin` → Redirect to `/`
## 🎯 Kết quả mong đợi
## 🎯 Expected Results
### ✅ ProtectedRoute:
1. User chưa login không thể truy cập protected routes
2. Redirect về `/login` và lưu `state.from` để quay lại sau
3. User đã login có thể truy cập protected routes bình thường
1. Users not logged in cannot access protected routes
2. Redirect to `/login` and save `state.from` to return later
3. Logged in users can access protected routes normally
### ✅ AdminRoute:
1. User không phải admin không thể truy cập `/admin/*`
2. Redirect về `/` nếu không phải admin
3. Admin có thể truy cập tất cả admin routes
1. Non-admin users cannot access `/admin/*`
2. Redirect to `/` if not admin
3. Admin can access all admin routes
### ✅ Không có redirect loop:
1. Redirect chỉ xảy ra 1 lần
2. Không có vòng lặp redirect vô tận
3. Browser history hoạt động đúng (back/forward)
### ✅ No redirect loop:
1. Redirect only happens once
2. No infinite redirect loop
3. Browser history works correctly (back/forward)
## 📝 Demo Buttons (Tạm thời)
## 📝 Demo Buttons (Temporary)
### 🔒 Demo Login/Logout:
- Click để toggle authentication state
- Mô phỏng login/logout
- Sẽ được thay bằng Zustand store ở Chức năng 3
- Click to toggle authentication state
- Simulates login/logout
- Will be replaced by Zustand store in Function 3
### 👑 Switch Role:
- Chỉ hiển thị khi đã login
- Toggle giữa `customer``admin`
- Test AdminRoute hoạt động đúng
- Only displays when logged in
- Toggle between `customer``admin`
- Test AdminRoute works correctly
## 🚀 Bước tiếp theo
## 🚀 Next Steps
Chức năng 3: useAuthStore (Zustand Store)
- Tạo store quản lý auth state toàn cục
- Thay thế demo state bằng Zustand
- Tích hợp với localStorage
- Xóa demo toggle buttons
Function 3: useAuthStore (Zustand Store)
- Create store to manage global auth state
- Replace demo state with Zustand
- Integrate with localStorage
- Remove demo toggle buttons
## 🔧 File Structure

View File

@@ -1,15 +1,15 @@
# 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
2. **`src/services/api/apiClient.ts`** - Axios client với interceptors
1. **`src/store/useAuthStore.ts`** - Zustand store managing auth
2. **`src/services/api/apiClient.ts`** - Axios client with interceptors
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:
```typescript
@@ -24,19 +24,19 @@ interface AuthState {
```
#### Actions:
-`login(credentials)` - Đăng nhập
-`register(data)` - Đăng ký tài khoản mới
-`logout()` - Đăng xuất
-`setUser(user)` - Cập nhật thông tin user
-`refreshAuthToken()` - Làm mới token
-`forgotPassword(data)` - Quên mật khẩu
-`resetPassword(data)` - Đặt lại mật khẩu
-`initializeAuth()` - Khởi tạo auth từ localStorage
-`clearError()` - Xóa error message
-`login(credentials)` - Login
-`register(data)` - Register new account
-`logout()` - Logout
-`setUser(user)` - Update user information
-`refreshAuthToken()` - Refresh token
-`forgotPassword(data)` - Forgot password
-`resetPassword(data)` - Reset password
-`initializeAuth()` - Initialize auth from localStorage
-`clearError()` - Clear error message
### 📝 Cách sử dụng:
### 📝 Usage:
#### 1. Khởi tạo trong App.tsx:
#### 1. Initialize in App.tsx:
```typescript
import useAuthStore from './store/useAuthStore';
@@ -56,7 +56,7 @@ function App() {
}
```
#### 2. Sử dụng trong Login Form:
#### 2. Use in Login Form:
```typescript
import useAuthStore from '../store/useAuthStore';
@@ -67,9 +67,9 @@ const LoginPage = () => {
const handleSubmit = async (data) => {
try {
await login(data);
navigate('/dashboard'); // Redirect sau khi login
navigate('/dashboard'); // Redirect after login
} catch (error) {
// Error đã được xử lý bởi store
// Error has been handled by store
}
};
@@ -78,14 +78,14 @@ const LoginPage = () => {
{/* Form fields */}
{error && <div>{error}</div>}
<button disabled={isLoading}>
{isLoading ? 'Đang xử lý...' : 'Đăng nhập'}
{isLoading ? 'Processing...' : 'Login'}
</button>
</form>
);
};
```
#### 3. Sử dụng trong Register Form:
#### 3. Use in Register Form:
```typescript
const RegisterPage = () => {
const { register, isLoading } = useAuthStore();
@@ -94,9 +94,9 @@ const RegisterPage = () => {
const handleSubmit = async (data) => {
try {
await register(data);
navigate('/login'); // Redirect về login
navigate('/login'); // Redirect to login
} catch (error) {
// Error được hiển thị qua toast
// Error displayed via toast
}
};
@@ -111,21 +111,21 @@ const Header = () => {
const handleLogout = async () => {
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
const Profile = () => {
const { userInfo } = useAuthStore();
return (
<div>
<h1>Xin chào, {userInfo?.name}</h1>
<h1>Hello, {userInfo?.name}</h1>
<p>Email: {userInfo?.email}</p>
<p>Role: {userInfo?.role}</p>
</div>
@@ -135,68 +135,68 @@ const Profile = () => {
### 🔐 LocalStorage Persistence:
Store tự động lưu và đọc từ localStorage:
Store automatically saves and reads from localStorage:
- `token` - JWT access 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:
#### Base URL Configuration:
Tạo file `.env` trong thư mục `client/`:
Create `.env` file in `client/` directory:
```env
VITE_API_URL=http://localhost:3000
VITE_ENV=development
```
#### API Endpoints được sử dụng:
- `POST /api/auth/login` - Đăng nhập
- `POST /api/auth/register` - Đăng ký
- `POST /api/auth/logout` - Đăng xuất
- `GET /api/auth/profile` - Lấy profile
#### API Endpoints Used:
- `POST /api/auth/login` - Login
- `POST /api/auth/register` - Register
- `POST /api/auth/logout` - Logout
- `GET /api/auth/profile` - Get profile
- `POST /api/auth/refresh-token` - Refresh token
- `POST /api/auth/forgot-password` - Quên mật khẩu
- `POST /api/auth/reset-password` - Đặt lại mật khẩu
- `POST /api/auth/forgot-password` - Forgot password
- `POST /api/auth/reset-password` - Reset password
### 🛡️ Security Features:
1. **Auto Token Injection**:
- Axios interceptor tự động thêm token o headers
- Axios interceptor automatically adds token to headers
```typescript
Authorization: Bearer <token>
```
2. **Auto Logout on 401**:
- Khi token hết hạn (401), tự động logout redirect về login
- When token expires (401), automatically logout and redirect to login
3. **Token Refresh**:
- Có thể refresh token khi sắp hết hạn
- Can refresh token when about to expire
4. **Password Hashing**:
- Backend xử lý bcrypt hashing
- Backend handles bcrypt hashing
### 📱 Toast Notifications:
Store tự động hiển thị toast cho các events:
- ✅ Login thành công
- ✅ Đăng ký thành công
Store automatically displays toast for events:
- ✅ Login successful
- ✅ Registration successful
- ✅ Logout
- ❌ Login thất bại
- ❌ Đăng ký thất bại
- ❌ Login failed
- ❌ Registration failed
- ❌ API errors
### 🔄 Component Updates:
#### ProtectedRoute:
```typescript
// TRƯỚC (với props)
// BEFORE (with props)
<ProtectedRoute isAuthenticated={isAuthenticated}>
<Dashboard />
</ProtectedRoute>
// SAU (tự động lấy từ store)
// AFTER (automatically gets from store)
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
@@ -204,19 +204,19 @@ Store tự động hiển thị toast cho các events:
#### AdminRoute:
```typescript
// TRƯỚC (với props)
// BEFORE (with props)
<AdminRoute userInfo={userInfo}>
<AdminPanel />
</AdminRoute>
// SAU (tự động lấy từ store)
// AFTER (automatically gets from store)
<AdminRoute>
<AdminPanel />
</AdminRoute>
```
#### 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
<LayoutMain
isAuthenticated={isAuthenticated}
@@ -227,44 +227,44 @@ Vẫn nhận props từ App.tsx để hiển thị Header/Navbar:
### 🧪 Testing:
Để test authentication flow:
To test authentication flow:
1. **Tạo file `.env`**:
1. **Create `.env` file**:
```bash
cp .env.example .env
```
2. **Ensure backend đang chạy**:
2. **Ensure backend is running**:
```bash
cd server
npm run dev
```
3. **Chạy frontend**:
3. **Run frontend**:
```bash
cd client
npm run dev
```
4. **Test flow**:
- Truy cập `/register` → Đăng ký tài khoản
- Truy cập `/login` → Đăng nhập
- Truy cập `/dashboard` → Xem dashboard (protected)
- Click logout → Xóa session
- Reload page → Auth state được khôi phục
- Access `/register` → Register account
- Access `/login` → Login
- Access `/dashboard` → View dashboard (protected)
- Click logout → Clear session
- Reload page → Auth state restored
### 🚀 Next Steps:
**Chức năng 4: Form Login**
- Tạo LoginPage với React Hook Form + Yup
- Tích hợp với useAuthStore
**Function 4: Login Form**
- Create LoginPage with React Hook Form + Yup
- Integrate with useAuthStore
- UX enhancements (loading, show/hide password, remember me)
**Chức năng 5: Form Register**
- Tạo RegisterPage với validation
- Tích hợp với useAuthStore
**Function 5: Register Form**
- Create RegisterPage with validation
- Integrate with useAuthStore
**Chức năng 6-7: Password Reset Flow**
**Function 6-7: Password Reset Flow**
- ForgotPasswordPage
- 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
2.Duy trì đăng nhập sau khi reload trang
3.Dễ dàng truy cập userInfo trong mọi component
1.All user information managed centrally
2.Maintain login after page reload
3.Easy access to userInfo in any component
4. ✅ Auto token management
5. ✅ Type-safe với TypeScript
6. ✅ Clean code, dễ maintain
5. ✅ Type-safe with TypeScript
6. ✅ Clean code, easy to maintain

View File

@@ -69,20 +69,20 @@ import {
CheckOutPage,
} 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 }) => (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-800">
{title}
</h1>
<p className="text-gray-600 mt-4">
Page này đang đưc phát triển...
This page is under development...
</p>
</div>
);
function App() {
// Sử dụng Zustand store
// Use Zustand store
const {
isAuthenticated,
userInfo,
@@ -96,7 +96,7 @@ function App() {
loadGuestFavorites,
} = useFavoritesStore();
// Khởi tạo auth state khi app load
// Initialize auth state when app loads
useEffect(() => {
initializeAuth();
}, [initializeAuth]);
@@ -161,10 +161,10 @@ function App() {
/>
<Route
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
path="dashboard"
element={
@@ -225,7 +225,7 @@ function App() {
path="profile"
element={
<ProtectedRoute>
<DemoPage title="Hồ sơ" />
<DemoPage title="Profile" />
</ProtectedRoute>
}
/>
@@ -249,7 +249,7 @@ function App() {
element={<ResetPasswordPage />}
/>
{/* Admin Routes - Chỉ admin mới truy cập được */}
{/* Admin Routes - Only admin can access */}
<Route
path="/admin"
element={
@@ -301,22 +301,22 @@ function App() {
/>
<Route
path="banners"
element={<DemoPage title="Quản lý banner" />}
element={<DemoPage title="Banner Management" />}
/>
<Route
path="reports"
element={<DemoPage title="Báo cáo" />}
element={<DemoPage title="Reports" />}
/>
<Route
path="settings"
element={<DemoPage title="Cài đặt" />}
element={<DemoPage title="Settings" />}
/>
</Route>
{/* 404 Route */}
<Route
path="*"
element={<DemoPage title="404 - Không tìm thấy trang" />}
element={<DemoPage title="404 - Page not found" />}
/>
</Routes>

View File

@@ -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:
* 1. User đã đăng nhập chưa → nếu chưa, redirect /login
* 2. User có role admin không → nếu không, redirect /
* Checks:
* 1. Is user logged in → if not, redirect to /login
* 2. Does user have admin role → if not, redirect to /
*/
const AdminRoute: React.FC<AdminRouteProps> = ({
children
@@ -19,7 +19,7 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
const location = useLocation();
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
// Đang loading auth state → hiển thị loading
// Loading auth state → show loading
if (isLoading) {
return (
<div
@@ -32,14 +32,14 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
border-b-2 border-indigo-600 mx-auto"
/>
<p className="mt-4 text-gray-600">
Đang xác thực...
Authenticating...
</p>
</div>
</div>
);
}
// Chưa đăng nhập → redirect về /login
// Not logged in → redirect to /login
if (!isAuthenticated) {
return (
<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';
if (!isAdmin) {
return <Navigate to="/" replace />;

View File

@@ -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
* và lưu location hiện tại để redirect về sau khi login
* If user is not logged in, redirect to /login
* and save current location to redirect back after login
*/
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children
@@ -18,7 +18,7 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
const location = useLocation();
const { isAuthenticated, isLoading } = useAuthStore();
// Đang loading auth state → hiển thị loading
// Loading auth state → show loading
if (isLoading) {
return (
<div
@@ -31,14 +31,14 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
border-b-2 border-indigo-600 mx-auto"
/>
<p className="mt-4 text-gray-600">
Đang tải...
Loading...
</p>
</div>
</div>
);
}
// Chưa đăng nhập → redirect về /login
// Not logged in → redirect to /login
if (!isAuthenticated) {
return (
<Navigate

View File

@@ -72,12 +72,12 @@ class ErrorBoundary extends Component<Props, State> {
<h1 className="text-2xl font-bold
text-gray-900 text-center mb-2"
>
Đã xảy ra lỗi
An Error Occurred
</h1>
<p className="text-gray-600 text-center mb-6">
Xin lỗi, đã lỗi xảy ra. Vui lòng thử lại
hoặc liên hệ hỗ trợ nếu vấn đ vẫn tiếp diễn.
Sorry, an error has occurred. Please try again
or contact support if the problem persists.
</p>
{process.env.NODE_ENV === 'development' &&
@@ -96,7 +96,7 @@ class ErrorBoundary extends Component<Props, State> {
text-red-700 cursor-pointer
hover:text-red-800"
>
Chi tiết lỗi
Error Details
</summary>
<pre className="mt-2 text-xs
text-red-600 overflow-auto
@@ -119,7 +119,7 @@ class ErrorBoundary extends Component<Props, State> {
font-semibold"
>
<RefreshCw className="w-5 h-5" />
Tải lại trang
Reload Page
</button>
<button
onClick={() => window.location.href = '/'}
@@ -128,7 +128,7 @@ class ErrorBoundary extends Component<Props, State> {
hover:bg-gray-300 transition-colors
font-semibold"
>
Về trang chủ
Go to Home
</button>
</div>
</div>

View File

@@ -69,7 +69,7 @@ const Pagination: React.FC<PaginationProps> = ({
: 'text-gray-700 hover:bg-gray-50'
}`}
>
Trước
Previous
</button>
<button
onClick={() => onPageChange(currentPage + 1)}
@@ -80,7 +80,7 @@ const Pagination: React.FC<PaginationProps> = ({
: 'text-gray-700 hover:bg-gray-50'
}`}
>
Sau
Next
</button>
</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>
<p className="text-sm text-gray-700">
Hiển thị{' '}
<span className="font-medium">{startItem}</span> đến{' '}
<span className="font-medium">{endItem}</span> trong tổng số{' '}
<span className="font-medium">{totalItems || 0}</span> kết quả
Showing{' '}
<span className="font-medium">{startItem}</span> to{' '}
<span className="font-medium">{endItem}</span> of{' '}
<span className="font-medium">{totalItems || 0}</span> results
</p>
</div>
<div>

View File

@@ -53,32 +53,32 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
{
path: '/admin/users',
icon: Users,
label: 'Người dùng'
label: 'Users'
},
{
path: '/admin/rooms',
icon: Hotel,
label: 'Phòng'
label: 'Rooms'
},
{
path: '/admin/bookings',
icon: Calendar,
label: 'Đặt phòng'
label: 'Bookings'
},
{
path: '/admin/payments',
icon: CreditCard,
label: 'Thanh toán'
label: 'Payments'
},
{
path: '/admin/services',
icon: Settings,
label: 'Dịch vụ'
label: 'Services'
},
{
path: '/admin/promotions',
icon: Tag,
label: 'Khuyến mãi'
label: 'Promotions'
},
{
path: '/admin/check-in',
@@ -93,22 +93,22 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
{
path: '/admin/reviews',
icon: Star,
label: 'Đánh giá'
label: 'Reviews'
},
{
path: '/admin/banners',
icon: Image,
label: 'Banner'
label: 'Banners'
},
{
path: '/admin/reports',
icon: BarChart3,
label: 'Báo cáo'
label: 'Reports'
},
{
path: '/admin/settings',
icon: FileText,
label: 'Cài đặt'
label: 'Settings'
},
];

View File

@@ -43,7 +43,7 @@ const BannerCarousel: React.FC<BannerCarouselProps> = ({
// Default fallback banner if no banners provided
const defaultBanner = {
id: 0,
title: 'Chào mừng đến với Hotel Booking',
title: 'Welcome to Hotel Booking',
image_url: '/images/default-banner.jpg',
position: 'home',
display_order: 0,

View File

@@ -61,8 +61,8 @@ const FavoriteButton: React.FC<FavoriteButtonProps> = ({
};
const tooltipText = favorited
? 'Bỏ yêu thích'
: 'Thêm vào yêu thích';
? 'Remove from favorites'
: 'Add to favorites';
return (
<div className="relative inline-block">

View File

@@ -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
* trong production
* This file is for reference only, should not be used
* in production
*/
import { useNavigate } from 'react-router-dom';
@@ -23,7 +23,7 @@ export const LoginExample = () => {
await login({ email, password });
navigate('/dashboard');
} catch (error) {
// Error đã được xử lý trong store
// Error has been handled in store
console.error('Login failed:', error);
}
};
@@ -38,7 +38,7 @@ export const LoginExample = () => {
)}
disabled={isLoading}
>
{isLoading ? 'Đang xử lý...' : 'Đăng nhập'}
{isLoading ? 'Processing...' : 'Login'}
</button>
</div>
);
@@ -70,7 +70,7 @@ export const RegisterExample = () => {
onClick={handleRegister}
disabled={isLoading}
>
{isLoading ? 'Đang xử lý...' : 'Đăng ký'}
{isLoading ? 'Processing...' : 'Register'}
</button>
);
};
@@ -82,13 +82,13 @@ export const UserProfileExample = () => {
const { userInfo, isAuthenticated } = useAuthStore();
if (!isAuthenticated) {
return <p>Vui lòng đăng nhập</p>;
return <p>Please login</p>;
}
return (
<div>
<h2>Thông tin người dùng</h2>
<p>Tên: {userInfo?.name}</p>
<h2>User Information</h2>
<p>Name: {userInfo?.name}</p>
<p>Email: {userInfo?.email}</p>
<p>Role: {userInfo?.role}</p>
{userInfo?.avatar && (
@@ -118,7 +118,7 @@ export const LogoutButtonExample = () => {
onClick={handleLogout}
disabled={isLoading}
>
{isLoading ? 'Đang xử lý...' : 'Đăng xuất'}
{isLoading ? 'Processing...' : 'Logout'}
</button>
);
};
@@ -134,7 +134,7 @@ export const ForgotPasswordExample = () => {
) => {
try {
await forgotPassword({ email });
// Toast sẽ hiển thị thông báo thành công
// Toast will display success message
} catch (error) {
console.error('Forgot password failed:', error);
}
@@ -147,7 +147,7 @@ export const ForgotPasswordExample = () => {
}
disabled={isLoading}
>
Gửi email đt lại mật khẩu
Send password reset email
</button>
);
};
@@ -185,7 +185,7 @@ export const ResetPasswordExample = () => {
}
disabled={isLoading}
>
Đt lại mật khẩu
Reset Password
</button>
);
};
@@ -222,14 +222,14 @@ export const AuthStateCheckExample = () => {
} = useAuthStore();
if (isLoading) {
return <p>Đang tải...</p>;
return <p>Loading...</p>;
}
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 (
<button onClick={handleUpdateProfile}>
Cập nhật thông tin
Update Information
</button>
);
};
@@ -270,7 +270,7 @@ export const ErrorHandlingExample = () => {
onClick={clearError}
className="mt-2 text-sm text-red-600"
>
Đóng
Close
</button>
</div>
);

View File

@@ -301,7 +301,7 @@ const BookingManagementPage: React.FC = () => {
onClick={() => setShowDetailModal(false)}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
>
Đóng
Close
</button>
</div>
</div>

View File

@@ -229,7 +229,7 @@ const CheckInPage: React.FC = () => {
type="text"
value={actualRoomNumber}
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"
/>
<p className="text-xs text-gray-500 mt-1">
@@ -271,7 +271,7 @@ const CheckInPage: React.FC = () => {
value={guest.name}
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"
placeholder="Nguyễn Văn A"
placeholder="John Doe"
/>
</div>
<div>

View File

@@ -191,7 +191,7 @@ const DashboardPage: React.FC = () => {
))}
</div>
) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p>
<p className="text-gray-500 text-center py-8">No data available</p>
)}
</div>
@@ -227,7 +227,7 @@ const DashboardPage: React.FC = () => {
})}
</div>
) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p>
<p className="text-gray-500 text-center py-8">No data available</p>
)}
</div>
</div>
@@ -236,7 +236,7 @@ const DashboardPage: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Rooms */}
<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 ? (
<div className="space-y-3">
{stats.top_rooms.map((room, index) => (
@@ -246,8 +246,8 @@ const DashboardPage: React.FC = () => {
{index + 1}
</span>
<div>
<p className="font-medium text-gray-900">Phòng {room.room_number}</p>
<p className="text-sm text-gray-500">{room.bookings} lượt đt</p>
<p className="font-medium text-gray-900">Room {room.room_number}</p>
<p className="text-sm text-gray-500">{room.bookings} bookings</p>
</div>
</div>
<span className="font-semibold text-green-600">
@@ -257,20 +257,20 @@ const DashboardPage: React.FC = () => {
))}
</div>
) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p>
<p className="text-gray-500 text-center py-8">No data available</p>
)}
</div>
{/* Service Usage */}
<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 ? (
<div className="space-y-3">
{stats.service_usage.map((service) => (
<div key={service.service_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<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>
<span className="font-semibold text-purple-600">
{formatCurrency(service.total_revenue)}
@@ -279,7 +279,7 @@ const DashboardPage: React.FC = () => {
))}
</div>
) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p>
<p className="text-gray-500 text-center py-8">No data available</p>
)}
</div>
</div>

View File

@@ -152,8 +152,8 @@ const PromotionManagementPage: React.FC = () => {
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Quản khuyến mãi</h1>
<p className="text-gray-500 mt-1">Quản giảm giá chương trình khuyến mãi</p>
<h1 className="text-3xl font-bold text-gray-900">Promotion Management</h1>
<p className="text-gray-500 mt-1">Manage discount codes and promotion programs</p>
</div>
<button
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"
>
<Plus className="w-5 h-5" />
Thêm khuyến mãi
Add Promotion
</button>
</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" />
<input
type="text"
placeholder="Tìm theo code hoặc tên..."
placeholder="Search by code or name..."
value={filters.search}
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"
@@ -187,18 +187,18 @@ const PromotionManagementPage: React.FC = () => {
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"
>
<option value="">Tất cả loại</option>
<option value="percentage">Phần trăm</option>
<option value="fixed">Số tiền cố đnh</option>
<option value="">All Types</option>
<option value="percentage">Percentage</option>
<option value="fixed">Fixed Amount</option>
</select>
<select
value={filters.status}
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"
>
<option value="">Tất cả trạng thái</option>
<option value="active">Hoạt đng</option>
<option value="inactive">Ngừng</option>
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
@@ -209,25 +209,25 @@ const PromotionManagementPage: React.FC = () => {
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
code
Code
</th>
<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 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Giá trị
Value
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Thời gian
Period
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Đã dùng
Used
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Trạng thái
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Thao tác
Actions
</th>
</tr>
</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="flex justify-between items-center mb-4">
<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>
<button onClick={() => setShowModal(false)}>
<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>
<label className="block text-sm font-medium text-gray-700 mb-1">
code <span className="text-red-500">*</span>
Code <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.code}
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"
placeholder="VD: SUMMER2024"
placeholder="e.g: SUMMER2024"
required
/>
</div>
<div>
<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>
<input
type="text"
value={formData.name}
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"
placeholder="VD: Giảm giá mùa hè"
placeholder="e.g: Summer Sale"
required
/>
</div>
@@ -339,34 +339,34 @@ const PromotionManagementPage: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
tả
Description
</label>
<textarea
value={formData.description}
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"
rows={3}
placeholder="Mô tả chi tiết về chương trình..."
placeholder="Detailed description of the program..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<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>
<select
value={formData.discount_type}
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"
>
<option value="percentage">Phần trăm (%)</option>
<option value="fixed">Số tiền cố đnh (VND)</option>
<option value="percentage">Percentage (%)</option>
<option value="fixed">Fixed Amount (VND)</option>
</select>
</div>
<div>
<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>
<input
type="number"
@@ -383,7 +383,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4">
<div>
<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>
<input
type="number"
@@ -395,7 +395,7 @@ const PromotionManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Giảm tối đa (VND)
Maximum Discount (VND)
</label>
<input
type="number"
@@ -410,7 +410,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4">
<div>
<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>
<input
type="date"
@@ -422,7 +422,7 @@ const PromotionManagementPage: React.FC = () => {
</div>
<div>
<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>
<input
type="date"
@@ -437,7 +437,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4">
<div>
<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>
<input
type="number"
@@ -449,15 +449,15 @@ const PromotionManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái
Status
</label>
<select
value={formData.status}
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"
>
<option value="active">Hoạt đng</option>
<option value="inactive">Ngừng</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
@@ -468,13 +468,13 @@ const PromotionManagementPage: React.FC = () => {
onClick={() => setShowModal(false)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Hủy
Cancel
</button>
<button
type="submit"
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>
</div>
</form>

View File

@@ -150,7 +150,7 @@ const ReviewManagementPage: React.FC = () => {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<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>
</td>
<td className="px-6 py-4 whitespace-nowrap">
@@ -173,14 +173,14 @@ const ReviewManagementPage: React.FC = () => {
<button
onClick={() => handleApprove(review.id)}
className="text-green-600 hover:text-green-900 mr-3"
title="Phê duyệt"
title="Approve"
>
</button>
<button
onClick={() => handleReject(review.id)}
className="text-red-600 hover:text-red-900"
title="Từ chối"
title="Reject"
>
</button>

View File

@@ -155,29 +155,29 @@ const RoomManagementPage: React.FC = () => {
const handleDeleteImage = async (imageUrl: string) => {
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 {
await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
data: { imageUrl },
});
toast.success('Xóa ảnh thành công');
toast.success('Image deleted successfully');
fetchRooms();
// Refresh editing room data
const response = await roomService.getRoomById(editingRoom.id);
setEditingRoom(response.data.room);
} 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 badges: Record<string, { bg: string; text: string; label: string }> = {
available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Trống' },
occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Đang sử dụng' },
maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Bảo trì' },
available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Available' },
occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Occupied' },
maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Maintenance' },
};
const badge = badges[status] || badges.available;
return (
@@ -196,8 +196,8 @@ const RoomManagementPage: React.FC = () => {
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Quản phòng</h1>
<p className="text-gray-500 mt-1">Quản thông tin phòng khách sạn</p>
<h1 className="text-3xl font-bold text-gray-900">Room Management</h1>
<p className="text-gray-500 mt-1">Manage hotel room information</p>
</div>
<button
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"
>
<Plus className="w-5 h-5" />
Thêm phòng
Add Room
</button>
</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" />
<input
type="text"
placeholder="Tìm kiếm phòng..."
placeholder="Search rooms..."
value={filters.search}
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"
@@ -229,17 +229,17 @@ const RoomManagementPage: React.FC = () => {
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"
>
<option value="">Tất cả trạng thái</option>
<option value="available">Trống</option>
<option value="occupied">Đang sử dụng</option>
<option value="maintenance">Bảo trì</option>
<option value="">All Statuses</option>
<option value="available">Available</option>
<option value="occupied">Occupied</option>
<option value="maintenance">Maintenance</option>
</select>
<select
value={filters.type}
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"
>
<option value="">Tất cả loại phòng</option>
<option value="">All Room Types</option>
<option value="1">Standard</option>
<option value="2">Deluxe</option>
<option value="3">Suite</option>
@@ -253,25 +253,25 @@ const RoomManagementPage: React.FC = () => {
<thead className="bg-gray-50">
<tr>
<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 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 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tầng
Floor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Giá
Price
</th>
<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 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nổi bật
Featured
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Thao tác
Actions
</th>
</tr>
</thead>
@@ -285,7 +285,7 @@ const RoomManagementPage: React.FC = () => {
<div className="text-sm text-gray-900">{room.room_type?.name || 'N/A'}</div>
</td>
<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 className="px-6 py-4 whitespace-nowrap">
<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="flex justify-between items-center mb-4">
<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>
<button onClick={() => setShowModal(false)}>
<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>
<label className="block text-sm font-medium text-gray-700 mb-1">
Số phòng
Room Number
</label>
<input
type="text"
@@ -363,7 +363,7 @@ const RoomManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tầng
Floor
</label>
<input
type="number"
@@ -378,7 +378,7 @@ const RoomManagementPage: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Loại phòng
Room Type
</label>
<select
value={formData.room_type_id}
@@ -394,7 +394,7 @@ const RoomManagementPage: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái
Status
</label>
<select
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"
required
>
<option value="available">Trống</option>
<option value="occupied">Đang sử dụng</option>
<option value="maintenance">Bảo trì</option>
<option value="available">Available</option>
<option value="occupied">Occupied</option>
<option value="maintenance">Maintenance</option>
</select>
</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"
/>
<label htmlFor="featured" className="ml-2 text-sm text-gray-700">
Phòng nổi bật
Featured Room
</label>
</div>
@@ -427,13 +427,13 @@ const RoomManagementPage: React.FC = () => {
onClick={() => setShowModal(false)}
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
type="submit"
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>
</div>
</form>
@@ -443,13 +443,13 @@ const RoomManagementPage: React.FC = () => {
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<ImageIcon className="w-5 h-5" />
Hình nh phòng
Room Images
</h3>
{/* Current Images */}
{editingRoom.room_type?.images && editingRoom.room_type.images.length > 0 && (
<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">
{editingRoom.room_type.images.map((img, index) => (
<div key={index} className="relative group">
@@ -474,7 +474,7 @@ const RoomManagementPage: React.FC = () => {
{/* Upload New Images */}
<div>
<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>
<div className="flex gap-3">
<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"
>
<Upload className="w-4 h-4" />
{uploadingImages ? 'Đang tải...' : 'Upload'}
{uploadingImages ? 'Uploading...' : 'Upload'}
</button>
</div>
{selectedFiles.length > 0 && (
<p className="text-sm text-gray-600 mt-2">
{selectedFiles.length} file đã chọn
{selectedFiles.length} file(s) selected
</p>
)}
</div>

View File

@@ -23,7 +23,7 @@ const ServiceManagementPage: React.FC = () => {
name: '',
description: '',
price: 0,
unit: 'lần',
unit: 'time',
status: 'active' as 'active' | 'inactive',
});
@@ -155,9 +155,9 @@ const ServiceManagementPage: React.FC = () => {
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"
>
<option value="">Tất cả trạng thái</option>
<option value="active">Hoạt đng</option>
<option value="inactive">Tạm dừng</option>
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
@@ -167,22 +167,22 @@ const ServiceManagementPage: React.FC = () => {
<thead className="bg-gray-50">
<tr>
<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 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
tả
Description
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Giá
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Đơn vị
Unit
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Trạng thái
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Thao tác
Actions
</th>
</tr>
</thead>
@@ -207,7 +207,7 @@ const ServiceManagementPage: React.FC = () => {
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{service.status === 'active' ? 'Hoạt động' : 'Tạm dừng'}
{service.status === 'active' ? 'Active' : 'Inactive'}
</span>
</td>
<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="flex justify-between items-center mb-4">
<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>
<button onClick={() => setShowModal(false)}>
<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">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tên dịch vụ
Service Name
</label>
<input
type="text"
@@ -263,7 +263,7 @@ const ServiceManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
tả
Description
</label>
<textarea
value={formData.description}
@@ -274,7 +274,7 @@ const ServiceManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Giá
Price
</label>
<input
type="number"
@@ -287,27 +287,27 @@ const ServiceManagementPage: React.FC = () => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Đơn vị
Unit
</label>
<input
type="text"
value={formData.unit}
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"
placeholder="VD: lần, giờ, ngày..."
placeholder="e.g: time, hour, day..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái
Status
</label>
<select
value={formData.status}
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"
>
<option value="active">Hoạt đng</option>
<option value="inactive">Tạm dừng</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div className="flex gap-3 mt-6">
@@ -316,13 +316,13 @@ const ServiceManagementPage: React.FC = () => {
onClick={() => setShowModal(false)}
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
type="submit"
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>
</div>
</form>

View File

@@ -45,7 +45,7 @@ const ForgotPasswordPage: React.FC = () => {
// Show success state
setIsSuccess(true);
} catch (error) {
// Error đã được xử lý trong store
// Error has been handled in store
console.error('Forgot password error:', error);
}
};

View File

@@ -49,12 +49,12 @@ const LoginPage: React.FC = () => {
rememberMe: data.rememberMe,
});
// Redirect về trang trước đó hoặc dashboard
// Redirect to previous page or dashboard
const from = location.state?.from?.pathname ||
'/dashboard';
navigate(from, { replace: true });
} catch (error) {
// Error đã được xử lý trong store
// Error has been handled in store
console.error('Login error:', error);
}
};

View File

@@ -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');
// Password strength checker
@@ -88,7 +88,7 @@ const RegisterPage: React.FC = () => {
// Redirect to login page
navigate('/login', { replace: true });
} catch (error) {
// Error đã được xử lý trong store
// Error has been handled in store
console.error('Register error:', error);
}
};

View File

@@ -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');
// Check if token exists
@@ -66,11 +66,11 @@ const ResetPasswordPage: React.FC = () => {
if (/[@$!%*?&]/.test(pwd)) strength++;
const labels = [
{ label: 'Rất yếu', color: 'bg-red-500' },
{ label: 'Yếu', color: 'bg-orange-500' },
{ label: 'Trung bình', color: 'bg-yellow-500' },
{ label: 'Mạnh', color: 'bg-blue-500' },
{ label: 'Rất mạnh', color: 'bg-green-500' },
{ label: 'Very Weak', color: 'bg-red-500' },
{ label: 'Weak', color: 'bg-orange-500' },
{ label: 'Medium', color: 'bg-yellow-500' },
{ label: 'Strong', color: 'bg-blue-500' },
{ label: 'Very Strong', color: 'bg-green-500' },
];
return { strength, ...labels[strength] };
@@ -100,7 +100,7 @@ const ResetPasswordPage: React.FC = () => {
navigate('/login', { replace: true });
}, 3000);
} catch (error) {
// Error đã được xử lý trong store
// Error has been handled in store
console.error('Reset password error:', error);
}
};
@@ -129,12 +129,12 @@ const ResetPasswordPage: React.FC = () => {
</div>
</div>
<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>
<p className="mt-2 text-sm text-gray-600">
{isSuccess
? 'Mật khẩu đã được đặt lại thành công'
: 'Nhập mật khẩu mới cho tài khoản của bạn'}
? 'Password has been reset successfully'
: 'Enter a new password for your account'}
</p>
</div>
@@ -160,13 +160,13 @@ const ResetPasswordPage: React.FC = () => {
className="text-xl font-semibold
text-gray-900"
>
Đt lại mật khẩu thành công!
Password reset successful!
</h3>
<p className="text-sm text-gray-600">
Mật khẩu của bn đã đưc cập nhật.
Your password has been updated.
</p>
<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>
</div>
@@ -175,7 +175,7 @@ const ResetPasswordPage: React.FC = () => {
rounded-lg p-4"
>
<p className="text-sm text-gray-700">
Đang chuyển hướng đến trang đăng nhập...
Redirecting to login page...
</p>
<div className="mt-2 flex justify-center">
<Loader2
@@ -198,7 +198,7 @@ const ResetPasswordPage: React.FC = () => {
transition-colors"
>
<KeyRound className="-ml-1 mr-2 h-5 w-5" />
Đăng nhập ngay
Login Now
</Link>
</div>
) : (
@@ -224,7 +224,7 @@ const ResetPasswordPage: React.FC = () => {
<div className="flex-1">
<p className="font-medium">
{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}
</p>
{isTokenError && (
@@ -234,7 +234,7 @@ const ResetPasswordPage: React.FC = () => {
font-medium underline
hover:text-yellow-900"
>
Yêu cầu link mới
Request new link
</Link>
)}
</div>
@@ -248,7 +248,7 @@ const ResetPasswordPage: React.FC = () => {
className="block text-sm font-medium
text-gray-700 mb-2"
>
Mật khẩu mới
New Password
</label>
<div className="relative">
<div
@@ -338,23 +338,23 @@ const ResetPasswordPage: React.FC = () => {
<div className="mt-2 space-y-1">
<PasswordRequirement
met={password.length >= 8}
text="Ít nhất 8 ký tự"
text="At least 8 characters"
/>
<PasswordRequirement
met={/[a-z]/.test(password)}
text="Chữ thường (a-z)"
text="Lowercase letter (a-z)"
/>
<PasswordRequirement
met={/[A-Z]/.test(password)}
text="Chữ hoa (A-Z)"
text="Uppercase letter (A-Z)"
/>
<PasswordRequirement
met={/\d/.test(password)}
text="Số (0-9)"
text="Number (0-9)"
/>
<PasswordRequirement
met={/[@$!%*?&]/.test(password)}
text="Ký tự đặc biệt (@$!%*?&)"
text="Special character (@$!%*?&)"
/>
</div>
</div>
@@ -368,7 +368,7 @@ const ResetPasswordPage: React.FC = () => {
className="block text-sm font-medium
text-gray-700 mb-2"
>
Xác nhận mật khẩu
Confirm Password
</label>
<div className="relative">
<div
@@ -453,14 +453,14 @@ const ResetPasswordPage: React.FC = () => {
className="animate-spin -ml-1 mr-2
h-5 w-5"
/>
Đang xử ...
Processing...
</>
) : (
<>
<KeyRound
className="-ml-1 mr-2 h-5 w-5"
/>
Đt lại mật khẩu
Reset Password
</>
)}
</button>
@@ -473,7 +473,7 @@ const ResetPasswordPage: React.FC = () => {
text-indigo-600 hover:text-indigo-500
transition-colors"
>
Quay lại đăng nhập
Back to Login
</Link>
</div>
</form>
@@ -492,16 +492,16 @@ const ResetPasswordPage: React.FC = () => {
gap-2"
>
<Lock className="h-4 w-4" />
Bảo mật
Security
</h3>
<ul
className="text-xs text-gray-600 space-y-1
list-disc list-inside"
>
<li>Link đt lại chỉ hiệu lực trong 1 giờ</li>
<li>Mật khẩu đưc hóa an toàn</li>
<li>Reset link is valid for 1 hour only</li>
<li>Password is securely encrypted</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>
</ul>
</div>

View File

@@ -50,7 +50,7 @@ const BookingDetailPage: React.FC = () => {
useEffect(() => {
if (!isAuthenticated) {
toast.error(
'Vui lòng đăng nhập để xem chi tiết đặt phòng'
'Please login to view booking details'
);
navigate('/login', {
state: { from: `/bookings/${id}` }
@@ -79,14 +79,14 @@ const BookingDetailPage: React.FC = () => {
setBooking(response.data.booking);
} else {
throw new Error(
'Không thể tải thông tin đặt phòng'
'Unable to load booking information'
);
}
} catch (err: any) {
console.error('Error fetching booking:', err);
const message =
err.response?.data?.message ||
'Không thể tải thông tin đặt phòng';
'Unable to load booking information';
setError(message);
toast.error(message);
} finally {
@@ -98,12 +98,12 @@ const BookingDetailPage: React.FC = () => {
if (!booking) return;
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` +
`⚠️ Lưu ý:\n` +
`- Bạn sẽ bị giữ 20% giá trị đơn\n` +
`- 80% còn lại sẽ được hoàn trả\n` +
`- Trạng thái phòng sẽ được cập nhật về "available"`
`⚠️ Note:\n` +
`- You will be charged 20% of the order value\n` +
`- The remaining 80% will be refunded\n` +
`- Room status will be updated to "available"`
);
if (!confirmed) return;
@@ -115,8 +115,7 @@ const BookingDetailPage: React.FC = () => {
if (response.success) {
toast.success(
`Đã hủy đặt phòng ${booking.booking_number} ` +
`thành công!`
`Booking ${booking.booking_number} cancelled successfully!`
);
// Update local state
@@ -128,14 +127,14 @@ const BookingDetailPage: React.FC = () => {
} else {
throw new Error(
response.message ||
'Không thể hủy đặt phòng'
'Unable to cancel booking'
);
}
} catch (err: any) {
console.error('Error cancelling booking:', err);
const 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);
} finally {
setCancelling(false);
@@ -164,31 +163,31 @@ const BookingDetailPage: React.FC = () => {
return {
icon: Clock,
color: 'bg-yellow-100 text-yellow-800',
text: 'Chờ xác nhận',
text: 'Pending confirmation',
};
case 'confirmed':
return {
icon: CheckCircle,
color: 'bg-green-100 text-green-800',
text: 'Đã xác nhận',
text: 'Confirmed',
};
case 'cancelled':
return {
icon: XCircle,
color: 'bg-red-100 text-red-800',
text: 'Đã hủy',
text: 'Cancelled',
};
case 'checked_in':
return {
icon: DoorOpen,
color: 'bg-blue-100 text-blue-800',
text: 'Đã nhận phòng',
text: 'Checked in',
};
case 'checked_out':
return {
icon: DoorClosed,
color: 'bg-gray-100 text-gray-800',
text: 'Đã trả phòng',
text: 'Checked out',
};
default:
return {
@@ -207,7 +206,7 @@ const BookingDetailPage: React.FC = () => {
};
if (loading) {
return <Loading fullScreen text="Đang tải..." />;
return <Loading fullScreen text="Loading..." />;
}
if (error || !booking) {
@@ -223,7 +222,7 @@ const BookingDetailPage: React.FC = () => {
mx-auto mb-3"
/>
<p className="text-red-700 font-medium mb-4">
{error || 'Không tìm thấy đặt phòng'}
{error || 'Booking not found'}
</p>
<button
onClick={() => navigate('/bookings')}
@@ -231,7 +230,7 @@ const BookingDetailPage: React.FC = () => {
text-white rounded-lg
hover:bg-red-700 transition-colors"
>
Quay lại danh sách
Back to list
</button>
</div>
</div>
@@ -255,7 +254,7 @@ const BookingDetailPage: React.FC = () => {
mb-6 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Quay lại danh sách</span>
<span>Back to list</span>
</Link>
{/* Page Title */}
@@ -263,7 +262,7 @@ const BookingDetailPage: React.FC = () => {
mb-6"
>
<h1 className="text-3xl font-bold text-gray-900">
Chi tiết đt phòng
Booking Details
</h1>
{/* Status Badge */}
@@ -284,7 +283,7 @@ const BookingDetailPage: React.FC = () => {
<p className="text-sm text-indigo-600
font-medium mb-1"
>
đt phòng
Booking Number
</p>
<p className="text-2xl font-bold text-indigo-900
font-mono"
@@ -300,7 +299,7 @@ const BookingDetailPage: React.FC = () => {
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Thông tin phòng
Room Information
</h2>
{roomType && (
@@ -328,25 +327,25 @@ const BookingDetailPage: React.FC = () => {
</h3>
<p className="text-gray-600 mb-4">
<MapPin className="w-4 h-4 inline mr-1" />
Phòng {room?.room_number} -
Tầng {room?.floor}
Room {room?.room_number} -
Floor {room?.floor}
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">
Sức chứa
Capacity
</p>
<p className="font-medium text-gray-900">
Tối đa {roomType.capacity} người
Max {roomType.capacity} guests
</p>
</div>
<div>
<p className="text-sm text-gray-500">
Giá phòng
Room Price
</p>
<p className="font-medium text-indigo-600">
{formatPrice(roomType.base_price)}/đêm
{formatPrice(roomType.base_price)}/night
</p>
</div>
</div>
@@ -362,7 +361,7 @@ const BookingDetailPage: React.FC = () => {
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Chi tiết đt phòng
Booking Details
</h2>
<div className="space-y-4">
@@ -373,7 +372,7 @@ const BookingDetailPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Ngày nhận phòng
Check-in Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_in_date)}
@@ -382,7 +381,7 @@ const BookingDetailPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Ngày trả phòng
Check-out Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_out_date)}
@@ -394,10 +393,10 @@ const BookingDetailPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Users className="w-4 h-4 inline mr-1" />
Số người
Number of Guests
</p>
<p className="font-medium text-gray-900">
{booking.guest_count} người
{booking.guest_count} guest(s)
</p>
</div>
@@ -406,7 +405,7 @@ const BookingDetailPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<FileText className="w-4 h-4 inline mr-1" />
Ghi chú
Notes
</p>
<p className="font-medium text-gray-900">
{booking.notes}
@@ -418,16 +417,16 @@ const BookingDetailPage: React.FC = () => {
<div className="border-t pt-4">
<p className="text-sm text-gray-600 mb-1">
<CreditCard className="w-4 h-4 inline mr-1" />
Phương thức thanh toán
Payment Method
</p>
<p className="font-medium text-gray-900 mb-2">
{booking.payment_method === 'cash'
? '💵 Thanh toán tại chỗ'
: '🏦 Chuyển khoản ngân hàng'}
? '💵 Pay at hotel'
: '🏦 Bank transfer'}
</p>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">
Trạng thái:
Status:
</span>
<PaymentStatusBadge
status={booking.payment_status}
@@ -444,7 +443,7 @@ const BookingDetailPage: React.FC = () => {
<span className="text-lg font-semibold
text-gray-900"
>
Tổng thanh toán
Total Payment
</span>
<span className="text-2xl font-bold
text-indigo-600"
@@ -464,13 +463,13 @@ const BookingDetailPage: React.FC = () => {
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Thông tin khách hàng
Customer Information
</h2>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">
<User className="w-4 h-4 inline mr-1" />
Họ tên
Full Name
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.full_name}
@@ -488,7 +487,7 @@ const BookingDetailPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600">
<Phone className="w-4 h-4 inline mr-1" />
Số điện thoại
Phone Number
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.phone}
@@ -512,25 +511,25 @@ const BookingDetailPage: React.FC = () => {
/>
<div className="flex-1">
<h3 className="font-bold text-blue-900 mb-2">
Thông tin chuyển khoản
Bank Transfer Information
</h3>
<div className="bg-white rounded p-4
space-y-2 text-sm"
>
<p>
<strong>Ngân hàng:</strong>
<strong>Bank:</strong>
Vietcombank (VCB)
</p>
<p>
<strong>Số tài khoản:</strong>
<strong>Account Number:</strong>
0123456789
</p>
<p>
<strong>Chủ tài khoản:</strong>
<strong>Account Holder:</strong>
KHACH SAN ABC
</p>
<p>
<strong>Số tiền:</strong>{' '}
<strong>Amount:</strong>{' '}
<span className="text-indigo-600
font-bold"
>
@@ -538,7 +537,7 @@ const BookingDetailPage: React.FC = () => {
</span>
</p>
<p>
<strong>Nội dung:</strong>{' '}
<strong>Content:</strong>{' '}
<span className="font-mono
text-indigo-600"
>
@@ -594,7 +593,7 @@ const BookingDetailPage: React.FC = () => {
font-semibold"
>
<CreditCard className="w-5 h-5" />
Xác nhận thanh toán
Confirm Payment
</Link>
)}
@@ -614,12 +613,12 @@ const BookingDetailPage: React.FC = () => {
<Loader2
className="w-5 h-5 animate-spin"
/>
Đang hủy...
Cancelling...
</>
) : (
<>
<XCircle className="w-5 h-5" />
Hủy đt phòng
Cancel Booking
</>
)}
</button>
@@ -633,7 +632,7 @@ const BookingDetailPage: React.FC = () => {
hover:bg-gray-700 transition-colors
font-semibold"
>
Quay lại danh sách
Back to list
</Link>
</div>
</div>

View File

@@ -6,10 +6,10 @@ const BookingListPage: React.FC = () => {
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-800 mb-2">
Lịch sử đt phòng
Booking History
</h1>
<p className="text-gray-600">
Quản theo dõi các đt phòng của bạn
Manage and track your bookings
</p>
</div>
@@ -30,13 +30,13 @@ const BookingListPage: React.FC = () => {
<h3 className="text-xl font-semibold
text-gray-800"
>
Phòng {booking}01 - Deluxe
Room {booking}01 - Deluxe
</h3>
<span className="px-3 py-1
bg-green-100 text-green-800
rounded-full text-sm font-medium"
>
Đã xác nhận
Confirmed
</span>
</div>
@@ -51,7 +51,7 @@ const BookingListPage: React.FC = () => {
text-blue-500"
/>
<span>
Nhận phòng: 15/11/2025
Check-in: 15/11/2025
</span>
</div>
<div className="flex items-center
@@ -61,7 +61,7 @@ const BookingListPage: React.FC = () => {
text-blue-500"
/>
<span>
Trả phòng: 18/11/2025
Check-out: 18/11/2025
</span>
</div>
<div className="flex items-center
@@ -70,7 +70,7 @@ const BookingListPage: React.FC = () => {
<Clock className="w-4 h-4
text-blue-500"
/>
<span>3 đêm</span>
<span>3 nights</span>
</div>
</div>
</div>
@@ -96,7 +96,7 @@ const BookingListPage: React.FC = () => {
hover:bg-blue-700 transition-colors
text-sm"
>
Xem chi tiết
View Details
</button>
</div>
</div>
@@ -105,16 +105,16 @@ const BookingListPage: React.FC = () => {
</div>
{/* Empty State */}
{/* Uncomment khi không có booking
{/* Uncomment when there are no bookings
<div className="text-center py-12">
<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>
<button className="mt-4 px-6 py-3
bg-blue-600 text-white rounded-lg
hover:bg-blue-700 transition-colors"
>
Đặt phòng ngay
Book Now
</button>
</div>
*/}

View File

@@ -320,7 +320,7 @@ const BookingPage: React.FC = () => {
border-gray-300 rounded-lg
focus:ring-2 focus:ring-indigo-500
focus:border-indigo-500"
placeholder="Nguyễn Văn A"
placeholder="John Doe"
/>
{errors.fullName && (
<p className="text-sm text-red-600 mt-1">

View File

@@ -82,14 +82,14 @@ const BookingSuccessPage: React.FC = () => {
}
} else {
throw new Error(
'Không thể tải thông tin đặt phòng'
'Unable to load booking information'
);
}
} catch (err: any) {
console.error('Error fetching booking:', err);
const message =
err.response?.data?.message ||
'Không thể tải thông tin đặt phòng';
'Unable to load booking information';
setError(message);
toast.error(message);
} finally {
@@ -133,15 +133,15 @@ const BookingSuccessPage: React.FC = () => {
const getStatusText = (status: string) => {
switch (status) {
case 'confirmed':
return 'Đã xác nhận';
return 'Confirmed';
case 'pending':
return 'Chờ xác nhận';
return 'Pending confirmation';
case 'cancelled':
return 'Đã hủy';
return 'Cancelled';
case 'checked_in':
return 'Đã nhận phòng';
return 'Checked in';
case 'checked_out':
return 'Đã trả phòng';
return 'Checked out';
default:
return status;
}
@@ -155,10 +155,10 @@ const BookingSuccessPage: React.FC = () => {
booking.booking_number
);
setCopiedBookingNumber(true);
toast.success('Đã sao chép mã đặt phòng');
toast.success('Booking number copied');
setTimeout(() => setCopiedBookingNumber(false), 2000);
} 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
if (!file.type.startsWith('image/')) {
toast.error('Vui lòng chọn file ảnh');
toast.error('Please select an image file');
return;
}
// Validate file size (max 5MB)
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;
}
@@ -208,8 +208,8 @@ const BookingSuccessPage: React.FC = () => {
if (response.success) {
toast.success(
'✅ Đã gửi xác nhận thanh toán thành công! ' +
'Chúng tôi sẽ xác nhận trong thời gian sớm nhất.'
'✅ Payment confirmation sent successfully! ' +
'We will confirm as soon as possible.'
);
setReceiptUploaded(true);
@@ -228,15 +228,15 @@ const BookingSuccessPage: React.FC = () => {
} else {
throw new Error(
response.message ||
'Không thể xác nhận thanh toán'
'Unable to confirm payment'
);
}
} catch (err: any) {
console.error('Error uploading receipt:', err);
const message =
err.response?.data?.message ||
'Không thể gửi xác nhận thanh toán. ' +
'Vui lòng thử lại.';
'Unable to send payment confirmation. ' +
'Please try again.';
toast.error(message);
} finally {
setUploadingReceipt(false);
@@ -251,7 +251,7 @@ const BookingSuccessPage: React.FC = () => {
: null;
if (loading) {
return <Loading fullScreen text="Đang tải..." />;
return <Loading fullScreen text="Loading..." />;
}
if (error || !booking) {
@@ -267,7 +267,7 @@ const BookingSuccessPage: React.FC = () => {
mx-auto mb-3"
/>
<p className="text-red-700 font-medium mb-4">
{error || 'Không tìm thấy đặt phòng'}
{error || 'Booking not found'}
</p>
<button
onClick={() => navigate('/rooms')}
@@ -275,7 +275,7 @@ const BookingSuccessPage: React.FC = () => {
text-white px-3 py-2 rounded-md hover:bg-indigo-700
disabled:bg-gray-400 mb-6 transition-colors"
>
Quay lại danh sách phòng
Back to room list
</button>
</div>
</div>
@@ -307,11 +307,10 @@ const BookingSuccessPage: React.FC = () => {
className="text-3xl font-bold text-gray-900
mb-2"
>
Đt phòng thành công!
Booking Successful!
</h1>
<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
tôi
Thank you for booking with our hotel
</p>
{/* Booking Number */}
@@ -322,7 +321,7 @@ const BookingSuccessPage: React.FC = () => {
<span className="text-sm text-indigo-600
font-medium"
>
đt phòng:
Booking Number:
</span>
<span className="text-lg font-bold
text-indigo-900"
@@ -333,7 +332,7 @@ const BookingSuccessPage: React.FC = () => {
onClick={copyBookingNumber}
className="ml-2 p-1 hover:bg-indigo-100
rounded transition-colors"
title="Sao chép mã"
title="Copy booking number"
>
{copiedBookingNumber ? (
<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
mb-4"
>
Chi tiết đt phòng
Booking Details
</h2>
<div className="space-y-4">
@@ -389,14 +388,14 @@ const BookingSuccessPage: React.FC = () => {
<MapPin className="w-4 h-4
inline mr-1"
/>
Phòng {room.room_number} -
Tầng {room.floor}
Room {room.room_number} -
Floor {room.floor}
</p>
)}
<p className="text-indigo-600
font-semibold mt-1"
>
{formatPrice(roomType.base_price)}/đêm
{formatPrice(roomType.base_price)}/night
</p>
</div>
</div>
@@ -410,7 +409,7 @@ const BookingSuccessPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Ngày nhận phòng
Check-in Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_in_date)}
@@ -419,7 +418,7 @@ const BookingSuccessPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Ngày trả phòng
Check-out Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_out_date)}
@@ -431,10 +430,10 @@ const BookingSuccessPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<Users className="w-4 h-4 inline mr-1" />
Số người
Number of Guests
</p>
<p className="font-medium text-gray-900">
{booking.guest_count} người
{booking.guest_count} guest(s)
</p>
</div>
@@ -443,7 +442,7 @@ const BookingSuccessPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600 mb-1">
<FileText className="w-4 h-4 inline mr-1" />
Ghi chú
Notes
</p>
<p className="font-medium text-gray-900">
{booking.notes}
@@ -455,12 +454,12 @@ const BookingSuccessPage: React.FC = () => {
<div className="border-t pt-4">
<p className="text-sm text-gray-600 mb-1">
<CreditCard className="w-4 h-4 inline mr-1" />
Phương thức thanh toán
Payment Method
</p>
<p className="font-medium text-gray-900">
{booking.payment_method === 'cash'
? '💵 Thanh toán tại chỗ'
: '🏦 Chuyển khoản ngân hàng'}
? '💵 Pay at hotel'
: '🏦 Bank transfer'}
</p>
</div>
@@ -472,7 +471,7 @@ const BookingSuccessPage: React.FC = () => {
<span className="text-lg font-semibold
text-gray-900"
>
Tổng thanh toán
Total Payment
</span>
<span className="text-2xl font-bold
text-indigo-600"
@@ -492,13 +491,13 @@ const BookingSuccessPage: React.FC = () => {
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Thông tin khách hàng
Customer Information
</h2>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">
<User className="w-4 h-4 inline mr-1" />
Họ tên
Full Name
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.full_name}
@@ -516,7 +515,7 @@ const BookingSuccessPage: React.FC = () => {
<div>
<p className="text-sm text-gray-600">
<Phone className="w-4 h-4 inline mr-1" />
Số điện thoại
Phone Number
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.phone}
@@ -539,13 +538,13 @@ const BookingSuccessPage: React.FC = () => {
/>
<div className="flex-1">
<h3 className="font-bold text-blue-900 mb-2">
Hướng dẫn chuyển khoản
Bank Transfer Instructions
</h3>
<div className="space-y-2 text-sm
text-blue-800"
>
<p>
Vui lòng chuyển khoản theo thông tin sau:
Please transfer according to the following information:
</p>
<div className="grid grid-cols-1
@@ -556,19 +555,19 @@ const BookingSuccessPage: React.FC = () => {
p-4 space-y-2"
>
<p>
<strong>Ngân hàng:</strong>
<strong>Bank:</strong>
Vietcombank (VCB)
</p>
<p>
<strong>Số tài khoản:</strong>
<strong>Account Number:</strong>
0123456789
</p>
<p>
<strong>Chủ tài khoản:</strong>
<strong>Account Holder:</strong>
KHACH SAN ABC
</p>
<p>
<strong>Số tiền:</strong>{' '}
<strong>Amount:</strong>{' '}
<span className="text-indigo-600
font-bold"
>
@@ -576,7 +575,7 @@ const BookingSuccessPage: React.FC = () => {
</span>
</p>
<p>
<strong>Nội dung:</strong>{' '}
<strong>Content:</strong>{' '}
<span className="font-mono
text-indigo-600"
>
@@ -594,7 +593,7 @@ const BookingSuccessPage: React.FC = () => {
<p className="text-sm font-medium
text-gray-700 mb-2"
>
Quét QR đ chuyển khoản
Scan QR code to transfer
</p>
<img
src={qrCodeUrl}
@@ -605,16 +604,15 @@ const BookingSuccessPage: React.FC = () => {
<p className="text-xs text-gray-500
mt-2 text-center"
>
QR đã bao gồm đy đ thông tin
QR code includes all information
</p>
</div>
)}
</div>
<p className="text-xs italic mt-2">
💡 Lưu ý: Vui lòng ghi đúng đt phòng
vào nội dung chuyển khoản đ chúng tôi
thể xác nhận thanh toán của bạn.
💡 Note: Please enter the correct booking number
in the transfer content so we can confirm your payment.
</p>
</div>
</div>
@@ -628,12 +626,11 @@ const BookingSuccessPage: React.FC = () => {
<h4 className="font-semibold text-blue-900
mb-3"
>
📎 Xác nhận thanh toán
📎 Payment Confirmation
</h4>
<p className="text-sm text-blue-700 mb-3">
Sau khi chuyển khoản, vui lòng tải lên
nh biên lai đ chúng tôi xác nhận nhanh
hơn.
After transferring, please upload
the receipt image so we can confirm faster.
</p>
<div className="space-y-3">
@@ -675,7 +672,7 @@ const BookingSuccessPage: React.FC = () => {
<p className="text-xs
text-gray-500"
>
Click đ chọn nh khác
Click to select another image
</p>
</>
) : (
@@ -687,12 +684,12 @@ const BookingSuccessPage: React.FC = () => {
<p className="text-sm
text-blue-600 font-medium"
>
Chọn nh biên lai
Select receipt image
</p>
<p className="text-xs
text-gray-500"
>
PNG, JPG, JPEG (Tối đa 5MB)
PNG, JPG, JPEG (Max 5MB)
</p>
</>
)}
@@ -720,14 +717,14 @@ const BookingSuccessPage: React.FC = () => {
className="w-5 h-5
animate-spin"
/>
Đang gửi...
Sending...
</>
) : (
<>
<CheckCircle
className="w-5 h-5"
/>
Xác nhận đã thanh toán
Confirm payment completed
</>
)}
</button>
@@ -804,7 +801,7 @@ const BookingSuccessPage: React.FC = () => {
font-semibold"
>
<ListOrdered className="w-5 h-5" />
Xem đơn của tôi
View My Bookings
</Link>
<Link
to="/"
@@ -815,7 +812,7 @@ const BookingSuccessPage: React.FC = () => {
font-semibold"
>
<Home className="w-5 h-5" />
Về trang chủ
Go to Home
</Link>
</div>
</div>

View File

@@ -15,7 +15,7 @@ const DashboardPage: React.FC = () => {
Dashboard
</h1>
<p className="text-gray-600">
Tổng quan hoạt đng của bạn
Overview of your activity
</p>
</div>
@@ -43,7 +43,7 @@ const DashboardPage: React.FC = () => {
<h3 className="text-gray-500 text-sm
font-medium mb-1"
>
Tổng đt phòng
Total Bookings
</h3>
<p className="text-3xl font-bold text-gray-800">
45
@@ -70,7 +70,7 @@ const DashboardPage: React.FC = () => {
<h3 className="text-gray-500 text-sm
font-medium mb-1"
>
Tổng chi tiêu
Total Spending
</h3>
<p className="text-3xl font-bold text-gray-800">
$12,450
@@ -95,7 +95,7 @@ const DashboardPage: React.FC = () => {
<h3 className="text-gray-500 text-sm
font-medium mb-1"
>
Đang
Currently Staying
</h3>
<p className="text-3xl font-bold text-gray-800">
2
@@ -122,7 +122,7 @@ const DashboardPage: React.FC = () => {
<h3 className="text-gray-500 text-sm
font-medium mb-1"
>
Điểm thưởng
Reward Points
</h3>
<p className="text-3xl font-bold text-gray-800">
1,250
@@ -140,24 +140,24 @@ const DashboardPage: React.FC = () => {
<h2 className="text-xl font-semibold
text-gray-800 mb-4"
>
Hoạt đng gần đây
Recent Activity
</h2>
<div className="space-y-4">
{[
{
action: 'Đặt phòng',
room: 'Phòng 201',
time: '2 giờ trước'
action: 'Booking',
room: 'Room 201',
time: '2 hours ago'
},
{
action: 'Check-in',
room: 'Phòng 105',
time: '1 ngày trước'
room: 'Room 105',
time: '1 day ago'
},
{
action: 'Check-out',
room: 'Phòng 302',
time: '3 ngày trước'
room: 'Room 302',
time: '3 days ago'
},
].map((activity, index) => (
<div key={index}
@@ -194,19 +194,19 @@ const DashboardPage: React.FC = () => {
<h2 className="text-xl font-semibold
text-gray-800 mb-4"
>
Đt phòng sắp tới
Upcoming Bookings
</h2>
<div className="space-y-4">
{[
{
room: 'Phòng 401',
room: 'Room 401',
date: '20/11/2025',
status: 'Đã xác nhận'
status: 'Confirmed'
},
{
room: 'Phòng 203',
room: 'Room 203',
date: '25/11/2025',
status: 'Chờ xác nhận'
status: 'Pending confirmation'
},
].map((booking, index) => (
<div key={index}
@@ -224,7 +224,7 @@ const DashboardPage: React.FC = () => {
</div>
<span className={`px-3 py-1 rounded-full
text-xs font-medium
${booking.status === 'Đã xác nhận'
${booking.status === 'Confirmed'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}

View File

@@ -52,7 +52,7 @@ const DepositPaymentPage: React.FC = () => {
// Fetch booking details
const bookingResponse = await getBookingById(id);
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;
@@ -60,7 +60,7 @@ const DepositPaymentPage: React.FC = () => {
// Check if booking 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}`);
return;
}
@@ -86,7 +86,7 @@ const DepositPaymentPage: React.FC = () => {
} catch (err: any) {
console.error('Error fetching data:', err);
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);
toast.error(message);
} finally {
@@ -160,7 +160,7 @@ const DepositPaymentPage: React.FC = () => {
>
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-3" />
<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>
<Link
to="/bookings"
@@ -169,7 +169,7 @@ const DepositPaymentPage: React.FC = () => {
transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Quay lại danh sách booking
Back to booking list
</Link>
</div>
</div>
@@ -191,7 +191,7 @@ const DepositPaymentPage: React.FC = () => {
hover:text-gray-900 mb-6 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Quay lại chi tiết booking</span>
<span>Back to booking details</span>
</Link>
{/* Success Header (if paid) */}
@@ -209,11 +209,11 @@ const DepositPaymentPage: React.FC = () => {
</div>
<div className="flex-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>
<p className="text-green-700">
Booking của bn đã đưc xác nhận.
Phần còn lại thanh toán khi nhận phòng.
Your booking has been confirmed.
Remaining amount to be paid at check-in.
</p>
</div>
</div>
@@ -235,11 +235,11 @@ const DepositPaymentPage: React.FC = () => {
</div>
<div className="flex-1">
<h1 className="text-2xl font-bold text-orange-900 mb-1">
Thanh toán tiền đt cọc
Deposit Payment
</h1>
<p className="text-orange-700">
Vui lòng thanh toán <strong>20% tiền cọc</strong> đ
xác nhận đt phòng
Please pay <strong>20% deposit</strong> to
confirm your booking
</p>
</div>
</div>
@@ -252,12 +252,12 @@ const DepositPaymentPage: React.FC = () => {
{/* Payment Summary */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">
Thông tin thanh toán
Payment Information
</h2>
<div className="space-y-3">
<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">
{formatPrice(booking.total_price)}
</span>
@@ -268,7 +268,7 @@ const DepositPaymentPage: React.FC = () => {
text-orange-600"
>
<span className="font-medium">
Tiền cọc cần thanh toán (20%)
Deposit Amount to Pay (20%)
</span>
<span className="text-xl font-bold">
{formatPrice(depositAmount)}
@@ -276,7 +276,7 @@ const DepositPaymentPage: React.FC = () => {
</div>
<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>
</div>
</div>
@@ -284,14 +284,14 @@ const DepositPaymentPage: React.FC = () => {
{isDepositPaid && (
<div className="mt-4 bg-green-50 border border-green-200 rounded p-3">
<p className="text-sm text-green-800">
Đã thanh toán tiền cọc vào:{' '}
Deposit paid on:{' '}
{depositPayment.payment_date
? new Date(depositPayment.payment_date).toLocaleString('en-US')
: 'N/A'}
</p>
{depositPayment.transaction_id && (
<p className="text-xs text-green-700 mt-1">
giao dịch: {depositPayment.transaction_id}
Transaction ID: {depositPayment.transaction_id}
</p>
)}
</div>
@@ -302,7 +302,7 @@ const DepositPaymentPage: React.FC = () => {
{!isDepositPaid && (
<div className="bg-white rounded-lg shadow-md p-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>
{/* Payment Method Buttons */}
@@ -331,10 +331,10 @@ const DepositPaymentPage: React.FC = () => {
: 'text-gray-700'
}`}
>
Chuyển khoản
Bank Transfer
</div>
<div className="text-xs text-gray-500 mt-1">
Chuyển khoản ngân hàng
Bank transfer
</div>
</button>
@@ -351,7 +351,7 @@ const DepositPaymentPage: React.FC = () => {
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">
<Building2 className="w-5 h-5 inline mr-2" />
Thông tin chuyển khoản
Bank Transfer Information
</h2>
<div className="space-y-4">
@@ -359,16 +359,16 @@ const DepositPaymentPage: React.FC = () => {
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<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>
<button
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"
>
{copiedText === 'tên ngân hàng' ? (
{copiedText === 'bank name' ? (
<Check className="w-4 h-4 text-green-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>
<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">
{bankInfo.account_number}
</div>
</div>
<button
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"
>
{copiedText === 'số tài khoản' ? (
{copiedText === 'account number' ? (
<Check className="w-4 h-4 text-green-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>
<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>
<button
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"
>
{copiedText === 'chủ tài khoản' ? (
{copiedText === 'account holder' ? (
<Check className="w-4 h-4 text-green-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>
<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">
{formatPrice(bankInfo.amount)}
</div>
</div>
<button
onClick={() =>
copyToClipboard(bankInfo.amount.toString(), 'số tiền')
copyToClipboard(bankInfo.amount.toString(), 'amount')
}
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" />
) : (
<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>
<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">
{bankInfo.content}
</div>
</div>
<button
onClick={() =>
copyToClipboard(bankInfo.content, 'nội dung')
copyToClipboard(bankInfo.content, 'content')
}
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" />
) : (
<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">
<p className="text-sm text-yellow-800">
<strong> Lưu ý:</strong> Vui lòng nhập đúng nội dung chuyển khoản đ
hệ thống tự đng xác nhận thanh toán.
<strong> Note:</strong> Please enter the correct transfer content so
the system can automatically confirm the payment.
</p>
</div>
@@ -478,17 +478,17 @@ const DepositPaymentPage: React.FC = () => {
{notifying ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Đang gửi...
Sending...
</>
) : (
<>
<CheckCircle className="w-5 h-5" />
Tôi đã chuyển khoản
I have transferred
</>
)}
</button>
<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>
</div>
</div>
@@ -504,7 +504,7 @@ const DepositPaymentPage: React.FC = () => {
<div className="lg:col-span-1">
<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">
Quét QR đ thanh toán
Scan QR Code to Pay
</h3>
<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">
<p className="text-sm text-gray-600">
Quét QR bằng app ngân hàng
Scan QR code with your bank app
</p>
<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>
</div>
@@ -532,7 +532,7 @@ const DepositPaymentPage: React.FC = () => {
text-gray-700 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
Tải QR
Download QR Code
</a>
</div>
</div>

View File

@@ -39,10 +39,10 @@ const FavoritesPage: React.FC = () => {
className="text-xl font-bold
text-gray-900 mb-2"
>
Vui lòng đăng nhập
Please Login
</h3>
<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>
<Link
to="/login"
@@ -51,7 +51,7 @@ const FavoritesPage: React.FC = () => {
hover:bg-indigo-700 transition-colors
font-semibold"
>
Đăng nhập
Login
</Link>
</div>
</div>
@@ -71,7 +71,7 @@ const FavoritesPage: React.FC = () => {
mb-4 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Quay lại trang chủ</span>
<span>Back to home</span>
</Link>
<div className="flex items-center gap-3">
@@ -84,12 +84,12 @@ const FavoritesPage: React.FC = () => {
className="text-3xl font-bold
text-gray-900"
>
Danh sách yêu thích
Favorites List
</h1>
<p className="text-gray-600 mt-1">
{favorites.length > 0
? `${favorites.length} phòng`
: 'Chưa có phòng yêu thích'}
? `${favorites.length} room${favorites.length !== 1 ? 's' : ''}`
: 'No favorite rooms yet'}
</p>
</div>
</div>
@@ -126,7 +126,7 @@ const FavoritesPage: React.FC = () => {
text-white rounded-lg
hover:bg-red-700 transition-colors"
>
Thử lại
Try again
</button>
</div>
)}
@@ -153,16 +153,14 @@ const FavoritesPage: React.FC = () => {
className="text-2xl font-bold
text-gray-900 mb-3"
>
Chưa phòng yêu thích
No favorite rooms yet
</h3>
<p
className="text-gray-600 mb-6
max-w-md mx-auto"
>
Bạn chưa thêm phòng o vào danh sách
yêu thích. Hãy khám phá lưu những
phòng bạn thích!
You haven't added any rooms to your favorites list yet. Explore and save the rooms you like!
</p>
<Link
@@ -172,7 +170,7 @@ const FavoritesPage: React.FC = () => {
hover:bg-indigo-700 transition-colors
font-semibold"
>
Khám phá phòng
Explore rooms
</Link>
</div>
)}

View File

@@ -108,7 +108,7 @@ const MyBookingsPage: React.FC = () => {
console.error('Error fetching bookings:', err);
const message =
err.response?.data?.message ||
'Không thể tải danh sách đặt phòng';
'Unable to load bookings list';
setError(message);
toast.error(message);
} finally {

View File

@@ -101,7 +101,7 @@ const PaymentConfirmationPage: React.FC = () => {
console.error('Error fetching booking:', err);
const message =
err.response?.data?.message ||
'Không thể tải thông tin đặt phòng';
'Unable to load booking information';
setError(message);
toast.error(message);
} finally {

View File

@@ -218,7 +218,7 @@ const PaymentResultPage: React.FC = () => {
font-medium"
>
<Home className="w-5 h-5" />
Về trang chủ
Go to home
</Link>
)}
</div>

View File

@@ -46,7 +46,7 @@ const RoomDetailPage: React.FC = () => {
console.error('Error fetching room:', err);
const message =
err.response?.data?.message ||
'Không thể tải thông tin phòng';
'Unable to load room information';
setError(message);
} finally {
setLoading(false);
@@ -109,7 +109,7 @@ const RoomDetailPage: React.FC = () => {
disabled:bg-gray-400 mb-6 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Quay lại danh sách phòng</span>
<span>Back to room list</span>
</Link>
{/* Image Gallery */}
@@ -138,14 +138,14 @@ const RoomDetailPage: React.FC = () => {
<div className="flex items-center gap-2">
<MapPin className="w-5 h-5" />
<span>
Phòng {room.room_number} - Tầng {room.floor}
Room {room.room_number} - Floor {room.floor}
</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-5 h-5" />
<span>
{roomType?.capacity || 0} người
{roomType?.capacity || 0} guests
</span>
</div>
@@ -176,10 +176,10 @@ const RoomDetailPage: React.FC = () => {
}`}
>
{room.status === 'available'
? 'Còn phòng'
? 'Available'
: room.status === 'occupied'
? 'Đã đặt'
: 'Bảo trì'}
? 'Booked'
: 'Maintenance'}
</div>
</div>
@@ -189,7 +189,7 @@ const RoomDetailPage: React.FC = () => {
<h2 className="text-2xl font-bold
text-gray-900 mb-4"
>
tả phòng
Room Description
</h2>
<p className="text-gray-700 leading-relaxed">
{roomType.description}
@@ -202,7 +202,7 @@ const RoomDetailPage: React.FC = () => {
<h2 className="text-2xl font-bold
text-gray-900 mb-4"
>
Tiện ích
Amenities
</h2>
<RoomAmenities
amenities={
@@ -223,7 +223,7 @@ const RoomDetailPage: React.FC = () => {
<div className="text-3xl font-extrabold text-indigo-600">
{formattedPrice}
</div>
<div className="text-sm text-gray-500">/ đêm</div>
<div className="text-sm text-gray-500">/ night</div>
</div>
</div>
@@ -239,13 +239,13 @@ const RoomDetailPage: React.FC = () => {
if (room.status !== 'available') e.preventDefault();
}}
>
{room.status === 'available' ? 'Đặt ngay' : 'Không khả dụng'}
{room.status === 'available' ? 'Book Now' : 'Not Available'}
</Link>
</div>
{room.status === 'available' && (
<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>
)}
@@ -253,15 +253,15 @@ const RoomDetailPage: React.FC = () => {
<div className="text-sm text-gray-700 space-y-2">
<div className="flex items-center justify-between">
<span>Loại phòng</span>
<span>Room Type</span>
<strong>{roomType?.name}</strong>
</div>
<div className="flex items-center justify-between">
<span>Số khách</span>
<span>{roomType?.capacity} người</span>
<span>Guests</span>
<span>{roomType?.capacity} guests</span>
</div>
<div className="flex items-center justify-between">
<span>Số phòng</span>
<span>Rooms</span>
<span>1</span>
</div>
</div>

View File

@@ -56,7 +56,7 @@ const RoomListPage: React.FC = () => {
}
} catch (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 {
setLoading(false);
}
@@ -76,12 +76,12 @@ const RoomListPage: React.FC = () => {
disabled:bg-gray-400 mb-6 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Quay lại trang chủ</span>
<span>Back to home</span>
</Link>
<div className="mb-10">
<h1 className="text-3xl text-center font-bold text-gray-900">
Danh sách phòng
Room List
</h1>
</div>
@@ -126,7 +126,7 @@ const RoomListPage: React.FC = () => {
text-white rounded-lg hover:bg-red-700
transition-colors"
>
Thử lại
Try Again
</button>
</div>
)}
@@ -153,17 +153,17 @@ const RoomListPage: React.FC = () => {
<h3 className="text-xl font-semibold
text-gray-800 mb-2"
>
Không tìm thấy phòng phù hợp
No matching rooms found
</h3>
<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>
<button
onClick={() => window.location.href = '/rooms'}
className="px-6 py-2 bg-blue-600 text-white
rounded-lg hover:bg-blue-700 transition-colors"
>
Xóa bộ lọc
Clear Filters
</button>
</div>
)}

View File

@@ -1,6 +1,6 @@
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
// like '/bookings/me' resolve to e.g. 'http://localhost:3000/api/bookings/me'.
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 + '/api';
// Tạo axios instance
// Create axios instance
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
@@ -22,7 +22,7 @@ const apiClient = axios.create({
withCredentials: true, // Enable sending cookies
});
// Request interceptor - Thêm token o header
// Request interceptor - Add token to header
apiClient.interceptors.request.use(
(config) => {
// Normalize request URL: if a request path accidentally begins

View File

@@ -45,12 +45,12 @@ export interface ResetPasswordData {
}
/**
* Auth Service - Xử lý các API calls liên quan
* đến authentication
* Auth Service - Handles API calls related
* to authentication
*/
const authService = {
/**
* Đăng nhập
* Login
*/
login: async (
credentials: LoginCredentials
@@ -63,7 +63,7 @@ const authService = {
},
/**
* Đăng ký tài khoản mới
* Register new account
*/
register: async (
data: RegisterData
@@ -76,7 +76,7 @@ const authService = {
},
/**
* Đăng xuất
* Logout
*/
logout: async (): Promise<void> => {
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> => {
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 (
data: ForgotPasswordData
@@ -121,7 +121,7 @@ const authService = {
},
/**
* Đặt lại mật khẩu
* Reset password
*/
resetPassword: async (
data: ResetPasswordData

View File

@@ -239,7 +239,7 @@ export const checkRoomAvailability = async (
available: false,
message:
error.response.data.message ||
'Phòng đã được đặt trong thời gian này',
'Room already booked during this time',
};
}
throw error;

View File

@@ -3,60 +3,60 @@ import * as yup from 'yup';
export const bookingValidationSchema = yup.object().shape({
checkInDate: yup
.date()
.required('Vui lòng chọn ngày nhận phòng')
.required('Please select check-in date')
.min(
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
.date()
.required('Vui lòng chọn ngày trả phòng')
.required('Please select check-out date')
.min(
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
.number()
.required('Vui lòng nhập số người')
.min(1, 'Số người tối thiểu là 1')
.max(10, 'Số người tối đa là 10')
.integer('Số người phải là số nguyên')
.typeError('Số người phải là số'),
.required('Please enter number of guests')
.min(1, 'Minimum number of guests is 1')
.max(10, 'Maximum number of guests is 10')
.integer('Number of guests must be an integer')
.typeError('Number of guests must be a number'),
notes: yup
.string()
.max(500, 'Ghi chú không được quá 500 ký tự')
.max(500, 'Notes cannot exceed 500 characters')
.optional(),
paymentMethod: yup
.mixed<'cash' | 'bank_transfer'>()
.required('Vui lòng chọn phương thức thanh toán')
.required('Please select payment method')
.oneOf(
['cash', 'bank_transfer'],
'Phương thức thanh toán không hợp lệ'
'Invalid payment method'
),
fullName: yup
.string()
.required('Vui lòng nhập họ tên')
.min(2, 'Họ tên phải có ít nhất 2 ký tự')
.max(100, 'Họ tên không được quá 100 ký tự'),
.required('Please enter full name')
.min(2, 'Full name must be at least 2 characters')
.max(100, 'Full name cannot exceed 100 characters'),
email: yup
.string()
.required('Vui lòng nhập email')
.email('Email không hợp lệ'),
.required('Please enter email')
.email('Invalid email'),
phone: yup
.string()
.required('Vui lòng nhập số điện thoại')
.required('Please enter phone number')
.matches(
/^[0-9]{10,11}$/,
'Số điện thoại phải có 10-11 chữ số'
'Phone number must have 10-11 digits'
),
});

View File

@@ -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
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
3. **`client/src/App.tsx`** - Route `/forgot-password`
### Backend
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)
```
┌─────────────────────────────────────┐
│ 🏨 Hotel Icon (Blue) │
Quên mật khẩu?
Nhập email để nhận link...
Forgot password?
Enter email to receive link... │
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ Email │ │
│ │ [📧 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 │
│ │
│ Email đã được gửi!
Chúng tôi đã gửi link đến
│ Email has been sent!
We have sent a link to
│ user@example.com │
├─────────────────────────────────────┤
Lưu ý:
│ • Link có hiệu lực trong 1 giờ
│ • Kiểm tra cả thư mục Spam/Junk
│ • Nếu không nhận được, thử lại
Note:
│ • Link is valid for 1 hour
│ • Check Spam/Junk folder
│ • If not received, try again
├─────────────────────────────────────┤
│ [📧 Gửi lại email] │
│ [← Quay lại đăng nhập]
│ [📧 Resend email]
│ [← Back to login]
└─────────────────────────────────────┘
```
### 3. Two-State Design Pattern
**Form State** - Nhập email
**Success State** - Hiển thị xác nhận & hướng dẫn
**Form State** - Enter email
**Success State** - Display confirmation & instructions
State management:
```typescript
@@ -61,19 +61,19 @@ const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState('');
```
## 🔧 Features Chi Tiết
## 🔧 Detailed Features
### 1. Validation (Yup Schema)
```typescript
email:
- Required: "Email là bắt buộc"
- Valid format: "Email không hợp lệ"
- Required: "Email is required"
- Valid format: "Invalid email format"
- Trim whitespace
```
### 2. Form Field
- **Email input** với Mail icon
- Auto-focus khi load page
- **Email input** with Mail icon
- Auto-focus when page loads
- Validation real-time
- Error message inline
@@ -82,12 +82,12 @@ email:
{isLoading ? (
<>
<Loader2 className="animate-spin" />
Đang xử ...
Processing...
</>
) : (
<>
<Send />
Gửi link đặt lại mật khẩu
Send reset link
</>
)}
```
@@ -104,15 +104,15 @@ email:
- Can resend email
**Action Buttons**
- "Gửi lại email" - Reset to form state
- "Quay lại đăng nhập" - Navigate to /login
- "Resend email" - Reset to form state
- "Back to login" - Navigate to /login
### 5. Help Section
```tsx
<div className="bg-white rounded-lg shadow-sm border">
<h3>Cần trợ giúp?</h3>
<h3>Need help?</h3>
<p>
Liên hệ: support@hotel.com hoặc 1900-xxxx
Contact: support@hotel.com or 1900-xxxx
</p>
</div>
```
@@ -267,7 +267,7 @@ CREATE TABLE password_reset_tokens (
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)
@@ -286,7 +286,7 @@ CREATE TABLE password_reset_tokens (
10. Click link → /reset-password/:token
11. Enter new password (Chức năng 7)
11. Enter new password (Function 7)
```
## 🧪 Test Scenarios
@@ -305,7 +305,7 @@ Expected:
```
Input: email = "notanemail"
Expected:
- Validation error: "Email không hợp lệ"
- Validation error: "Invalid email format"
- Form not submitted
```
@@ -313,7 +313,7 @@ Expected:
```
Input: email = ""
Expected:
- Validation error: "Email là bắt buộc"
- Validation error: "Email is required"
- Form not submitted
```
@@ -323,12 +323,12 @@ Action: Submit form
Expected:
- Button disabled
- Spinner shows
- Text: "Đang xử lý..."
- Text: "Processing..."
```
### Test Case 5: Resend email
```
Action: Click "Gửi lại email" in success state
Action: Click "Resend email" in success state
Expected:
- Return to form state
- Email field cleared
@@ -337,7 +337,7 @@ Expected:
### Test Case 6: Back to login
```
Action: Click "Quay lại đăng nhập"
Action: Click "Back to login"
Expected:
- 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
**Next:** Chức năng 7 - Reset Password (form to change password with token)
**Status:**Function 6 completed
**Next:** Function 7 - Reset Password (form to change password with token)
**Test URL:** http://localhost:5173/forgot-password
**API:** POST /api/auth/forgot-password

View File

@@ -1,48 +1,48 @@
# Layout Components - Chức năng 1
# Layout Components - Function 1
## Tổng quan
Đã triển khai thành công **Chức năng 1: Layout cơ bản** bao gồm:
## Overview
Successfully implemented **Function 1: Basic Layout** including:
### Components đã tạo
### Components Created
#### 1. **Header** (`src/components/layout/Header.tsx`)
- Logo và tên ứng dụng
- Sticky header với shadow
- Logo and application name
- Sticky header with shadow
- 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`)
- Thông tin công ty
- Quick links (Liên kết nhanh)
- Support links (Hỗ trợ)
- Contact info (Thông tin liên hệ)
- Company information
- Quick links
- Support links
- Contact information
- Social media icons
- Copyright info
- Copyright information
- Fully responsive (4 columns → 2 → 1)
#### 3. **Navbar** (`src/components/layout/Navbar.tsx`)
- **Trạng thái chưa đăng nhập**:
- Hiển thị nút "Đăng nhập" và "Đăng ký"
- **Trạng thái đã đăng nhập**:
- Hiển thị avatar/tên user
- Dropdown menu với "Hồ sơ", "Quản trị" (admin), "Đăng xuất"
- Mobile menu với hamburger icon
- Responsive cho desktop mobile
- **Not logged in state**:
- Display "Login" and "Register" buttons
- **Logged in state**:
- Display avatar/user name
- Dropdown menu with "Profile", "Admin" (admin), "Logout"
- Mobile menu with hamburger icon
- Responsive for desktop and mobile
#### 4. **SidebarAdmin** (`src/components/layout/SidebarAdmin.tsx`)
- Chỉ hiển thị cho role = "admin"
- Collapsible sidebar (mở/đóng)
- Only displays for role = "admin"
- Collapsible sidebar (open/close)
- Menu items: Dashboard, Users, Rooms, Bookings, Payments, Services, Promotions, Banners, Reports, Settings
- Active state highlighting
- Responsive design
#### 5. **LayoutMain** (`src/components/layout/LayoutMain.tsx`)
- Tích hợp Header, Navbar, Footer
- Sử dụng `<Outlet />` để render nội dung động
- Integrates Header, Navbar, Footer
- Uses `<Outlet />` to render dynamic content
- 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/
├── components/
@@ -62,13 +62,13 @@ src/
└── main.tsx
```
### Cách sử dụng
### Usage
#### 1. Import Layout o App
#### 1. Import Layout into App
```tsx
import LayoutMain from './components/layout/LayoutMain';
// Trong Routes
// In Routes
<Route
path="/"
element={
@@ -80,11 +80,11 @@ import LayoutMain from './components/layout/LayoutMain';
}
>
<Route index element={<HomePage />} />
{/* Các route con khác */}
{/* Other child routes */}
</Route>
```
#### 2. Sử dụng SidebarAdmin cho trang Admin
#### 2. Use SidebarAdmin for Admin Pages
```tsx
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] Header.tsx với logo navigation
- [x] Footer.tsx với thông tin đầy đủ
- [x] Navbar.tsx với logic đăng nhập/đăng xuất động
- [x] SidebarAdmin.tsx chỉ hiển thị với role admin
- [x] LayoutMain.tsx sử dụng `<Outlet />`
- [x] Navbar thay đổi theo trạng thái đăng nhập
- [x] Giao diện responsive, tương thích desktop/mobile
- [x] Tích hợp TailwindCSS cho styling
- [x] Export tất cả components qua index.ts
- [x] Create `src/components/layout/` directory
- [x] Header.tsx with logo and navigation
- [x] Footer.tsx with complete information
- [x] Navbar.tsx with dynamic login/logout logic
- [x] SidebarAdmin.tsx only displays with admin role
- [x] LayoutMain.tsx uses `<Outlet />`
- [x] Navbar changes based on login state
- [x] Responsive interface, compatible with desktop/mobile
- [x] TailwindCSS integration for styling
- [x] Export all components via index.ts
### Demo Routes đã tạo
### Demo Routes Created
**Public Routes** (với LayoutMain):
- `/` - Trang chủ
- `/rooms` - Danh sách phòng
- `/bookings` - Đặt phòng
- `/about` - Giới thiệu
**Public Routes** (with LayoutMain):
- `/` - Home
- `/rooms` - Room list
- `/bookings` - Bookings
- `/about` - About
**Auth Routes** (không có layout):
- `/login` - Đăng nhập
- `/register` - Đăng ký
- `/forgot-password` - Quên mật khẩu
**Auth Routes** (no layout):
- `/login` - Login
- `/register` - Register
- `/forgot-password` - Forgot password
**Admin Routes** (với SidebarAdmin):
**Admin Routes** (with SidebarAdmin):
- `/admin/dashboard` - Dashboard
- `/admin/users` - Quản lý người dùng
- `/admin/rooms` - Quản lý phòng
- `/admin/bookings` - Quản lý đặt phòng
- `/admin/payments` - Quản lý thanh toán
- `/admin/services` - Quản lý dịch vụ
- `/admin/promotions` - Quản lý khuyến mãi
- `/admin/banners` - Quản lý banner
- `/admin/users` - User Management
- `/admin/rooms` - Room Management
- `/admin/bookings` - Booking Management
- `/admin/payments` - Payment Management
- `/admin/services` - Service Management
- `/admin/promotions` - Promotion Management
- `/admin/banners` - Banner Management
### Chạy ứng dụng
### Run Application
```bash
# Di chuyển vào thư mục client
# Navigate to client directory
cd client
# Cài đặt dependencies (nếu chưa cài)
# Install dependencies (if not installed)
npm install
# Chạy development server
# Run development server
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
- AdminRoute component
- Redirect logic
**Chức năng 3**: useAuthStore (Zustand Store)
- Quản lý authentication state
**Function 3**: useAuthStore (Zustand Store)
- Manage authentication state
- 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
- RegisterPage
- ForgotPasswordPage
- ResetPasswordPage
### Notes
- Layout components được thiết kế để tái sử dụng
- Props-based design cho flexibility
- Sẵn sàng tích hợp với Zustand store
- Tailwind classes tuân thủ 80 ký tự/dòng
- Icons sử dụng lucide-react (đã có trong dependencies)
- Layout components designed for reusability
- Props-based design for flexibility
- Ready to integrate with Zustand store
- Tailwind classes follow 80 characters/line
- Icons use lucide-react (already in dependencies)

View File

@@ -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:
-Validation form bằng React Hook Form + Yup
-Hiển thị/ẩn mật khẩu
-Checkbox "Nhớ đăng nhập" (7 ngày)
- ✅ Loading state trong quá trình đăng nhập
-Hiển thị lỗi từ server
- ✅ Redirect sau khi đăng nhập thành công
-UI đẹp với Tailwind CSS Lucide Icons
Login form has been fully implemented with:
-Form validation with React Hook Form + Yup
-Show/hide password
-"Remember me" checkbox (7 days)
- ✅ Loading state during login process
-Display errors from server
- ✅ Redirect after successful login
-Beautiful UI with Tailwind CSS and Lucide Icons
- ✅ Responsive design
## 🗂️ Các File Đã Tạo/Cập Nhật
## 🗂️ Files Created/Updated
### 1. **LoginPage.tsx** - Component form đăng nhập
**Đường dẫn:** `client/src/pages/auth/LoginPage.tsx`
### 1. **LoginPage.tsx** - Login form component
**Path:** `client/src/pages/auth/LoginPage.tsx`
```typescript
// Các tính năng chính:
- React Hook Form với Yup validation
// Main features:
- React Hook Form with Yup validation
- Show/hide password toggle
- Remember me checkbox
- Loading state với spinner
- Loading state with spinner
- Error handling
- Redirect với location state
- Redirect with location state
```
### 2. **index.ts** - Export module
**Đường dẫn:** `client/src/pages/auth/index.ts`
**Path:** `client/src/pages/auth/index.ts`
```typescript
export { default as LoginPage } from './LoginPage';
```
### 3. **App.tsx** - Đã cập nhật routing
**Đường dẫn:** `client/src/App.tsx`
### 3. **App.tsx** - Routing updated
**Path:** `client/src/App.tsx`
```typescript
// Đã thêm:
// Added:
import { LoginPage } from './pages/auth';
// Route:
<Route path="/login" element={<LoginPage />} />
```
## 🎨 Cấu Trúc UI
## 🎨 UI Structure
### Layout
```
┌─────────────────────────────────────┐
│ 🏨 Hotel Icon │
Đăng nhập
Chào mừng bạn trở lại...
Login
Welcome back...
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ [Error message if any] │ │
@@ -60,72 +60,72 @@ import { LoginPage } from './pages/auth';
│ │ Email │ │
│ │ [📧 email@example.com ] │ │
│ ├───────────────────────────────┤ │
│ │ Mật khẩu │ │
│ │ Password │ │
│ │ [🔒 •••••••• 👁️] │ │
│ ├───────────────────────────────┤ │
│ │ ☑️ Nhớ đăng nhập │ │
│ │ Quên mật khẩu? → │ │
│ │ ☑️ Remember me │ │
│ │ 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
# URL
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ệ |
| Password | password | ✅ | Min 8 ký tự |
| Email | text | ✅ | Valid email |
| Password | password | ✅ | Min 8 characters |
| Remember Me | checkbox | ❌ | Boolean |
### 3. Validation Rules
**Email:**
```typescript
- Required: "Email là bắt buộc"
- Valid email format: "Email không hợp lệ"
- Required: "Email is required"
- Valid email format: "Invalid email format"
- Trim whitespace
```
**Password:**
```typescript
- Required: "Mật khẩu là bắt buộc"
- Min 8 characters: "Mật khẩu phải có ít nhất 8 ký tự"
- Required: "Password is required"
- 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
2. Click "Đăng nhập"
3. Validation form (client-side)
4. Nếu valid:
- Button disabled + hiển thị loading
- Gọi useAuthStore.login()
1. User enters email + password
2. Click "Login"
3. Form validation (client-side)
4. If valid:
- Button disabled + show loading
- Call useAuthStore.login()
- API POST /api/auth/login
5. Nếu thành công:
- Lưu token o localStorage
5. If successful:
- Save token to localStorage
- Update Zustand state
- Redirect đến /dashboard
6. Nếu lỗi:
- Hiển thị error message
- Button enabled lại
- Redirect to /dashboard
6. If error:
- Display error message
- Button enabled again
```
## 🎯 Tính Năng Chính
## 🎯 Main Features
### 1. Show/Hide Password
@@ -141,33 +141,33 @@ const [showPassword, setShowPassword] = useState(false);
<input type={showPassword ? 'text' : 'password'} />
```
### 2. Remember Me (7 ngày)
### 2. Remember Me (7 days)
```typescript
// Checkbox
<input {...register('rememberMe')} type="checkbox" />
// Logic trong authService.login()
// Logic in authService.login()
if (rememberMe) {
// Token sẽ được lưu trong localStorage
// và không bị xóa khi đóng trình duyệt
// Token will be saved in localStorage
// and won't be deleted when closing browser
}
```
### 3. Loading State
```typescript
// Button disabled khi đang loading
// Button disabled when loading
<button disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="animate-spin" />
Đang xử ...
Processing...
</>
) : (
<>
<LogIn />
Đăng nhập
Login
</>
)}
</button>
@@ -176,10 +176,10 @@ if (rememberMe) {
### 4. Error Handling
```typescript
// Error từ Zustand store
// Error from Zustand store
const { error } = useAuthStore();
// Hiển thị error message
// Display error message
{error && (
<div className="bg-red-50 border border-red-200">
{error}
@@ -190,20 +190,20 @@ const { error } = useAuthStore();
### 5. Redirect Logic
```typescript
// Lấy location state từ ProtectedRoute
// Get location state from ProtectedRoute
const location = useLocation();
// Redirect về trang trước đó hoặc dashboard
// Redirect to previous page or dashboard
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
```
## 🔗 Integration với Zustand Store
## 🔗 Integration with Zustand Store
```typescript
// Hook usage
const {
login, // Function để login
login, // Function to login
isLoading, // Loading state
error, // Error message
clearError // Clear error
@@ -217,7 +217,7 @@ await login({
});
```
## 🎨 Styling với Tailwind
## 🎨 Styling with Tailwind
### Color Scheme
```
@@ -232,7 +232,7 @@ await login({
// Container
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"
```
@@ -242,20 +242,20 @@ className="grid grid-cols-1 md:grid-cols-2"
**Test Case 1: Empty form**
```
- Input: Submit form trống
- Expected: Hiển thị lỗi "Email là bắt buộc"
- Input: Submit empty form
- Expected: Display error "Email is required"
```
**Test Case 2: Invalid email**
```
- Input: Email = "notanemail"
- Expected: "Email không hợp lệ"
- Expected: "Invalid email format"
```
**Test Case 3: Short password**
```
- 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
@@ -269,13 +269,13 @@ className="grid grid-cols-1 md:grid-cols-2"
**Test Case 5: Invalid credentials**
```
- Input: Wrong password
- Expected: Error message từ server
- Expected: Error message from server
```
**Test Case 6: Network error**
```
- Input: Server offline
- Expected: Error message "Có lỗi xảy ra"
- Expected: Error message "An error occurred"
```
### 3. UX Testing
@@ -288,14 +288,14 @@ className="grid grid-cols-1 md:grid-cols-2"
**Test Case 8: Remember me**
```
- Action: Check "Nhớ đăng nhập"
- Expected: Token persist sau khi reload
- Action: Check "Remember me"
- Expected: Token persists after reload
```
**Test Case 9: Loading state**
```
- Action: Submit form
- Expected: Button disabled, spinner hiển thị
- Expected: Button disabled, spinner displayed
```
## 🔐 Security Features
@@ -311,18 +311,18 @@ onClick={() => setShowPassword(!showPassword)}
### 2. HTTPS Only (Production)
```typescript
// Trong .env
// In .env
VITE_API_URL=https://api.yourdomain.com
```
### 3. Token Storage
```typescript
// LocalStorage cho remember me
// LocalStorage for remember me
if (rememberMe) {
localStorage.setItem('token', token);
}
// SessionStorage cho session only
// SessionStorage for session only
else {
sessionStorage.setItem('token', token);
}
@@ -355,22 +355,22 @@ else {
Remember form state
```
## 🚀 Next Steps (Chức năng 5-7)
## 🚀 Next Steps (Function 5-7)
1. **Chức năng 5: Form Register**
- Copy structure từ LoginPage
- Thêm fields: name, phone, confirmPassword
1. **Function 5: Register Form**
- Copy structure from LoginPage
- Add fields: name, phone, confirmPassword
- Use registerSchema
- Redirect to /login after success
2. **Chức năng 6: Forgot Password**
- Simple form với email only
2. **Function 6: Forgot Password**
- Simple form with email only
- Send reset link
- Success message
3. **Chức năng 7: Reset Password**
- Form với password + confirmPassword
- Token từ URL params
3. **Function 7: Reset Password**
- Form with password + confirmPassword
- Token from URL params
- Redirect to /login after success
## 🐛 Troubleshooting
@@ -382,24 +382,24 @@ Solution: Check token expiry time
- Refresh token: 7 days
```
### Issue 2: Form không submit
### Issue 2: Form doesn't submit
```typescript
Solution: Check console for validation errors
- Open DevTools > Console
- Look for Yup validation errors
```
### Issue 3: Redirect không hoạt động
### Issue 3: Redirect doesn't work
```typescript
Solution: Check location state
console.log(location.state?.from);
```
### Issue 4: Remember me không work
### Issue 4: Remember me doesn't work
```typescript
Solution: Check localStorage
- Open DevTools > Application > Local Storage
- Check "token" "refreshToken" keys
- Check "token" and "refreshToken" keys
```
## 📚 Resources
@@ -428,5 +428,5 @@ Solution: Check localStorage
---
**Status:**Chức năng 4 hoàn thành
**Next:** Chức năng 5 - Form Register
**Status:**Function 4 completed
**Next:** Function 5 - Register Form

View File

@@ -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ý
**Đường dẫn:** `client/src/pages/auth/RegisterPage.tsx`
### 1. **RegisterPage.tsx** - Register form component
**Path:** `client/src/pages/auth/RegisterPage.tsx`
### 2. **index.ts** - Export module
**Đường dẫn:** `client/src/pages/auth/index.ts`
- Đã thêm export RegisterPage
**Path:** `client/src/pages/auth/index.ts`
- Added export RegisterPage
### 3. **App.tsx** - Cập nhật routing
**Đường dẫn:** `client/src/App.tsx`
- Đã thêm route `/register`
### 3. **App.tsx** - Routing updated
**Path:** `client/src/App.tsx`
- Added route `/register`
## ✨ Tính Năng Chính
## ✨ Main Features
### 1. Form Fields (5 fields)
**Họ và tên** (name)
- Required, 2-50 ký tự
**Full Name** (name)
- Required, 2-50 characters
- Icon: User
- Placeholder: "Nguyễn Văn A"
- Placeholder: "John Doe"
**Email**
- Required, valid email format
- Icon: Mail
- Placeholder: "email@example.com"
**Số điện thoại** (phone) - Optional
- 10-11 chữ số
**Phone Number** (phone) - Optional
- 10-11 digits
- Icon: Phone
- Placeholder: "0123456789"
**Mật khẩu** (password)
**Password** (password)
- Required, min 8 chars
- Must contain: uppercase, lowercase, number, special char
- Show/hide toggle với Eye icon
- Show/hide toggle with Eye icon
- Icon: Lock
**Xác nhận mật khẩu** (confirmPassword)
**Confirm Password** (confirmPassword)
- Must match password
- Show/hide toggle với Eye icon
- Show/hide toggle with Eye icon
- Icon: Lock
### 2. Password Strength Indicator
**Visual Progress Bar** với 5 levels:
1. 🔴 Rất yếu (0/5)
2. 🟠 Yếu (1/5)
3. 🟡 Trung bình (2/5)
4. 🔵 Mạnh (3/5)
5. 🟢 Rất mạnh (5/5)
**Visual Progress Bar** with 5 levels:
1. 🔴 Very weak (0/5)
2. 🟠 Weak (1/5)
3. 🟡 Medium (2/5)
4. 🔵 Strong (3/5)
5. 🟢 Very strong (5/5)
**Real-time Requirements Checker:**
- ✅/❌ Ít nhất 8 ký tự
- ✅/❌ Chữ thường (a-z)
- ✅/❌ Chữ hoa (A-Z)
- ✅/❌ Số (0-9)
- ✅/❌ Ký tự đặc biệt (@$!%*?&)
- ✅/❌ At least 8 characters
- ✅/❌ Lowercase (a-z)
- ✅/❌ Uppercase (A-Z)
- ✅/❌ Number (0-9)
- ✅/❌ Special character (@$!%*?&)
### 3. Validation Rules (Yup Schema)
```typescript
name:
- Required: "Họ tên là bắt buộc"
- Min 2 chars: "Họ tên phải có ít nhất 2 ký tự"
- Max 50 chars: "Họ tên không được quá 50 ký tự"
- Required: "Full name is required"
- Min 2 chars: "Full name must be at least 2 characters"
- Max 50 chars: "Full name must not exceed 50 characters"
- Trim whitespace
email:
- Required: "Email là bắt buộc"
- Valid format: "Email không hợp lệ"
- Required: "Email is required"
- Valid format: "Invalid email format"
- Trim whitespace
phone (optional):
- Pattern /^[0-9]{10,11}$/
- Error: "Số điện thoại không hợp lệ"
- Error: "Invalid phone number"
password:
- Required: "Mật khẩu là bắt buộc"
- Required: "Password is required"
- Min 8 chars
- 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:
- Required: "Vui lòng xác nhận mật khẩu"
- Must match password: "Mật khẩu không khớp"
- Required: "Please confirm password"
- Must match password: "Passwords do not match"
```
### 4. UX Features
@@ -93,32 +93,32 @@ confirmPassword:
{isLoading ? (
<>
<Loader2 className="animate-spin" />
Đang xử ...
Processing...
</>
) : (
<>
<UserPlus />
Đăng
Register
</>
)}
```
**Show/Hide Password** (2 toggles)
- Eye/EyeOff icons
- Separate toggle cho password confirmPassword
- Visual feedback khi hover
- Separate toggle for password and confirmPassword
- Visual feedback on hover
**Error Display**
- Inline validation errors dưới mỗi field
- Global error message top của form
- Red border cho fields có lỗi
- Inline validation errors under each field
- Global error message at top of form
- Red border for fields with errors
**Success Flow**
```typescript
1. Submit form
2. Validation passes
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
```
@@ -135,38 +135,38 @@ confirmPassword:
```
┌─────────────────────────────────────┐
│ 🏨 Hotel Icon (Purple) │
Đăng ký tài khoản
Tạo tài khoản mới để đặt phòng...
Register Account
Create a new account to book...
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ [Error message if any] │ │
│ ├───────────────────────────────┤ │
│ │ Họ và tên │ │
│ │ [👤 Nguyễn Văn A ] │ │
│ │ Full Name │ │
│ │ [👤 John Doe ] │ │
│ ├───────────────────────────────┤ │
│ │ Email │ │
│ │ [📧 email@example.com ] │ │
│ ├───────────────────────────────┤ │
│ │ Số điện thoại (Tùy chọn) │ │
│ │ Phone Number (Optional) │ │
│ │ [📱 0123456789 ] │ │
│ ├───────────────────────────────┤ │
│ │ Mật khẩu │ │
│ │ Password │ │
│ │ [🔒 •••••••• 👁️] │ │
│ │ ▓▓▓▓▓░░░░░ Rất mạnh │ │
│ │ ✅ Ít nhất 8 ký tự │ │
│ │ ✅ Chữ thường (a-z) │ │
│ │ ✅ Chữ hoa (A-Z) │ │
│ │ ✅ Số (0-9) │ │
│ │ ✅ Ký tự đặc biệt │ │
│ │ ▓▓▓▓▓░░░░░ Very strong │ │
│ │ ✅ At least 8 characters │ │
│ │ ✅ Lowercase (a-z) │ │
│ │ ✅ Uppercase (A-Z) │ │
│ │ ✅ Number (0-9) │ │
│ │ ✅ 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
```typescript
{
name: string; // "Nguyễn Văn A"
name: string; // "John Doe"
email: string; // "user@example.com"
password: string; // "Password123@"
phone?: string; // "0123456789" (optional)
@@ -195,7 +195,7 @@ POST /api/auth/register
"data": {
"user": {
"id": 1,
"name": "Nguyễn Văn A",
"name": "John Doe",
"email": "user@example.com",
"phone": "0123456789",
"role": "customer"
@@ -235,20 +235,20 @@ Expected: Show validation errors for name, email, password
**Test Case 2: Invalid email**
```
Input: email = "notanemail"
Expected: "Email không hợp lệ"
Expected: "Invalid email format"
```
**Test Case 3: Short name**
```
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**
```
Input: password = "abc123"
Expected: "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
Password strength: Yếu/Trung bình
Expected: "Password must contain uppercase, lowercase, number and special characters"
Password strength: Weak/Medium
```
**Test Case 5: Password mismatch**
@@ -256,13 +256,13 @@ Password strength: Yếu/Trung bình
Input:
password = "Password123@"
confirmPassword = "Password456@"
Expected: "Mật khẩu không khớp"
Expected: "Passwords do not match"
```
**Test Case 6: Invalid phone**
```
Input: phone = "123"
Expected: "Số điện thoại không hợp lệ"
Expected: "Invalid phone number"
```
### 2. UX Tests
@@ -290,7 +290,7 @@ Action: Submit valid form
Expected:
- Button disabled
- Spinner shows
- Text changes to "Đang xử lý..."
- Text changes to "Processing..."
```
### 3. Integration Tests
@@ -300,7 +300,7 @@ Expected:
Input: All valid data
Expected:
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
```
@@ -317,7 +317,7 @@ Expected:
```
Scenario: Server offline
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
```
@@ -335,7 +335,7 @@ function getPasswordStrength(pwd: string) {
return {
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]
};
}
@@ -347,7 +347,7 @@ function getPasswordStrength(pwd: string) {
RegisterPage/
├── Header Section
├── Hotel Icon (purple)
├── Title: "Đăng ký tài khoản"
├── Title: "Register Account"
└── Subtitle
├── Form Container (white card)
@@ -364,7 +364,7 @@ RegisterPage/
└── Submit Button (with loading)
├── Login Link
└── "Đã có tài khoản? Đăng nhập ngay"
└── "Already have an account? Login now"
└── Footer Links
├── Terms of Service
@@ -413,7 +413,7 @@ http://localhost:5173/register
### Example Registration
```typescript
Name: "Nguyễn Văn A"
Name: "John Doe"
Email: "nguyenvana@example.com"
Phone: "0123456789"
Password: "Password123@"
@@ -481,6 +481,6 @@ to /login Show errors
---
**Status:**Chức năng 5 hoàn thành
**Next:** Chức năng 6 - Forgot Password
**Status:**Function 5 completed
**Next:** Function 6 - Forgot Password
**Test URL:** http://localhost:5173/register

View File

@@ -1,19 +1,19 @@
# 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:
- **ProtectedRoute**: Yêu cầu user phải đăng nhập
- **AdminRoute**: Yêu cầu user phải là Admin
The system uses 2 components to protect routes:
- **ProtectedRoute**: Requires user to be logged in
- **AdminRoute**: Requires user to be Admin
---
## 1. ProtectedRoute
### Mục đích
Bảo vệ các route yêu cầu authentication (đăng nhập).
### Purpose
Protects routes requiring authentication (login).
### Cách hoạt động
### How It Works
```tsx
// File: client/src/components/auth/ProtectedRoute.tsx
@@ -23,32 +23,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
const location = useLocation();
const { isAuthenticated, isLoading } = useAuthStore();
// 1. Nếu đang loading → hiển thị spinner
// 1. If loading → display spinner
if (isLoading) {
return <LoadingScreen />;
}
// 2. Nếu chưa đăng nhập → redirect /login
// 2. If not logged in → redirect /login
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{ from: location }} // Lưu location để quay lại
state={{ from: location }} // Save location to return later
replace
/>
);
}
// 3. Đã đăng nhập → cho phép truy cập
// 3. Logged in → allow access
return <>{children}</>;
};
```
### Sử dụng trong App.tsx
### Usage in App.tsx
```tsx
import { ProtectedRoute } from './components/auth';
// Route yêu cầu đăng nhập
// Route requiring login
<Route
path="/dashboard"
element={
@@ -77,20 +77,20 @@ import { ProtectedRoute } from './components/auth';
/>
```
### Luồng hoạt động
1. User chưa đăng nhập truy cập `/dashboard`
2. ProtectedRoute kiểm tra `isAuthenticated === false`
3. Redirect về `/login` và lưu `state={{ from: '/dashboard' }}`
4. Sau khi login thành công, redirect về `/dashboard`
### Flow
1. User not logged in accesses `/dashboard`
2. ProtectedRoute checks `isAuthenticated === false`
3. Redirect to `/login` and save `state={{ from: '/dashboard' }}`
4. After successful login, redirect to `/dashboard`
---
## 2. AdminRoute
### Mục đích
Bảo vệ các route chỉ dành cho Admin (role-based access).
### Purpose
Protects routes for Admin only (role-based access).
### Cách hoạt động
### How It Works
```tsx
// File: client/src/components/auth/AdminRoute.tsx
@@ -100,12 +100,12 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
const location = useLocation();
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
// 1. Nếu đang loading → hiển thị spinner
// 1. If loading → display spinner
if (isLoading) {
return <LoadingScreen />;
}
// 2. Nếu chưa đăng nhập → redirect /login
// 2. If not logged in → redirect /login
if (!isAuthenticated) {
return (
<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';
if (!isAdmin) {
return <Navigate to="/" replace />;
}
// 4. admin → cho phép truy cập
// 4. Is admin → allow access
return <>{children}</>;
};
```
### Sử dụng trong App.tsx
### Usage in App.tsx
```tsx
import { AdminRoute } from './components/auth';
// Route chỉ dành cho Admin
// Route for Admin only
<Route
path="/admin"
element={
@@ -148,50 +148,50 @@ import { AdminRoute } from './components/auth';
</Route>
```
### Luồng hoạt động
### Flow
#### Case 1: User chưa đăng nhập
1. Truy cập `/admin`
2. AdminRoute kiểm tra `isAuthenticated === false`
3. Redirect về `/login` với `state={{ from: '/admin' }}`
4. Sau login thành công → quay lại `/admin`
5. AdminRoute kiểm tra lại role
#### Case 1: User not logged in
1. Access `/admin`
2. AdminRoute checks `isAuthenticated === false`
3. Redirect to `/login` with `state={{ from: '/admin' }}`
4. After successful login → return to `/admin`
5. AdminRoute checks role again
#### Case 2: User đã đăng nhập nhưng không phải Admin
1. Customer (role='customer') truy cập `/admin`
2. AdminRoute kiểm tra `isAuthenticated === true`
3. AdminRoute kiểm tra `userInfo.role === 'customer'` (không phải 'admin')
4. Redirect về `/` (trang chủ)
#### Case 2: User logged in but not Admin
1. Customer (role='customer') accesses `/admin`
2. AdminRoute checks `isAuthenticated === true`
3. AdminRoute checks `userInfo.role === 'customer'` (not 'admin')
4. Redirect to `/` (homepage)
#### Case 3: User Admin
1. Admin (role='admin') truy cập `/admin`
2. AdminRoute kiểm tra `isAuthenticated === true`
3. AdminRoute kiểm tra `userInfo.role === 'admin'`
4. Cho phép truy cập
#### Case 3: User is Admin
1. Admin (role='admin') accesses `/admin`
2. AdminRoute checks `isAuthenticated === true`
3. AdminRoute checks `userInfo.role === 'admin'`
4. Allow access
---
## 3. Cấu trúc Route trong App.tsx
## 3. Route Structure in App.tsx
```tsx
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public Routes - Không cần đăng nhập */}
{/* Public Routes - No login required */}
<Route path="/" element={<LayoutMain />}>
<Route index element={<HomePage />} />
<Route path="rooms" element={<RoomListPage />} />
<Route path="about" element={<AboutPage />} />
</Route>
{/* Auth Routes - Không cần layout */}
{/* Auth Routes - No layout */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<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="dashboard"
@@ -219,7 +219,7 @@ function App() {
/>
</Route>
{/* Admin Routes - Chỉ Admin */}
{/* Admin Routes - Admin only */}
<Route
path="/admin"
element={
@@ -251,7 +251,7 @@ function App() {
---
## 4. Tích hợp với Zustand Store
## 4. Integration with Zustand Store
### useAuthStore State
```tsx
@@ -286,17 +286,17 @@ const useAuthStore = create<AuthStore>((set) => ({
```
### User Roles
- **admin**: Quản trị viên (full access)
- **staff**: Nhân viên (limited access)
- **customer**: Khách hàng (customer features only)
- **admin**: Administrator (full access)
- **staff**: Staff (limited access)
- **customer**: Customer (customer features only)
---
## 5. Loading State
Cả 2 component đều xử lý loading state để tránh:
- Flash of redirect (nhấp nháy khi chuyển trang)
- Race condition (auth state chưa load xong)
Both components handle loading state to avoid:
- Flash of redirect (flickering when changing pages)
- Race condition (auth state not loaded yet)
```tsx
if (isLoading) {
@@ -306,7 +306,7 @@ if (isLoading) {
<div className="animate-spin rounded-full h-12 w-12
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>
);
@@ -330,12 +330,12 @@ const LoginPage: React.FC = () => {
try {
await login(data);
// Redirect về page ban đầu hoặc /dashboard
// Redirect to original page or /dashboard
navigate(from, { replace: true });
toast.success('Đăng nhập thành công!');
toast.success('Login successful!');
} catch (error) {
toast.error('Đăng nhập thất bại!');
toast.error('Login failed!');
}
};
@@ -348,58 +348,58 @@ const LoginPage: React.FC = () => {
```
### Flow
1. User truy cập `/bookings` (protected)
1. User accesses `/bookings` (protected)
2. Redirect `/login?from=/bookings`
3. Login thành công
4. Redirect về `/bookings` (page ban đầu)
3. Login successful
4. Redirect to `/bookings` (original page)
---
## 7. Testing Route Protection
### Test Case 1: ProtectedRoute - Unauthenticated
**Given**: User chưa đăng nhập
**When**: Truy cập `/dashboard`
**Then**: Redirect về `/login`
**And**: Lưu `from=/dashboard` trong location state
**Given**: User not logged in
**When**: Access `/dashboard`
**Then**: Redirect to `/login`
**And**: Save `from=/dashboard` in location state
### Test Case 2: ProtectedRoute - Authenticated
**Given**: User đã đăng nhập
**When**: Truy cập `/dashboard`
**Then**: Hiển thị DashboardPage thành công
**Given**: User logged in
**When**: Access `/dashboard`
**Then**: Display DashboardPage successfully
### Test Case 3: AdminRoute - Not Admin
**Given**: User role='customer'
**When**: Truy cập `/admin`
**Then**: Redirect về `/` (trang chủ)
**Given**: User has role='customer'
**When**: Access `/admin`
**Then**: Redirect to `/` (homepage)
### Test Case 4: AdminRoute - Is Admin
**Given**: User role='admin'
**When**: Truy cập `/admin`
**Then**: Hiển thị AdminLayout thành công
**Given**: User has role='admin'
**When**: Access `/admin`
**Then**: Display AdminLayout successfully
### Test Case 5: Loading State
**Given**: Auth đang initialize
**Given**: Auth is initializing
**When**: isLoading === true
**Then**: Hiển thị loading spinner
**And**: Không redirect
**Then**: Display loading spinner
**And**: No redirect
---
## 8. Security Best Practices
### ✅ Đã Implement
### ✅ Implemented
1. **Client-side protection**: ProtectedRoute & AdminRoute
2. **Token persistence**: localStorage
3. **Role-based access**: Kiểm tra userInfo.role
4. **Location state**: Lưu "from" để redirect về đúng page
5. **Loading state**: Tránh flash của redirect
6. **Replace navigation**: Không lưu lịch sử redirect
3. **Role-based access**: Check userInfo.role
4. **Location state**: Save "from" to redirect to correct page
5. **Loading state**: Avoid flash of redirect
6. **Replace navigation**: Don't save redirect history
### ⚠️ Lưu Ý
- Client-side protection **không đủ** → Phải có backend validation
- API endpoints phải kiểm tra JWT + role
- Middleware backend: `auth`, `adminOnly`
### ⚠️ Note
- Client-side protection **is not enough** → Must have backend validation
- API endpoints must check JWT + role
- Backend middleware: `auth`, `adminOnly`
- Never trust client-side role → Always verify on server
### Backend Middleware Example
@@ -440,45 +440,45 @@ router.get('/admin/users', auth, adminOnly, getUsers);
## 9. Troubleshooting
### Vấn đề 1: Infinite redirect loop
**Nguyên nhân**: ProtectedRoute check sai logic
**Giải pháp**: Đảm bảo `replace={true}` trong Navigate
### Issue 1: Infinite redirect loop
**Cause**: ProtectedRoute check logic error
**Solution**: Ensure `replace={true}` in Navigate
### Vấn đề 2: Flash of redirect
**Nguyên nhân**: Không handle loading state
**Giải pháp**: Thêm check `if (isLoading)` trước check auth
### Issue 2: Flash of redirect
**Cause**: Not handling loading state
**Solution**: Add check `if (isLoading)` before auth check
### Vấn đề 3: Lost location state
**Nguyên nhân**: Không pass `state={{ from: location }}`
**Giải pháp**: Luôn lưu location khi redirect
### Issue 3: Lost location state
**Cause**: Not passing `state={{ from: location }}`
**Solution**: Always save location when redirecting
### Vấn đề 4: Admin có thể truy cập nhưng API fail
**Nguyên nhân**: Backend không verify role
**Giải pháp**: Thêm middleware `adminOnly` trên API routes
### Issue 4: Admin can access but API fails
**Cause**: Backend doesn't verify role
**Solution**: Add `adminOnly` middleware on API routes
---
## 10. Summary
### ProtectedRoute
-Kiểm tra `isAuthenticated`
- ✅ Redirect `/login` nếu chưa đăng nhập
-Lưu location state để quay lại
-Check `isAuthenticated`
- ✅ Redirect `/login` if not logged in
-Save location state to return
- ✅ Handle loading state
### AdminRoute
-Kiểm tra `isAuthenticated` trước
-Kiểm tra `userInfo.role === 'admin'`
- ✅ Redirect `/login` nếu chưa đăng nhập
- ✅ Redirect `/` nếu không phải admin
-Check `isAuthenticated` first
-Check `userInfo.role === 'admin'`
- ✅ Redirect `/login` if not logged in
- ✅ Redirect `/` if not admin
- ✅ Handle loading state
### Kết quả
- Bảo vệ toàn bộ protected routes
- UX mượt mà, không flash
- Role-based access hoạt động chính xác
- Security tốt (kết hợp backend validation)
### Results
- Protect all protected routes
- Smooth UX, no flash
- Role-based access works correctly
- Good security (combined with backend validation)
---
**Chức năng 8 hoàn thành! ✅**
**Function 8 completed! ✅**

View File

@@ -3,9 +3,9 @@
## 📦 Files Created
### Core Server Files
1. **`.env`** - Environment configuration (với mật khẩu và secrets)
2. **`src/server.js`** - Server entry point với database connection
3. **`src/app.js`** - Express application setup với middleware
1. **`.env`** - Environment configuration (with passwords and secrets)
2. **`src/server.js`** - Server entry point with database connection
3. **`src/app.js`** - Express application setup with middleware
### Controllers
4. **`src/controllers/authController.js`** - Authentication logic
@@ -230,11 +230,11 @@ CLIENT_URL=http://localhost:5173
### 1. Database Setup
```bash
# Tạo database
# Create database
mysql -u root -p
CREATE DATABASE hotel_db;
# Chạy migrations
# Run migrations
cd d:/hotel-booking/server
npm run migrate

View File

@@ -1,4 +1,4 @@
# Test Scenarios - Route Protection (Chức năng 8)
# Test Scenarios - Route Protection (Function 8)
## Test Setup
@@ -205,7 +205,7 @@ Verify that loading state is displayed during auth check.
### Expected Result
- ✅ Loading spinner displayed
- ✅ Text "Đang tải..." or "Đang xác thực..." visible
- ✅ Text "Loading..." or "Authenticating..." visible
- ✅ No flash of redirect
- ✅ Smooth transition after loading
@@ -336,7 +336,7 @@ Verify that logout clears auth and redirects properly.
- ✅ Token removed from localStorage
- ✅ userInfo removed from localStorage
- ✅ Redirected to `/` or `/login`
- ✅ Navbar shows "Đăng nhập" button
- ✅ Navbar shows "Login" button
- ✅ Cannot access protected routes anymore
### Actual Result
@@ -430,7 +430,7 @@ Verify that non-existent routes show 404 page.
### Expected Result
- ✅ 404 page displayed
- ✅ "404 - Không tìm thấy trang" message
- ✅ "404 - Page not found" message
- ✅ URL shows `/non-existent-route`
- ✅ No errors in console

View File

@@ -1,30 +1,30 @@
# 🏨 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
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).
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.
## 1. Introduction
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).
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)
**Vai trò sử dụng:** Manager, Admin
### 2.1.1 Setup Rooms (Room Management)
**User Roles:** Manager, Admin
**Các chức năng:**
- Thêm mới phòng
- Chỉnh sửa thông tin phòng
- Xoá phòng *(chỉ khi phòng chưa có booking)*
- Upload hình ảnh phòng
**Functions:**
- Add new room
- Edit room information
- Delete room *(only when room has no bookings)*
- Upload room images
**Thông tin phòng gồm:**
**Room Information:**
- RoomID
- Description
- 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
- Pictures
**Quy tắc:**
- Validate toàn bộ dữ liệu khi thêm/sửa
- Không cho xoá phòng đã phát sinh booking
**Rules:**
- Validate all data when adding/editing
- Do not allow deletion of rooms that have bookings
---
### 2.1.2 Setup Services (Quản lý dịch vụ)
**Vai trò:** Manager, Admin
### 2.1.2 Setup Services (Service Management)
**Roles:** Manager, Admin
**Chức năng:**
- Thêm dịch vụ
- Chỉnh sửa
- Xoá dịch vụ
**Functions:**
- Add service
- Edit
- Delete service
**Thông tin dịch vụ:**
**Service Information:**
- Service ID
- Service Name
- Description
- Unit (giờ, suất, lần,…)
- Unit (hour, portion, time, …)
- Price
**Quy tắc:**
- Validate tất cả dữ liệu nhập
**Rules:**
- Validate all input data
---
### 2.1.3 Promotion Management (Quản lý khuyến mãi)
**Vai trò:** Manager, Admin
### 2.1.3 Promotion Management
**Roles:** Manager, Admin
**Chức năng:**
**Functions:**
- Add promotion
- Edit 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
- Name
- 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
**Vai trò:** Staff, Manager, Admin
**Roles:** Staff, Manager, Admin
**Chức năng:**
- Tìm booking theo tên khách, số booking, ngày đặt
- Xem chi tiết booking
- Xem bill dịch vụ
- Xử lý yêu cầu:
- Hủy booking
**Functions:**
- Search booking by guest name, booking number, booking date
- View booking details
- View service bill
- Process requests:
- Cancel booking
- Checkout
---
## 2.2.2 Check-in
**Vai trò:** Staff, Manager
**Roles:** Staff, Manager
**Quy trình check-in:**
- Khách xuất trình Booking Number
- Nhân viên kiểm tra thông tin booking
- Nhập thông tin từng khách trong phòng
- Gán số phòng thực tế
- Thu thêm phí nếu có trẻ em hoặc extra person
**Check-in Process:**
- Guest presents Booking Number
- Staff verifies booking information
- Enter information for each guest in the room
- Assign actual room number
- 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ụ)
**Vai trò:** Staff
## 2.2.3 Use Services (Guest Service Registration)
**Roles:** Staff
**Chức năng:**
- Đăng ký dịch vụ cho khách dựa trên Room Number
- In ticket nếu có yêu cầu
**Functions:**
- Register services for guests based on Room Number
- Print ticket if requested
---
## 2.2.4 Check-out
**Vai trò:** Staff, Manager
**Roles:** Staff, Manager
**Chức năng:**
- Tính toán:
- Phí phòng
- Phí dịch vụ
- Phụ phí khác
- Tạo hóa đơn (Invoice)
- Khấu trừ tiền đã đặt cọc (booking value)
- Khách thanh toán phần còn lại
**Functions:**
- Calculate:
- Room fee
- Service fee
- Other surcharges
- Create invoice
- Deduct deposit amount (booking value)
- 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:**
- Nhập khoảng thời gian From → To
- Liệt kê toàn bộ booking trong khoảng thời gian
- Tính tổng doanh thu
- Xuất báo cáo:
**Functions:**
- Enter time range From → To
- List all bookings within the time range
- Calculate total revenue
- Export reports:
- Excel
- PDF
**Nội dung báo cáo:**
**Report Content:**
- Booking ID
- Customer Name
- 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
**Vai trò:** Admin
**Roles:** Admin
**Chức năng:**
**Functions:**
- Add user
- Edit user
- Delete user
- View user detail
- List tất cả user
- n role (Admin, Manager, Staff)
- List all users
- Assign role (Admin, Manager, Staff)
---
## 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:
| Role | Quyền |
### Defined Roles:
| Role | Permissions |
|------|-------|
| **Customer** | Không cần login |
| **Staff (Sale)** | Truy cập Operation Module |
| **Manager** | Truy cập Setup Module |
| **Admin** | Toàn quyền, bao gồm User & Security |
| **Customer** | No login required |
| **Staff (Sale)** | Access Operation Module |
| **Manager** | Access Setup Module |
| **Admin** | Full access, including User & Security |
### Quy tắc bảo mật:
- Nhân viên & admin bắt buộc phải login
- Quyền thao tác phụ thuộc vào role
- Session timeout sau 30 phút không hoạt động
### Security Rules:
- Staff & admin must login
- Operation permissions depend on role
- 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 |
| Service Setup | Full | CRUD dịch vụ |
| Promotion Setup | Full | CRUD khuyến mãi |
| Booking Management | Full | Xem, duyệt, hủy booking |
| Check-in / Check-out | Full | Quản lý vận hành |
| Service Usage | Full | Ghi log dịch vụ |
| Reports | Full | Thống kê, xuất file |
| User Management | Full | Quản lý nhân viên |
| Security | Full | Role, phân quyền |
| Room Setup | Full | CRUD rooms |
| Service Setup | Full | CRUD services |
| Promotion Setup | Full | CRUD promotions |
| Booking Management | Full | View, approve, cancel bookings |
| Check-in / Check-out | Full | Operations management |
| Service Usage | Full | Service logging |
| Reports | Full | Statistics, export files |
| User Management | Full | Staff management |
| Security | Full | Roles, permissions |
---
# 4. Kết luận
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.
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.
# 4. Conclusion
The above analysis helps identify all the functions that need to be implemented for **Admin / Manager / Staff** in the hotel management system.
This document can be used to build the database, API, UI/UX, and system permissions.

View File

@@ -1,41 +1,41 @@
# 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
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.
### Objective
Create a foundational layout for the entire system and structure content rendering by route.
#### Nhiệm vụ chi tiết
- Tạo thư mục:
#### Detailed Tasks
- Create directory:
```
src/components/layouts/
```
- Bao gồm:
- Include:
+ Header.jsx
+ Footer.jsx
+ Navbar.jsx
+ SidebarAdmin.jsx
+ LayoutMain.jsx
- Dùng <Outlet /> trong LayoutMain để render nội dung động.
- Navbar thay đổi tùy trạng thái đăng nhập:
+ Nếu chưa login → hiển thị nút “Đăng nhập / Đăng ký”.
+ Nếu đã login → hiển thị avatar, tên user và nút “Đăng xuất”.
- SidebarAdmin chỉ hiển thị với role = admin.
- Use <Outlet /> in LayoutMain to render dynamic content.
- Navbar changes based on login status:
+ If not logged in → display "Login / Register" button.
+ If logged in → display avatar, user name and "Logout" button.
- SidebarAdmin only displays with role = admin.
### Kết quả mong đợi
1. Layout tổng thể hiển thị ổn định.
2. Navbar hiển thị nội dung động theo trạng thái người dùng.
3. Giao diện responsive, tương thích desktop/mobile.
### Expected Results
1. Overall layout displays stably.
2. Navbar displays dynamic content based on user status.
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
Thiết lập hệ thống định tuyến chuẩn, có bảo vệ route theo role.
### Objective
Set up a standard routing system with role-based route protection.
#### Nhiệm vụ chi tiết
- Cấu trúc route chính:
#### Detailed Tasks
- Main route structure:
```
<Route path="/" element={<LayoutMain />}>
<Route index element={<HomePage />} />
@@ -51,25 +51,25 @@
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/admin/*" element={<AdminRoute><AdminModule /></AdminRoute>} />
```
- Dùng ProtectedRoute AdminRoute để kiểm tra:
- Use ProtectedRoute and AdminRoute to check:
+ isAuthenticated
+ role === "admin"
### Kết quả mong đợi
1. Người dùng không đăng nhập bị redirect về /login.
2. AdminRoute chỉ cho phép admin truy cập.
3. Tất cả route hoạt động mượt, không lỗi vòng lặp redirect.
### Expected Results
1. Unauthenticated users are redirected to /login.
2. AdminRoute only allows admin access.
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
Quản lý trạng thái xác thực toàn cục (token, userInfo, role).
### Objective
Manage global authentication state (token, userInfo, role).
#### Nhiệm vụ chi tiết
- Tạo src/stores/useAuthStore.js
- Cấu trúc:
#### Detailed Tasks
- Create src/stores/useAuthStore.js
- Structure:
```
const useAuthStore = create((set) => ({
token: localStorage.getItem("token") || null,
@@ -82,132 +82,132 @@
resetPassword: async (payload) => { ... },
}));
```
- Khi đăng nhập thành công:
+ Lưu token + userInfo o localStorage.
- Khi logout:
+ Xóa localStorage reset state.
- When login succeeds:
+ Save token + userInfo to localStorage.
- When logout:
+ Clear localStorage and reset state.
### Kết quả mong đợi
1. Toàn bộ thông tin user được quản lý tập trung.
2. Duy trì đăng nhập sau khi reload trang.
3. Dễ dàng truy cập userInfo trong mọi component.
### Expected Results
1. All user information is managed centrally.
2. Maintain login after page reload.
3. Easy access to userInfo in any component.
---
## Chức năng 4: Form Login
## Function 4: Login Form
### Mục tiêu
Cho phép người dùng đăng nhập hệ thống.
### Objective
Allow users to log into the system.
#### Nhiệm vụ chi tiết
- Tạo LoginPage.jsx
- Dùng React Hook Form + Yup validate:
+ Email hợp lệ
+ Mật khẩu ≥ 8 ký tự
#### Detailed Tasks
- Create LoginPage.jsx
- Use React Hook Form + Yup validation:
+ Valid email
+ Password ≥ 8 characters
- API:
```
POST /api/auth/login
```
- Sau khi đăng nhập thành công:
+ Lưu token o localStorage.
+ Gọi setUser() để cập nhật Zustand.
+ Redirect về /dashboard.
+ Gửi email POST /api/notify/login-success.
- UX nâng cao:
+ Nút loading khi đang gửi form.
+ “Hiện/Ẩn mật khẩu”.
+ “Nhớ đăng nhập” → lưu 7 ngày.
- After successful login:
+ Save token to localStorage.
+ Call setUser() to update Zustand.
+ Redirect to /dashboard.
+ Send email POST /api/notify/login-success.
- Enhanced UX:
+ Loading button when submitting form.
+ "Show/Hide password".
+ "Remember me" → save for 7 days.
### Kết quả mong đợi
1. Đăng nhập hoạt động mượt, hiển thị thông báo lỗi rõ ràng.
2. Email được gửi khi login thành công.
3. Chuyển hướng đúng theo vai trò user.
### Expected Results
1. Login works smoothly, displays clear error messages.
2. Email is sent when login succeeds.
3. Redirect correctly based on user role.
---
## Chức năng 5: Form Register
## Function 5: Register Form
### Mục tiêu
Cho phép người dùng đăng ký tài khoản mới.
### Objective
Allow users to register a new account.
#### Nhiệm vụ chi tiết
- Tạo RegisterPage.jsx
- Dùng React Hook Form + Yup validate:
+ Họ tên không rỗng
+ Email hợp lệ
+ Mật khẩu ≥ 8 ký tự, có ký tự đặc biệt
#### Detailed Tasks
- Create RegisterPage.jsx
- Use React Hook Form + Yup validation:
+ Full name not empty
+ Valid email
+ Password ≥ 8 characters, contains special characters
- API:
```
POST /api/auth/register
```
- Sau khi đăng ký thành công:
+ Hiển thị toast “Đăng ký thành công, vui lòng đăng nhập”.
+ Redirect về /login.
- After successful registration:
+ Display toast "Registration successful, please login".
+ Redirect to /login.
### Kết quả mong đợi
1. Người dùng tạo tài khoản mới thành công.
2. Validate chặt chẽ, UX mượt mà.
3. Giao diện thống nhất với form login.
### Expected Results
1. Users can create new accounts successfully.
2. Strict validation, smooth UX.
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
Cung cấp chức năng gửi email reset mật khẩu.
### Objective
Provide functionality to send password reset email.
#### Nhiệm vụ chi tiết
- Tạo ForgotPasswordPage.jsx
#### Detailed Tasks
- Create ForgotPasswordPage.jsx
- API:
```
POST /api/auth/forgot-password
```
- Sau khi gửi thành công:
+ Hiển thị thông báo “Vui lòng kiểm tra email để đặt lại mật khẩu.”
+ Backend gửi link reset có token dạng:
- After successful send:
+ Display message "Please check your email to reset password."
+ Backend sends reset link with token:
```
https://domain.com/reset-password/:token
```
### Kết quả mong đợi
1. Gửi email thành công.
2. UX rõ ràng, có loading và thông báo lỗi.
3. Giao diện thân thiện.
### Expected Results
1. Email sent successfully.
2. Clear UX, with loading and error messages.
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
Cho phép người dùng đổi mật khẩu thông qua link email.
### Objective
Allow users to change password through email link.
#### Nhiệm vụ chi tiết
- Tạo ResetPasswordPage.jsx
- Validate:
+ Mật khẩu mới ≥ 8 ký tự, chứa ký tự đặc biệt
+ Nhập lại mật khẩu trùng khớp
#### Detailed Tasks
- Create ResetPasswordPage.jsx
- Validation:
+ New password ≥ 8 characters, contains special characters
+ Confirm password matches
- API:
```
POST /api/auth/reset-password
```
- Sau khi đổi mật khẩu thành công:
+ Gửi email xác nhận POST /api/notify/reset-success.
+ Redirect về /login.
- After successful password change:
+ Send confirmation email POST /api/notify/reset-success.
+ Redirect to /login.
### Kết quả mong đợi
1. Mật khẩu được cập nhật thành công.
2. Gửi email thông báo thành công.
3. Bảo vệ token hết hạn (invalid token → redirect về forgot-password).
### Expected Results
1. Password updated successfully.
2. Success notification email sent.
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
Chặn truy cập trái phép và bảo vệ các route quan trọng.
### Objective
Block unauthorized access and protect important routes.
#### Nhiệm vụ chi tiết
- Tạo component ProtectedRoute.jsx:
#### Detailed Tasks
- Create ProtectedRoute.jsx component:
```
const ProtectedRoute = ({ children }) => {
const { isAuthenticated } = useAuthStore();
@@ -216,13 +216,13 @@
};
```
- Tạo AdminRoute.jsx:
- Create AdminRoute.jsx:
```
const AdminRoute = ({ children }) => {
const { userInfo } = useAuthStore();
return userInfo?.role === "admin" ? children : <Navigate to="/" replace />;
};
```
### Kết quả mong đợi
1. Chỉ người dùng hợp lệ mới truy cập được route quan trọng.
2. AdminRoute đảm bảo bảo mật cho module quản trị.
### Expected Results
1. Only valid users can access important routes.
2. AdminRoute ensures security for admin module.

View File

@@ -1,182 +1,182 @@
# 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
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.
### Objective
Create a homepage interface introducing featured rooms, banner and navigation to room list.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
1. Route: /
2. Banner:
```
GET /api/banners?position=home
```
- Nếu không có banner → hiển thị ảnh mặc định.
- Có thể dùng Carousel hoặc ảnh tĩnh.
3. Phòng nổi bật:
- If no banner → display default image.
- Can use Carousel or static image.
3. Featured rooms:
```
GET /api/rooms?featured=true
```
- Hiển thị 46 phòng bằng component RoomCard.
- Nút “Xem tất cả phòng” → điều hướng /rooms.
4. Loading skeleton trong khi chờ dữ liệu.
- Display 46 rooms using RoomCard component.
- "View all rooms" button → navigate to /rooms.
4. Loading skeleton while waiting for data.
### Kết quả mong đợi
1. Trang chủ hiển thị banner và danh sách phòng nổi bật rõ ràng.
2. Khi không có banner → ảnh fallback được hiển thị.
3. Phòng nổi bật load từ API, giới hạn 46 phòng.
4. UX mượt, có skeleton khi load.
5. Nút “Xem tất cả phòng” điều hướng chính xác đến /rooms.
### Expected Results
1. Homepage displays banner and featured room list clearly.
2. When no banner → fallback image is displayed.
3. Featured rooms load from API, limited to 46 rooms.
4. Smooth UX, with skeleton when loading.
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
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.
### Objective
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
2. Bộ lọc (component RoomFilter):
- Trường lọc: loại phòng, giá minmax, số người.
- Khi submit → gọi API:
2. Filters (RoomFilter component):
- Filter fields: room type, minmax price, number of guests.
- On submit → call API:
```
GET /api/rooms?type=&minPrice=&maxPrice=&capacity=&page=
```
- Lưu bộ lọc vào URL query.
- Nút “Reset” để xóa toàn bộ bộ lọc.
3. Phân trang (Pagination component).
4. Hiển thị danh sách bằng RoomCard.
- Save filters to URL query.
- "Reset" button to clear all filters.
3. Pagination (Pagination component).
4. Display list using RoomCard.
### Kết quả mong đợi
1. Danh sách phòng hiển thị chính xác theo filter.
2. Bộ lọc hoạt động mượt, có thể reset dễ dàng.
3. Phân trang hiển thị chính xác số trang.
4. Filter được lưu trong URL (giúp reload không mất).
5. Giao diện responsive, dễ đọc, không bị vỡ.
### Expected Results
1. Room list displays accurately according to filter.
2. Filters work smoothly, can reset easily.
3. Pagination displays correct page numbers.
4. Filters saved in URL (helps reload without losing).
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
Tạo trang chi tiết phòng đầy đủ thông tin, hình ảnh, tiện ích và khu vực đánh giá.
### Objective
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
2. Phần nội dung:
-Thông tin phòng (ảnh, mô tả, giá, tiện ích)
- RoomGallery: Carousel ảnh
- RoomAmenities: danh sách tiện ích
- Nút “Đặt ngay” → điều hướng /booking/:roomId
2. Content section:
- Room information (images, description, price, amenities)
- RoomGallery: Image carousel
- RoomAmenities: amenities list
- "Book Now" button → navigate to /booking/:roomId
3. Review Section:
- Lấy danh sách review đã duyệt:
- Get approved review list:
```
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
```
4. Component RatingStars + ReviewForm.
5. Nếu chưa đăng nhập → hiển thị “Vui lòng đăng nhập để đánh giá”.
6. Tính trung bình điểm review.
7. Loading skeleton khi chờ review.
4. RatingStars + ReviewForm component.
5. If not logged in → display "Please login to review".
6. Calculate average review rating.
7. Loading skeleton when waiting for reviews.
### Kết quả mong đợi
1. Hiển thị đầy đủ ảnh, mô tả, tiện ích phòng.
2. Carousel hoạt động mượt mà.
3. Review hiển thị đúng, có trung bình số sao.
4. Người đã đặt có thể viết review (sau duyệt).
5. Nút “Đặt ngay” điều hướng chính xác đến form booking.
6. Skeleton hiển thị khi chờ dữ liệu.
### Expected Results
1. Displays complete images, description, room amenities.
2. Carousel works smoothly.
3. Reviews display correctly, with average star rating.
4. Users who have booked can write reviews (after approval).
5. "Book Now" button navigates correctly to booking form.
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
Cho phép người dùng tìm phòng trống theo ngày và loại phòng.
### Objective
Allow users to find available rooms by date and room type.
#### Nhiệm vụ chi tiết
1. Form tìm kiếm (ở HomePage hoặc RoomListPage):
- Input: ngày đến (from), ngày đi (to), loại phòng.
#### Detailed Tasks
1. Search form (on HomePage or RoomListPage):
- Input: arrival date (from), departure date (to), room type.
2. API:
```
GET /api/rooms/available?from=&to=&type=
```
3. Validate:
3. Validation:
- from < to
- from không nhỏ hơn hôm nay.
4. Kết quả:
- Hiển thị danh sách bằng RoomCard.
- Nếu không có kết quả → “Không tìm thấy phòng phù hợp”.
5. Dùng react-datepicker hoặc react-day-picker.
6. Loading spinner khi đang tìm.
- from not less than today.
4. Results:
- Display list using RoomCard.
- If no results → "No matching rooms found".
5. Use react-datepicker or react-day-picker.
6. Loading spinner while searching.
### Kết quả mong đợi
1. Form tìm phòng hoạt động, validate chính xác.
2. Khi bấm tìm → hiển thị danh sách phòng trống.
3. Nếu không có kết quả → thông báo thân thiện.
4. Loading hiển thị rõ trong lúc chờ.
5. Tìm theo ngày & loại phòng chính xác từ backend.
### Expected Results
1. Room search form works, validates correctly.
2. When clicking search → displays available room list.
3. If no results → friendly message.
4. Loading displays clearly while waiting.
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
Cho phép người dùng thêm, bỏ hoặc xem danh sách phòng yêu thích.
### Objective
Allow users to add, remove or view favorite rooms list.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
1. API:
```
POST /api/favorites/:roomId # Thêm
DELETE /api/favorites/:roomId # Xóa
GET /api/favorites # Lấy danh sách yêu thích
POST /api/favorites/:roomId # Add
DELETE /api/favorites/:roomId # Remove
GET /api/favorites # Get favorites list
```
2. UI:
- FavoriteButton (icon ❤️):
+ Nếu yêu thích → tô đỏ
+ Nếu chưa → viền xám
- Tooltip: “Thêm vào yêu thích” / “Bỏ yêu thích”
3. Nếu chưa đăng nhập:
- Lưu tạm trong localStorage (guestFavorites)
- Khi đăng nhập → đồng bộ với server.
4. Toast thông báo khi thêm/bỏ yêu thích.
+ If favorited → filled red
+ If not → gray border
- Tooltip: "Add to favorites" / "Remove from favorites"
3. If not logged in:
- Save temporarily in localStorage (guestFavorites)
- When logged in → sync with server.
4. Toast notification when adding/removing favorites.
### Kết quả mong đợi
1. Nút ❤️ hoạt động đúng trạng thái (đỏ / xám).
2. Người chưa đăng nhập vẫn có thể lưu tạm yêu thích.
3. Khi đăng nhập → danh sách đồng bộ với backend.
4. Toast hiển thị “Đã thêm vào yêu thích” / “Đã bỏ yêu thích”.
5. API hoạt động đúng, không lỗi 401 khi đăng nhập hợp lệ.
### Expected Results
1. ❤️ button works correctly (red / gray).
2. Unauthenticated users can still save favorites temporarily.
3. When logged in → list syncs with backend.
4. Toast displays "Added to favorites" / "Removed from favorites".
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
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.
### Objective
Improve user experience, optimize loading speed and responsive display capability.
#### Nhiệm vụ chi tiết
1. Loading skeleton khi fetch phòng hoặc review.
2. Debounce khi nhập giá để tránh gọi API liên tục.
3. Infinite scroll (tùy chọn) thay cho pagination.
#### Detailed Tasks
1. Loading skeleton when fetching rooms or reviews.
2. Debounce when entering price to avoid continuous API calls.
3. Infinite scroll (optional) instead of pagination.
4. Responsive layout:
- Desktop: 34 cột
- Tablet: 2 cột
- Mobile: 1 cột
- Desktop: 34 columns
- Tablet: 2 columns
- Mobile: 1 column
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”.
- Không có review → “Hãy là người đầu tiên đánh giá!”.
6. Toast thông báo khi thêm yêu thích, gửi review, lỗi mạng.
- No rooms → display illustration + "No matching rooms found" message.
- No reviews"Be the first to review!".
6. Toast notifications when adding favorites, submitting reviews, network errors.
### Kết quả mong đợi
1. Trang hoạt động mượt, có skeleton khi chờ dữ liệu.
2. Tốc độ phản hồi nhanh (debounce hoạt động).
3. Responsive trên mọi kích thước màn hình.
4. Các empty state hiển thị thân thiện.
5. Toast thông báo rõ ràng, UX thân thiện.
### Expected Results
1. Page works smoothly, has skeleton when waiting for data.
2. Fast response speed (debounce works).
3. Responsive on all screen sizes.
4. Empty states display friendly.
5. Toast notifications clear, friendly UX.
---

View File

@@ -1,144 +1,144 @@
# Booking & Payment
## Chức năng 1: BookingPage Form Đặt phòng
## Function 1: BookingPage Booking Form
### Mục tiêu
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.
### Objective
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:
```
/booking/:roomId
```
2. Khi user click “Đặt ngay” ở RoomDetailPage → chuyển sang BookingPage.
3. Hiển thị:
- Ảnh phòng, tên phòng, giá/đêm
- Thông tin người dùng (tự động điền nếu đã login)
2. When user clicks "Book Now" on RoomDetailPage → navigate to BookingPage.
3. Display:
- Room image, room name, price/night
- User information (auto-fill if logged in)
- Form:
+ Ngày check-in / check-out (DateRangePicker)
+ Số người
+ Ghi chú
+ Phương thức thanh toán:
1. Thanh toán tại chỗ
2. Chuyển khoản (hiển thị QR + hướng dẫn)
4. Validate bằng Yup + React Hook Form:
+ Check-in / check-out date (DateRangePicker)
+ Number of guests
+ Notes
+ Payment method:
1. Pay at hotel
2. Bank transfer (display QR + instructions)
4. Validate using Yup + React Hook Form:
- Check-in < Check-out
- Không bỏ trống ngày
- Có chọn phương thức thanh toán
5. Tính tổng tiền:
- Dates not empty
- Payment method selected
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
- 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
Kết nối và xử lý API liên quan đến đặt phòng.
### Objective
Connect and handle APIs related to booking.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
🔧 Endpoints:
```
POST /api/bookings → Tạo booking
GET /api/bookings/me → Lấy danh sách booking của user
PATCH /api/bookings/:id/cancel → Hủy booking
GET /api/bookings/:id → Chi tiết booking
GET /api/bookings/check/:bookingNumber → Tra cứu booking
POST /api/bookings → Create booking
GET /api/bookings/me → Get user's booking list
PATCH /api/bookings/:id/cancel → Cancel booking
GET /api/bookings/:id → Booking details
GET /api/bookings/check/:bookingNumber → Look up booking
```
🔄 Luồng xử lý:
1. Frontend gọi POST /api/bookings
2. Backend kiểm tra phòng trống:
🔄 Processing flow:
1. Frontend calls POST /api/bookings
2. Backend checks room availability:
```
GET /api/rooms/available?roomId=...&from=...&to=...
```
3. Nếu trống → tạo booking
- Nếu trùng lịch → trả 409 “Phòng đã được đặt trong thời gian này”
4. Gửi email xác nhận booking (nếu cần)
5. Trả về dữ liệu booking để hiển thị /booking-success/:id.
3. If available → create booking
- If schedule conflict → return 409 "Room already booked during this time"
4. Send booking confirmation email (if needed)
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
Hiển thị kết quả đặt phòng thành công và các hành động tiếp theo.
### Objective
Display successful booking result and next actions.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
1. Route: /booking-success/:id
2. Gọi GET /api/bookings/:id → hiển thị chi tiết
3. Nút:
- “Xem đơn của tôi” → /my-bookings
- “Về trang chủ” → /
4. Nếu phương thức là Chuyển khoản:
+ Hiển thị QR code ngân hàng
+ Cho phép upload ảnh xác nhận
+ Gọi POST /api/notify/payment khi người dùng xác nhận đã chuyển khoản.
2. Call GET /api/bookings/:id → display details
3. Buttons:
- "View my bookings" → /my-bookings
- "Go to home" → /
4. If payment method is Bank transfer:
+ Display bank QR code
+ Allow upload confirmation image
+ 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
Hiển thị toàn bộ các đơn đặt của user + cho phép hủy đơn.
### Objective
Display all user's bookings + allow canceling bookings.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
1. Route: /my-bookings
2. API: GET /api/bookings/me
3. Hiển thị danh sách booking:
- Phòng, ngày nhận/trả, tổng tiền
- Trạng thái:
3. Display booking list:
- Room, check-in/check-out dates, total amount
- Status:
🟡 pending
🟢 confirmed
🔴 cancelled
4. Nút “Hủy đặt phòng”:
1. window.confirm("Bạn có chắc muốn hủy không?")
2. Gọi PATCH /api/bookings/:id/cancel (hoặc DELETE /api/bookings/:id tùy implement)
3. Logic hủy:
- Giữ 20% giá trị đơn
- Hoàn 80% còn lại cho user
- Cập nhật trạng thái phòng về available
4. Hiển thị toast “Đơn đã được hủy thành công”
5. Cho phép xem chi tiết booking:
4. "Cancel booking" button:
1. window.confirm("Are you sure you want to cancel?")
2. Call PATCH /api/bookings/:id/cancel (or DELETE /api/bookings/:id depending on implementation)
3. Cancel logic:
- Keep 20% of order value
- Refund remaining 80% to user
- Update room status to available
4. Display toast "Booking cancelled successfully"
5. Allow viewing booking details:
- Route: /bookings/:id
- Gọi GET /api/bookings/:id
- Hiển thị chi tiết phòng, thông tin user, tổng tiền, status.
- Call GET /api/bookings/:id
- 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
Cho phép người dùng chọn phương thức thanh toán và xác nhận thanh toán.
### Objective
Allow users to select payment method and confirm payment.
#### Nhiệm vụ chi tiết
- Phương thức:
1. Thanh toán tại chỗ
- Booking được tạo với status = "pending"
2. Chuyển khoản
- Hiển thị mã QR ngân hàng (tĩnh hoặc từ API)
- Upload ảnh biên lai (image upload)
- Sau khi upload → gọi POST /api/notify/payment gửi email xác nhận
- Cập nhật status = "confirmed"
#### Detailed Tasks
- Payment methods:
1. Pay at hotel
- Booking created with status = "pending"
2. Bank transfer
- Display bank QR code (static or from API)
- Upload receipt image (image upload)
- After upload → call POST /api/notify/payment send confirmation email
- Update status = "confirmed"
---
## Chức năng 6: UX & Hiệu năng
## Function 6: UX & Performance
### Mục tiêu
Cải thiện trải nghiệm người dùng và tính trực quan.
### Objective
Improve user experience and intuitiveness.
#### Nhiệm vụ chi tiết
1. Toasts (react-hot-toast hoặc sonner)
2. Loading spinner rõ ràng
3. DateRangePicker cho chọn ngày
4. Form được validate đầy đủ (và báo lỗi chi tiết)
5. Focus input đầu tiên
6. Tự động redirect khi đặt thành công / hủy đơn
#### Detailed Tasks
1. Toasts (react-hot-toast or sonner)
2. Clear loading spinner
3. DateRangePicker for date selection
4. Form fully validated (and detailed error messages)
5. Focus first input
6. Auto redirect when booking succeeds / canceling booking
---

View File

@@ -1,72 +1,72 @@
# 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
Cho phép người dùng viết đánh giá cho những phòng họ đã đặt thành công.
### Objective
Allow users to write reviews for rooms they have successfully booked.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
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.
POST /api/reviews → Gửi đánh giá.
GET /api/bookings/me → Get list of rooms user has booked.
POST /api/reviews → Submit review.
```
3. Giao diện:
- Hiển thị danh sách phòng đã đặt (tên, ngày ở, trạng thái)
- Nút “Đánh giá” (hiện nếu chưa đánh giá phòng đó)
4. Khi nhấn “Đánh giá” → mở Modal:
- Input chọn số sao (⭐ 15)
- Textarea nhập nội dung bình luận
- Nút “Gửi đánh giá”
5. Validate:
- Rating bắt buộc (15)
- Comment không để trống
6. Sau khi gửi thành công → toast thông báo “Đánh giá của bạn đang chờ duyệt”.
3. Interface:
- Display list of booked rooms (name, stay dates, status)
- "Review" button (shown if room not yet reviewed)
4. When clicking "Review" → open Modal:
- Input to select star rating (⭐ 15)
- Textarea to enter comment content
- "Submit Review" button
5. Validation:
- Rating required (15)
- Comment cannot be empty
6. After successful submission → toast notification "Your review is pending approval".
### Kết quả mong đợi
1. Người dùng chỉ thấy nút “Đánh giá” với phòng đã từng đặt.
2. Modal mở ra và validate chính xác.
3. Gửi thành công → review có trạng thái "pending".
4. Toast hiển thị thông báo hợp lý.
5. Giao diện gọn, trực quan, không lỗi khi chưa có phòng nào đặt.
### Expected Results
1. Users only see "Review" button for rooms they have booked.
2. Modal opens and validates correctly.
3. Successful submission → review has status "pending".
4. Toast displays appropriate notification.
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
Hiển thị danh sách các đánh giá đã được admin duyệt cho từng phòng.
### Objective
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
2. API:
```
GET /api/reviews?roomId={id}&status=approved
```
3. Hiển thị danh sách review:
- Avatar + tên người dùng
- Số sao (⭐)
- Nội dung bình luận
- Ngày đăng (createdAt)
4. Tính và hiển thị điểm trung bình rating (VD: ⭐ 4.2 / 5)
5. Nếu chưa có review → hiển thị: “Chưa có đánh giá nào.”
3. Display review list:
- Avatar + user name
- Star rating (⭐)
- Comment content
- Post date (createdAt)
4. Calculate and display average rating (e.g.: ⭐ 4.2 / 5)
5. If no reviewsdisplay: "No reviews yet."
### Kết quả mong đợi
1. Danh sách review hiển thị đúng theo phòng.
2. Chỉ review status = approved được render.
3. Tính điểm trung bình chính xác (làm tròn 1 chữ số thập phân).
4. Hiển thị avatar, tên, sao, và ngày đầy đủ.
5. Có thông báo “Chưa có đánh giá” khi danh sách trống.
### Expected Results
1. Review list displays correctly by room.
2. Only reviews with status = approved are rendered.
3. Calculate average rating accurately (rounded to 1 decimal place).
4. Display avatar, name, stars, and date completely.
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
Cho phép Admin xem, duyệt hoặc từ chối các đánh giá người dùng gửi lên.
### Objective
Allow Admin to view, approve or reject reviews submitted by users.
#### Nhiệm vụ chi tiết
#### Detailed Tasks
1. Route: /admin/reviews
2. API:
```
@@ -74,67 +74,67 @@
PATCH /api/reviews/:id/approve
PATCH /api/reviews/:id/reject
```
3. Hành động:
Duyệt → review chuyển sang approved
Từ chối → review chuyển sang rejected
4. Sau khi duyệt → cập nhật giao diện và hiển thị toast thông báo.
5. Có filter theo trạng thái (pending, approved, rejected).
3. Actions:
Approve → review changes to approved
Reject → review changes to rejected
4. After approval → update interface and display toast notification.
5. Filter by status (pending, approved, rejected).
### Kết quả mong đợi
1. Admin thấy đầy đủ danh sách review.
2. Duyệt hoặc từ chối hoạt động đúng API.
3. Bảng tự cập nhật khi thay đổi trạng thái.
4. Toast hiển thị rõ “Đã duyệt” hoặc “Đã từ chối”.
5. Chỉ review approved mới hiển thị công khai cho người dùng.
### Expected Results
1. Admin sees complete review list.
2. Approve or reject works correctly with API.
3. Table automatically updates when status changes.
4. Toast clearly displays "Approved" or "Rejected".
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
Đả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.
### Objective
Ensure only valid users can submit reviews and system displays correct data.
#### Nhiệm vụ chi tiết
1. Kiểm tra quyền:
- Người dùng chưa đăng nhập → redirect /login
- Người dùng chưa từng đặt phòng → không hiển thị nút “Đánh giá”
2. Kiểm tra logic:
- Mỗi người chỉ được đánh giá 1 lần / phòng
- Review mặc định status = pending
3. Phân quyền:
- User: chỉ gửi review
- Admin: duyệt / từ chối
- Staff: chỉ xem
#### Detailed Tasks
1. Permission check:
- User not logged in → redirect /login
- User has never booked room → don't display "Review" button
2. Logic check:
- Each person can only review once per room
- Review default status = pending
3. Authorization:
- User: can only submit review
- Admin: approve / reject
- Staff: view only
### Kết quả mong đợi
1. Người chưa đăng nhập không thể gửi review.
2. Mỗi phòng chỉ được review 1 lần bởi 1 user.
3. Dữ liệu hiển thị chính xác theo phân quyền.
4. Review chỉ xuất hiện công khai khi được duyệt.
5. Không có lỗi logic hoặc hiển thị sai trạng thái.
### Expected Results
1. Users not logged in cannot submit reviews.
2. Each room can only be reviewed once by one user.
3. Data displays correctly according to permissions.
4. Reviews only appear publicly when approved.
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
Cải thiện trải nghiệm người dùng và giao diện hiển thị đánh giá.
### Objective
Improve user experience and review display interface.
#### Nhiệm vụ chi tiết
1. Dùng component đánh giá sao trực quan (ví dụ react-rating-stars-component).
2. Format ngày tạo bằng:
#### Detailed Tasks
1. Use intuitive star rating component (e.g., react-rating-stars-component).
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.
4. Dùng toast (react-hot-toast) cho thông báo gửi / duyệt / từ chối.
5. Loading spinner khi chờ API.
3. Add light hover effect when displaying review list.
4. Use toast (react-hot-toast) for submit / approve / reject notifications.
5. Loading spinner when waiting for API.
### Kết quả mong đợi
1. UI mượt mà, dễ đọc và thân thiện.
2. Loading / toast hiển thị đúng trạng thái.
3. Ngày tháng, sao và bình luận được format đẹp.
4. Giao diện quản trị và người dùng thống nhất phong cách.
5. Trải nghiệm người dùng mượt, không giật lag.
### Expected Results
1. Smooth, readable and user-friendly UI.
2. Loading / toast displays correct status.
3. Dates, stars and comments are formatted nicely.
4. Admin and user interfaces have consistent styling.
5. Smooth user experience, no lag or stuttering.
---

View File

@@ -1,31 +1,31 @@
# 🚀 QUICK START - Server Setup
## Bước 1: Copy file .env
## Step 1: Copy .env file
```bash
cd d:/hotel-booking/server
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
# Mở MySQL command line
# Open MySQL command line
mysql -u root -p
# Tạo database
# Create database
CREATE DATABASE hotel_db;
# Thoát
# Exit
exit;
```
## Bước 3: Chạy Migrations
## Step 3: Run Migrations
```bash
cd d:/hotel-booking/server
npm run migrate
```
Lệnh này sẽ tạo các bảng:
This command will create the following tables:
- roles
- users
- refresh_tokens
@@ -41,23 +41,23 @@ Lệnh này sẽ tạo các bảng:
- password_reset_tokens
- reviews
## Bước 4: (Optional) Seed Data
## Step 4: (Optional) Seed Data
```bash
npm run seed
```
Lệnh này sẽ tạo:
This command will create:
- 3 roles: admin, staff, customer
- Demo users
- Demo rooms & room types
- Demo bookings
## Bước 5: Start Server
## Step 5: Start Server
```bash
npm run dev
```
Bạn sẽ thấy:
You will see:
```
✅ Database connection established successfully
📊 Database models synced
@@ -67,12 +67,12 @@ Bạn sẽ thấy:
🏥 Health: http://localhost:3000/health
```
## Bước 6: Test API
## Step 6: Test API
### 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
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
@@ -81,34 +81,34 @@ curl -X POST http://localhost:3000/api/auth/login \
## ⚠️ Troubleshooting
### Lỗi: "Access denied for user 'root'"
**Giải pháp:** Sửa DB_PASS trong file `.env`
### Error: "Access denied for user 'root'"
**Solution:** Update DB_PASS in `.env` file
```bash
DB_PASS=your_mysql_password
```
### Lỗi: "Unknown database 'hotel_db'"
**Giải pháp:** Tạo database thủ công (Bước 2)
### Error: "Unknown database 'hotel_db'"
**Solution:** Create database manually (Step 2)
### Lỗi: "Port 3000 already in use"
**Giải pháp:** Đổi PORT trong `.env`
### Error: "Port 3000 already in use"
**Solution:** Change PORT in `.env`
```bash
PORT=3001
```
### Lỗi: "Cannot find module"
**Giải pháp:** Cài lại dependencies
### Error: "Cannot find module"
**Solution:** Reinstall dependencies
```bash
npm install
```
## 📝 Next Steps
1. ✅ Server đang chạy
2. ✅ Database đã setup
3. ✅ API endpoints sẵn sàng
4. 🔜 Test với frontend login form
5. 🔜 Implement các API còn lại
1. ✅ Server is running
2. ✅ Database is set up
3. ✅ API endpoints are ready
4. 🔜 Test with frontend login form
5. 🔜 Implement remaining APIs
## 🧪 Test với Postman
@@ -146,14 +146,14 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
## ✅ Checklist
- [ ] MySQL đang chạy
- [ ] File .env đã tạo và cấu hình đúng
- [ ] Database hotel_db đã tạo
- [ ] Migrations đã chạy thành công
- [ ] Server đang chạy (port 3000)
- [ ] Health check trả về 200 OK
- [ ] Frontend .env đã có VITE_API_URL=http://localhost:3000
- [ ] Frontend đang chạy (port 5173)
- [ ] MySQL is running
- [ ] .env file has been created and configured correctly
- [ ] Database hotel_db has been created
- [ ] Migrations have run successfully
- [ ] Server is running (port 3000)
- [ ] Health check returns 200 OK
- [ ] Frontend .env has VITE_API_URL=http://localhost:3000
- [ ] Frontend is running (port 5173)
## 🎯 Ready to Test Login!
@@ -162,4 +162,4 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
3. Login page: http://localhost:5173/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! 🚀

View File

@@ -222,7 +222,7 @@ const resetPassword = async (req, res, next) => {
}
if (
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({
status: 'error',

View File

@@ -19,7 +19,7 @@ const addFavorite = async (req, res, next) => {
if (!room) {
return res.status(404).json({
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) {
return res.status(400).json({
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({
status: 'success',
message: 'Đã thêm vào danh sách yêu thích',
message: 'Added to favorites list',
data: {
favorite,
},
@@ -75,7 +75,7 @@ const removeFavorite = async (req, res, next) => {
if (!favorite) {
return res.status(404).json({
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({
status: 'success',
message: 'Đã xóa khỏi danh sách yêu thích',
message: 'Removed from favorites list',
});
} catch (error) {
next(error);

View File

@@ -9,7 +9,7 @@ module.exports = {
// booking_number, service_name, rest of fields
{
booking_number: 'BK2025010001',
service_name: 'Dịch vụ phòng - Bữa sáng',
service_name: 'Room Service - Breakfast',
quantity: 2,
unit_price: 150000,
total_price: 300000,
@@ -20,7 +20,7 @@ module.exports = {
},
{
booking_number: 'BK2025010001',
service_name: 'Dịch vụ giặt ủi - Thông thường',
service_name: 'Laundry Service - Regular',
quantity: 3,
unit_price: 60000,
total_price: 180000,
@@ -31,7 +31,7 @@ module.exports = {
},
{
booking_number: 'BK2025010002',
service_name: 'Dịch vụ phòng - Bữa sáng',
service_name: 'Room Service - Breakfast',
quantity: 1,
unit_price: 150000,
total_price: 150000,
@@ -42,7 +42,7 @@ module.exports = {
},
{
booking_number: 'BK2025010002',
service_name: 'Spa - Massage truyền thống',
service_name: 'Spa - Traditional Massage',
quantity: 1,
unit_price: 500000,
total_price: 500000,
@@ -53,7 +53,7 @@ module.exports = {
},
{
booking_number: 'BK2025010002',
service_name: 'Trả phòng muộn',
service_name: 'Late Check-out',
quantity: 1,
unit_price: 500000,
total_price: 500000,
@@ -64,7 +64,7 @@ module.exports = {
},
{
booking_number: 'BK2025010003',
service_name: 'Đón sân bay',
service_name: 'Airport Pickup',
quantity: 1,
unit_price: 400000,
total_price: 400000,
@@ -75,7 +75,7 @@ module.exports = {
},
{
booking_number: 'BK2025010005',
service_name: 'Đón sân bay',
service_name: 'Airport Pickup',
quantity: 1,
unit_price: 400000,
total_price: 400000,
@@ -86,7 +86,7 @@ module.exports = {
},
{
booking_number: 'BK2025010005',
service_name: 'Spa - Liệu pháp hương thơm',
service_name: 'Spa - Aromatherapy',
quantity: 1,
unit_price: 700000,
total_price: 700000,