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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -229,7 +229,7 @@ const CheckInPage: React.FC = () => {
type="text" type="text"
value={actualRoomNumber} value={actualRoomNumber}
onChange={(e) => setActualRoomNumber(e.target.value)} onChange={(e) => setActualRoomNumber(e.target.value)}
placeholder="VD: 101, 202, 305" placeholder="e.g: 101, 202, 305"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/> />
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
@@ -271,7 +271,7 @@ const CheckInPage: React.FC = () => {
value={guest.name} value={guest.name}
onChange={(e) => handleGuestChange(index, 'name', e.target.value)} onChange={(e) => handleGuestChange(index, 'name', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Nguyễn Văn A" placeholder="John Doe"
/> />
</div> </div>
<div> <div>

View File

@@ -191,7 +191,7 @@ const DashboardPage: React.FC = () => {
))} ))}
</div> </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>
@@ -227,7 +227,7 @@ const DashboardPage: React.FC = () => {
})} })}
</div> </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>
</div> </div>
@@ -236,7 +236,7 @@ const DashboardPage: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Rooms */} {/* Top Rooms */}
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Top phòng đưc đt</h2> <h2 className="text-xl font-semibold text-gray-900 mb-4">Top Booked Rooms</h2>
{stats?.top_rooms && stats.top_rooms.length > 0 ? ( {stats?.top_rooms && stats.top_rooms.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{stats.top_rooms.map((room, index) => ( {stats.top_rooms.map((room, index) => (
@@ -246,8 +246,8 @@ const DashboardPage: React.FC = () => {
{index + 1} {index + 1}
</span> </span>
<div> <div>
<p className="font-medium text-gray-900">Phòng {room.room_number}</p> <p className="font-medium text-gray-900">Room {room.room_number}</p>
<p className="text-sm text-gray-500">{room.bookings} lượt đt</p> <p className="text-sm text-gray-500">{room.bookings} bookings</p>
</div> </div>
</div> </div>
<span className="font-semibold text-green-600"> <span className="font-semibold text-green-600">
@@ -257,20 +257,20 @@ const DashboardPage: React.FC = () => {
))} ))}
</div> </div>
) : ( ) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p> <p className="text-gray-500 text-center py-8">No data available</p>
)} )}
</div> </div>
{/* Service Usage */} {/* Service Usage */}
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Dịch vụ đưc sử dụng</h2> <h2 className="text-xl font-semibold text-gray-900 mb-4">Services Used</h2>
{stats?.service_usage && stats.service_usage.length > 0 ? ( {stats?.service_usage && stats.service_usage.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{stats.service_usage.map((service) => ( {stats.service_usage.map((service) => (
<div key={service.service_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> <div key={service.service_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div> <div>
<p className="font-medium text-gray-900">{service.service_name}</p> <p className="font-medium text-gray-900">{service.service_name}</p>
<p className="text-sm text-gray-500">{service.usage_count} lần sử dụng</p> <p className="text-sm text-gray-500">{service.usage_count} times used</p>
</div> </div>
<span className="font-semibold text-purple-600"> <span className="font-semibold text-purple-600">
{formatCurrency(service.total_revenue)} {formatCurrency(service.total_revenue)}
@@ -279,7 +279,7 @@ const DashboardPage: React.FC = () => {
))} ))}
</div> </div>
) : ( ) : (
<p className="text-gray-500 text-center py-8">Không dữ liệu</p> <p className="text-gray-500 text-center py-8">No data available</p>
)} )}
</div> </div>
</div> </div>

View File

@@ -152,8 +152,8 @@ const PromotionManagementPage: React.FC = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Quản khuyến mãi</h1> <h1 className="text-3xl font-bold text-gray-900">Promotion Management</h1>
<p className="text-gray-500 mt-1">Quản giảm giá chương trình khuyến mãi</p> <p className="text-gray-500 mt-1">Manage discount codes and promotion programs</p>
</div> </div>
<button <button
onClick={() => { onClick={() => {
@@ -163,7 +163,7 @@ const PromotionManagementPage: React.FC = () => {
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2" className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2"
> >
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
Thêm khuyến mãi Add Promotion
</button> </button>
</div> </div>
@@ -175,7 +175,7 @@ const PromotionManagementPage: React.FC = () => {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Tìm theo code hoặc tên..." placeholder="Search by code or name..."
value={filters.search} value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })} onChange={(e) => setFilters({ ...filters, search: e.target.value })}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
@@ -187,18 +187,18 @@ const PromotionManagementPage: React.FC = () => {
onChange={(e) => setFilters({ ...filters, type: e.target.value })} onChange={(e) => setFilters({ ...filters, type: e.target.value })}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="">Tất cả loại</option> <option value="">All Types</option>
<option value="percentage">Phần trăm</option> <option value="percentage">Percentage</option>
<option value="fixed">Số tiền cố đnh</option> <option value="fixed">Fixed Amount</option>
</select> </select>
<select <select
value={filters.status} value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })} onChange={(e) => setFilters({ ...filters, status: e.target.value })}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="">Tất cả trạng thái</option> <option value="">All Statuses</option>
<option value="active">Hoạt đng</option> <option value="active">Active</option>
<option value="inactive">Ngừng</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
</div> </div>
@@ -209,25 +209,25 @@ const PromotionManagementPage: React.FC = () => {
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
code Code
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Tên chương trình Program Name
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Giá trị Value
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Thời gian Period
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Đã dùng Used
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Trạng thái Status
</th> </th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Thao tác Actions
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -301,7 +301,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold"> <h2 className="text-xl font-bold">
{editingPromotion ? 'Cập nhật khuyến mãi' : 'Thêm khuyến mãi mới'} {editingPromotion ? 'Update Promotion' : 'Add New Promotion'}
</h2> </h2>
<button onClick={() => setShowModal(false)}> <button onClick={() => setShowModal(false)}>
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" /> <X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
@@ -311,27 +311,27 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
code <span className="text-red-500">*</span> Code <span className="text-red-500">*</span>
</label> </label>
<input <input
type="text" type="text"
value={formData.code} value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })} onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="VD: SUMMER2024" placeholder="e.g: SUMMER2024"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Tên chương trình <span className="text-red-500">*</span> Program Name <span className="text-red-500">*</span>
</label> </label>
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="VD: Giảm giá mùa hè" placeholder="e.g: Summer Sale"
required required
/> />
</div> </div>
@@ -339,34 +339,34 @@ const PromotionManagementPage: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
tả Description
</label> </label>
<textarea <textarea
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
rows={3} rows={3}
placeholder="Mô tả chi tiết về chương trình..." placeholder="Detailed description of the program..."
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Loại giảm giá <span className="text-red-500">*</span> Discount Type <span className="text-red-500">*</span>
</label> </label>
<select <select
value={formData.discount_type} value={formData.discount_type}
onChange={(e) => setFormData({ ...formData, discount_type: e.target.value as 'percentage' | 'fixed' })} onChange={(e) => setFormData({ ...formData, discount_type: e.target.value as 'percentage' | 'fixed' })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="percentage">Phần trăm (%)</option> <option value="percentage">Percentage (%)</option>
<option value="fixed">Số tiền cố đnh (VND)</option> <option value="fixed">Fixed Amount (VND)</option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Giá trị giảm <span className="text-red-500">*</span> Discount Value <span className="text-red-500">*</span>
</label> </label>
<input <input
type="number" type="number"
@@ -383,7 +383,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Giá trị đơn tối thiểu (VND) Minimum Order Value (VND)
</label> </label>
<input <input
type="number" type="number"
@@ -395,7 +395,7 @@ const PromotionManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Giảm tối đa (VND) Maximum Discount (VND)
</label> </label>
<input <input
type="number" type="number"
@@ -410,7 +410,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Ngày bắt đu <span className="text-red-500">*</span> Start Date <span className="text-red-500">*</span>
</label> </label>
<input <input
type="date" type="date"
@@ -422,7 +422,7 @@ const PromotionManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Ngày kết thúc <span className="text-red-500">*</span> End Date <span className="text-red-500">*</span>
</label> </label>
<input <input
type="date" type="date"
@@ -437,7 +437,7 @@ const PromotionManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Giới hạn lượt dùng (0 = không giới hạn) Usage Limit (0 = unlimited)
</label> </label>
<input <input
type="number" type="number"
@@ -449,15 +449,15 @@ const PromotionManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái Status
</label> </label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'active' | 'inactive' })} onChange={(e) => setFormData({ ...formData, status: e.target.value as 'active' | 'inactive' })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="active">Hoạt đng</option> <option value="active">Active</option>
<option value="inactive">Ngừng</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
</div> </div>
@@ -468,13 +468,13 @@ const PromotionManagementPage: React.FC = () => {
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50" className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
> >
Hủy Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
> >
{editingPromotion ? 'Cập nhật' : 'Thêm mới'} {editingPromotion ? 'Update' : 'Add'}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -150,7 +150,7 @@ const ReviewManagementPage: React.FC = () => {
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
Phòng {review.room?.room_number} - {review.room?.room_type?.name} Room {review.room?.room_number} - {review.room?.room_type?.name}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
@@ -173,14 +173,14 @@ const ReviewManagementPage: React.FC = () => {
<button <button
onClick={() => handleApprove(review.id)} onClick={() => handleApprove(review.id)}
className="text-green-600 hover:text-green-900 mr-3" className="text-green-600 hover:text-green-900 mr-3"
title="Phê duyệt" title="Approve"
> >
</button> </button>
<button <button
onClick={() => handleReject(review.id)} onClick={() => handleReject(review.id)}
className="text-red-600 hover:text-red-900" className="text-red-600 hover:text-red-900"
title="Từ chối" title="Reject"
> >
</button> </button>

View File

@@ -155,29 +155,29 @@ const RoomManagementPage: React.FC = () => {
const handleDeleteImage = async (imageUrl: string) => { const handleDeleteImage = async (imageUrl: string) => {
if (!editingRoom) return; if (!editingRoom) return;
if (!window.confirm('Bạn có chắc muốn xóa ảnh này?')) return; if (!window.confirm('Are you sure you want to delete this image?')) return;
try { try {
await apiClient.delete(`/rooms/${editingRoom.id}/images`, { await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
data: { imageUrl }, data: { imageUrl },
}); });
toast.success('Xóa ảnh thành công'); toast.success('Image deleted successfully');
fetchRooms(); fetchRooms();
// Refresh editing room data // Refresh editing room data
const response = await roomService.getRoomById(editingRoom.id); const response = await roomService.getRoomById(editingRoom.id);
setEditingRoom(response.data.room); setEditingRoom(response.data.room);
} catch (error: any) { } catch (error: any) {
toast.error(error.response?.data?.message || 'Không thể xóa ảnh'); toast.error(error.response?.data?.message || 'Unable to delete image');
} }
}; };
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
const badges: Record<string, { bg: string; text: string; label: string }> = { const badges: Record<string, { bg: string; text: string; label: string }> = {
available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Trống' }, available: { bg: 'bg-green-100', text: 'text-green-800', label: 'Available' },
occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Đang sử dụng' }, occupied: { bg: 'bg-blue-100', text: 'text-blue-800', label: 'Occupied' },
maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Bảo trì' }, maintenance: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Maintenance' },
}; };
const badge = badges[status] || badges.available; const badge = badges[status] || badges.available;
return ( return (
@@ -196,8 +196,8 @@ const RoomManagementPage: React.FC = () => {
{/* Header */} {/* Header */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Quản phòng</h1> <h1 className="text-3xl font-bold text-gray-900">Room Management</h1>
<p className="text-gray-500 mt-1">Quản thông tin phòng khách sạn</p> <p className="text-gray-500 mt-1">Manage hotel room information</p>
</div> </div>
<button <button
onClick={() => { onClick={() => {
@@ -207,7 +207,7 @@ const RoomManagementPage: React.FC = () => {
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
> >
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
Thêm phòng Add Room
</button> </button>
</div> </div>
@@ -218,7 +218,7 @@ const RoomManagementPage: React.FC = () => {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Tìm kiếm phòng..." placeholder="Search rooms..."
value={filters.search} value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })} onChange={(e) => setFilters({ ...filters, search: e.target.value })}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
@@ -229,17 +229,17 @@ const RoomManagementPage: React.FC = () => {
onChange={(e) => setFilters({ ...filters, status: e.target.value })} onChange={(e) => setFilters({ ...filters, status: e.target.value })}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="">Tất cả trạng thái</option> <option value="">All Statuses</option>
<option value="available">Trống</option> <option value="available">Available</option>
<option value="occupied">Đang sử dụng</option> <option value="occupied">Occupied</option>
<option value="maintenance">Bảo trì</option> <option value="maintenance">Maintenance</option>
</select> </select>
<select <select
value={filters.type} value={filters.type}
onChange={(e) => setFilters({ ...filters, type: e.target.value })} onChange={(e) => setFilters({ ...filters, type: e.target.value })}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="">Tất cả loại phòng</option> <option value="">All Room Types</option>
<option value="1">Standard</option> <option value="1">Standard</option>
<option value="2">Deluxe</option> <option value="2">Deluxe</option>
<option value="3">Suite</option> <option value="3">Suite</option>
@@ -253,25 +253,25 @@ const RoomManagementPage: React.FC = () => {
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Số phòng Room Number
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Loại phòng Room Type
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tầng Floor
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Giá Price
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Trạng thái Status
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nổi bật Featured
</th> </th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Thao tác Actions
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -285,7 +285,7 @@ const RoomManagementPage: React.FC = () => {
<div className="text-sm text-gray-900">{room.room_type?.name || 'N/A'}</div> <div className="text-sm text-gray-900">{room.room_type?.name || 'N/A'}</div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">Tầng {room.floor}</div> <div className="text-sm text-gray-900">Floor {room.floor}</div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
@@ -340,7 +340,7 @@ const RoomManagementPage: React.FC = () => {
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold"> <h2 className="text-xl font-bold">
{editingRoom ? 'Cập nhật phòng' : 'Thêm phòng mới'} {editingRoom ? 'Update Room' : 'Add New Room'}
</h2> </h2>
<button onClick={() => setShowModal(false)}> <button onClick={() => setShowModal(false)}>
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" /> <X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
@@ -351,7 +351,7 @@ const RoomManagementPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Số phòng Room Number
</label> </label>
<input <input
type="text" type="text"
@@ -363,7 +363,7 @@ const RoomManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Tầng Floor
</label> </label>
<input <input
type="number" type="number"
@@ -378,7 +378,7 @@ const RoomManagementPage: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Loại phòng Room Type
</label> </label>
<select <select
value={formData.room_type_id} value={formData.room_type_id}
@@ -394,7 +394,7 @@ const RoomManagementPage: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái Status
</label> </label>
<select <select
value={formData.status} value={formData.status}
@@ -402,9 +402,9 @@ const RoomManagementPage: React.FC = () => {
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
required required
> >
<option value="available">Trống</option> <option value="available">Available</option>
<option value="occupied">Đang sử dụng</option> <option value="occupied">Occupied</option>
<option value="maintenance">Bảo trì</option> <option value="maintenance">Maintenance</option>
</select> </select>
</div> </div>
@@ -417,7 +417,7 @@ const RoomManagementPage: React.FC = () => {
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/> />
<label htmlFor="featured" className="ml-2 text-sm text-gray-700"> <label htmlFor="featured" className="ml-2 text-sm text-gray-700">
Phòng nổi bật Featured Room
</label> </label>
</div> </div>
@@ -427,13 +427,13 @@ const RoomManagementPage: React.FC = () => {
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50" className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
> >
Hủy Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
> >
{editingRoom ? 'Cập nhật' : 'Thêm'} {editingRoom ? 'Update' : 'Add'}
</button> </button>
</div> </div>
</form> </form>
@@ -443,13 +443,13 @@ const RoomManagementPage: React.FC = () => {
<div className="mt-6 pt-6 border-t border-gray-200"> <div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<ImageIcon className="w-5 h-5" /> <ImageIcon className="w-5 h-5" />
Hình nh phòng Room Images
</h3> </h3>
{/* Current Images */} {/* Current Images */}
{editingRoom.room_type?.images && editingRoom.room_type.images.length > 0 && ( {editingRoom.room_type?.images && editingRoom.room_type.images.length > 0 && (
<div className="mb-4"> <div className="mb-4">
<p className="text-sm text-gray-600 mb-2">nh hiện tại:</p> <p className="text-sm text-gray-600 mb-2">Current Images:</p>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{editingRoom.room_type.images.map((img, index) => ( {editingRoom.room_type.images.map((img, index) => (
<div key={index} className="relative group"> <div key={index} className="relative group">
@@ -474,7 +474,7 @@ const RoomManagementPage: React.FC = () => {
{/* Upload New Images */} {/* Upload New Images */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Thêm nh mới (tối đa 5 nh): Add New Images (max 5 images):
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
<input <input
@@ -491,12 +491,12 @@ const RoomManagementPage: React.FC = () => {
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2" className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
> >
<Upload className="w-4 h-4" /> <Upload className="w-4 h-4" />
{uploadingImages ? 'Đang tải...' : 'Upload'} {uploadingImages ? 'Uploading...' : 'Upload'}
</button> </button>
</div> </div>
{selectedFiles.length > 0 && ( {selectedFiles.length > 0 && (
<p className="text-sm text-gray-600 mt-2"> <p className="text-sm text-gray-600 mt-2">
{selectedFiles.length} file đã chọn {selectedFiles.length} file(s) selected
</p> </p>
)} )}
</div> </div>

View File

@@ -23,7 +23,7 @@ const ServiceManagementPage: React.FC = () => {
name: '', name: '',
description: '', description: '',
price: 0, price: 0,
unit: 'lần', unit: 'time',
status: 'active' as 'active' | 'inactive', status: 'active' as 'active' | 'inactive',
}); });
@@ -155,9 +155,9 @@ const ServiceManagementPage: React.FC = () => {
onChange={(e) => setFilters({ ...filters, status: e.target.value })} onChange={(e) => setFilters({ ...filters, status: e.target.value })}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="">Tất cả trạng thái</option> <option value="">All Statuses</option>
<option value="active">Hoạt đng</option> <option value="active">Active</option>
<option value="inactive">Tạm dừng</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
</div> </div>
@@ -167,22 +167,22 @@ const ServiceManagementPage: React.FC = () => {
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Tên dịch vụ Service Name
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
tả Description
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Giá Price
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Đơn vị Unit
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Trạng thái Status
</th> </th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase"> <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Thao tác Actions
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -207,7 +207,7 @@ const ServiceManagementPage: React.FC = () => {
? 'bg-green-100 text-green-800' ? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800' : 'bg-gray-100 text-gray-800'
}`}> }`}>
{service.status === 'active' ? 'Hoạt động' : 'Tạm dừng'} {service.status === 'active' ? 'Active' : 'Inactive'}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
@@ -242,7 +242,7 @@ const ServiceManagementPage: React.FC = () => {
<div className="bg-white rounded-lg p-6 w-full max-w-md"> <div className="bg-white rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold"> <h2 className="text-xl font-bold">
{editingService ? 'Cập nhật dịch vụ' : 'Thêm dịch vụ mới'} {editingService ? 'Update Service' : 'Add New Service'}
</h2> </h2>
<button onClick={() => setShowModal(false)}> <button onClick={() => setShowModal(false)}>
<X className="w-6 h-6 text-gray-500 hover:text-gray-700" /> <X className="w-6 h-6 text-gray-500 hover:text-gray-700" />
@@ -251,7 +251,7 @@ const ServiceManagementPage: React.FC = () => {
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Tên dịch vụ Service Name
</label> </label>
<input <input
type="text" type="text"
@@ -263,7 +263,7 @@ const ServiceManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
tả Description
</label> </label>
<textarea <textarea
value={formData.description} value={formData.description}
@@ -274,7 +274,7 @@ const ServiceManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Giá Price
</label> </label>
<input <input
type="number" type="number"
@@ -287,27 +287,27 @@ const ServiceManagementPage: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Đơn vị Unit
</label> </label>
<input <input
type="text" type="text"
value={formData.unit} value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })} onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="VD: lần, giờ, ngày..." placeholder="e.g: time, hour, day..."
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Trạng thái Status
</label> </label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })} onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="active">Hoạt đng</option> <option value="active">Active</option>
<option value="inactive">Tạm dừng</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
<div className="flex gap-3 mt-6"> <div className="flex gap-3 mt-6">
@@ -316,13 +316,13 @@ const ServiceManagementPage: React.FC = () => {
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50" className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
> >
Hủy Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
> >
{editingService ? 'Cập nhật' : 'Thêm'} {editingService ? 'Update' : 'Add'}
</button> </button>
</div> </div>
</form> </form>

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ const RoomListPage: React.FC = () => {
} }
} catch (err) { } catch (err) {
console.error('Error fetching rooms:', err); console.error('Error fetching rooms:', err);
setError('Không thể tải danh sách phòng. Vui lòng thử lại.'); setError('Unable to load room list. Please try again.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -76,12 +76,12 @@ const RoomListPage: React.FC = () => {
disabled:bg-gray-400 mb-6 transition-colors" disabled:bg-gray-400 mb-6 transition-colors"
> >
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
<span>Quay lại trang chủ</span> <span>Back to home</span>
</Link> </Link>
<div className="mb-10"> <div className="mb-10">
<h1 className="text-3xl text-center font-bold text-gray-900"> <h1 className="text-3xl text-center font-bold text-gray-900">
Danh sách phòng Room List
</h1> </h1>
</div> </div>
@@ -126,7 +126,7 @@ const RoomListPage: React.FC = () => {
text-white rounded-lg hover:bg-red-700 text-white rounded-lg hover:bg-red-700
transition-colors" transition-colors"
> >
Thử lại Try Again
</button> </button>
</div> </div>
)} )}
@@ -153,17 +153,17 @@ const RoomListPage: React.FC = () => {
<h3 className="text-xl font-semibold <h3 className="text-xl font-semibold
text-gray-800 mb-2" text-gray-800 mb-2"
> >
Không tìm thấy phòng phù hợp No matching rooms found
</h3> </h3>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 mb-6">
Vui lòng thử điều chỉnh bộ lọc hoặc tìm kiếm khác Please try adjusting the filters or search differently
</p> </p>
<button <button
onClick={() => window.location.href = '/rooms'} onClick={() => window.location.href = '/rooms'}
className="px-6 py-2 bg-blue-600 text-white className="px-6 py-2 bg-blue-600 text-white
rounded-lg hover:bg-blue-700 transition-colors" rounded-lg hover:bg-blue-700 transition-colors"
> >
Xóa bộ lọc Clear Filters
</button> </button>
</div> </div>
)} )}

View File

@@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
// Base URL từ environment hoặc mặc định. Ensure it points to the // Base URL from environment or default. Ensure it points to the
// server API root (append '/api' if not provided) so frontend calls // server API root (append '/api' if not provided) so frontend calls
// like '/bookings/me' resolve to e.g. 'http://localhost:3000/api/bookings/me'. // like '/bookings/me' resolve to e.g. 'http://localhost:3000/api/bookings/me'.
const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:3000';
@@ -12,7 +12,7 @@ const API_BASE_URL = /\/api(\/?$)/i.test(normalized)
? normalized ? normalized
: normalized + '/api'; : normalized + '/api';
// Tạo axios instance // Create axios instance
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
headers: { headers: {
@@ -22,7 +22,7 @@ const apiClient = axios.create({
withCredentials: true, // Enable sending cookies withCredentials: true, // Enable sending cookies
}); });
// Request interceptor - Thêm token o header // Request interceptor - Add token to header
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { (config) => {
// Normalize request URL: if a request path accidentally begins // Normalize request URL: if a request path accidentally begins

View File

@@ -45,12 +45,12 @@ export interface ResetPasswordData {
} }
/** /**
* Auth Service - Xử lý các API calls liên quan * Auth Service - Handles API calls related
* đến authentication * to authentication
*/ */
const authService = { const authService = {
/** /**
* Đăng nhập * Login
*/ */
login: async ( login: async (
credentials: LoginCredentials credentials: LoginCredentials
@@ -63,7 +63,7 @@ const authService = {
}, },
/** /**
* Đăng ký tài khoản mới * Register new account
*/ */
register: async ( register: async (
data: RegisterData data: RegisterData
@@ -76,7 +76,7 @@ const authService = {
}, },
/** /**
* Đăng xuất * Logout
*/ */
logout: async (): Promise<void> => { logout: async (): Promise<void> => {
try { try {
@@ -87,7 +87,7 @@ const authService = {
}, },
/** /**
* Lấy thông tin user hiện tại * Get current user information
*/ */
getProfile: async (): Promise<AuthResponse> => { getProfile: async (): Promise<AuthResponse> => {
const response = await apiClient.get<AuthResponse>( const response = await apiClient.get<AuthResponse>(
@@ -108,7 +108,7 @@ const authService = {
}, },
/** /**
* Quên mật khẩu - Gửi email reset * Forgot password - Send reset email
*/ */
forgotPassword: async ( forgotPassword: async (
data: ForgotPasswordData data: ForgotPasswordData
@@ -121,7 +121,7 @@ const authService = {
}, },
/** /**
* Đặt lại mật khẩu * Reset password
*/ */
resetPassword: async ( resetPassword: async (
data: ResetPasswordData data: ResetPasswordData

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,30 @@
# 🏨 Hotel Management & Booking System # 🏨 Hotel Management & Booking System
## Bản Phân Tích Dành Cho Admin (SRS Admin Analysis) ## Admin Analysis Document (SRS Admin Analysis)
--- ---
## 1. Giới thiệu ## 1. Introduction
Tài liệu này phân tích các yêu cầu từ SRS của hệ thống **Hotel Management & Booking Online (e-Hotel)**, tập trung hoàn toàn vào phần **Admin / Manager / Staff** (không bao gồm khách hàng). This document analyzes the requirements from the SRS of the **Hotel Management & Booking Online (e-Hotel)** system, focusing entirely on the **Admin / Manager / Staff** section (excluding customers).
Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo mật của hệ thống. The goal is to understand the administration, operation, and security functions of the system.
--- ---
# 2. Phân tích chức năng dành cho Admin # 2. Admin Functionality Analysis
--- ---
## 2.1 Setup Module (Thiết lập hệ thống) ## 2.1 Setup Module (System Setup)
### 2.1.1 Setup Rooms (Quản lý phòng) ### 2.1.1 Setup Rooms (Room Management)
**Vai trò sử dụng:** Manager, Admin **User Roles:** Manager, Admin
**Các chức năng:** **Functions:**
- Thêm mới phòng - Add new room
- Chỉnh sửa thông tin phòng - Edit room information
- Xoá phòng *(chỉ khi phòng chưa có booking)* - Delete room *(only when room has no bookings)*
- Upload hình ảnh phòng - Upload room images
**Thông tin phòng gồm:** **Room Information:**
- RoomID - RoomID
- Description - Description
- Type (VIP, DELUX, SUITE, …) - Type (VIP, DELUX, SUITE, …)
@@ -32,114 +32,114 @@ Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo
- Price - Price
- Pictures - Pictures
**Quy tắc:** **Rules:**
- Validate toàn bộ dữ liệu khi thêm/sửa - Validate all data when adding/editing
- Không cho xoá phòng đã phát sinh booking - Do not allow deletion of rooms that have bookings
--- ---
### 2.1.2 Setup Services (Quản lý dịch vụ) ### 2.1.2 Setup Services (Service Management)
**Vai trò:** Manager, Admin **Roles:** Manager, Admin
**Chức năng:** **Functions:**
- Thêm dịch vụ - Add service
- Chỉnh sửa - Edit
- Xoá dịch vụ - Delete service
**Thông tin dịch vụ:** **Service Information:**
- Service ID - Service ID
- Service Name - Service Name
- Description - Description
- Unit (giờ, suất, lần,…) - Unit (hour, portion, time, …)
- Price - Price
**Quy tắc:** **Rules:**
- Validate tất cả dữ liệu nhập - Validate all input data
--- ---
### 2.1.3 Promotion Management (Quản lý khuyến mãi) ### 2.1.3 Promotion Management
**Vai trò:** Manager, Admin **Roles:** Manager, Admin
**Chức năng:** **Functions:**
- Add promotion - Add promotion
- Edit promotion - Edit promotion
- Delete promotion - Delete promotion
- Promotion có thể áp dụng bằng code hoặc tự động trong booking - Promotion can be applied by code or automatically in booking
**Thông tin:** **Information:**
- ID - ID
- Name - Name
- Description - Description
- Value (phần trăm hoặc số tiền) - Value (percentage or fixed amount)
--- ---
# 2.2 Operation Module (Vận hành khách sạn) # 2.2 Operation Module (Hotel Operations)
--- ---
## 2.2.1 Booking Management ## 2.2.1 Booking Management
**Vai trò:** Staff, Manager, Admin **Roles:** Staff, Manager, Admin
**Chức năng:** **Functions:**
- Tìm booking theo tên khách, số booking, ngày đặt - Search booking by guest name, booking number, booking date
- Xem chi tiết booking - View booking details
- Xem bill dịch vụ - View service bill
- Xử lý yêu cầu: - Process requests:
- Hủy booking - Cancel booking
- Checkout - Checkout
--- ---
## 2.2.2 Check-in ## 2.2.2 Check-in
**Vai trò:** Staff, Manager **Roles:** Staff, Manager
**Quy trình check-in:** **Check-in Process:**
- Khách xuất trình Booking Number - Guest presents Booking Number
- Nhân viên kiểm tra thông tin booking - Staff verifies booking information
- Nhập thông tin từng khách trong phòng - Enter information for each guest in the room
- Gán số phòng thực tế - Assign actual room number
- Thu thêm phí nếu có trẻ em hoặc extra person - Collect additional fees if there are children or extra persons
--- ---
## 2.2.3 Use Services (Khách đăng ký sử dụng dịch vụ) ## 2.2.3 Use Services (Guest Service Registration)
**Vai trò:** Staff **Roles:** Staff
**Chức năng:** **Functions:**
- Đăng ký dịch vụ cho khách dựa trên Room Number - Register services for guests based on Room Number
- In ticket nếu có yêu cầu - Print ticket if requested
--- ---
## 2.2.4 Check-out ## 2.2.4 Check-out
**Vai trò:** Staff, Manager **Roles:** Staff, Manager
**Chức năng:** **Functions:**
- Tính toán: - Calculate:
- Phí phòng - Room fee
- Phí dịch vụ - Service fee
- Phụ phí khác - Other surcharges
- Tạo hóa đơn (Invoice) - Create invoice
- Khấu trừ tiền đã đặt cọc (booking value) - Deduct deposit amount (booking value)
- Khách thanh toán phần còn lại - Guest pays remaining amount
--- ---
# 2.3 Report Module (Báo cáo) # 2.3 Report Module
**Vai trò:** Manager, Admin **Roles:** Manager, Admin
**Chức năng:** **Functions:**
- Nhập khoảng thời gian From → To - Enter time range From → To
- Liệt kê toàn bộ booking trong khoảng thời gian - List all bookings within the time range
- Tính tổng doanh thu - Calculate total revenue
- Xuất báo cáo: - Export reports:
- Excel - Excel
- PDF - PDF
**Nội dung báo cáo:** **Report Content:**
- Booking ID - Booking ID
- Customer Name - Customer Name
- Room - Room
@@ -150,58 +150,58 @@ Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo
--- ---
# 2.4 System Administration Module (Quản trị hệ thống) # 2.4 System Administration Module
--- ---
## 2.4.1 User Management ## 2.4.1 User Management
**Vai trò:** Admin **Roles:** Admin
**Chức năng:** **Functions:**
- Add user - Add user
- Edit user - Edit user
- Delete user - Delete user
- View user detail - View user detail
- List tất cả user - List all users
- n role (Admin, Manager, Staff) - Assign role (Admin, Manager, Staff)
--- ---
## 2.4.2 Security ## 2.4.2 Security
**Chức năng bảo mật của hệ thống:** **System Security Functions:**
### Roles được định nghĩa: ### Defined Roles:
| Role | Quyền | | Role | Permissions |
|------|-------| |------|-------|
| **Customer** | Không cần login | | **Customer** | No login required |
| **Staff (Sale)** | Truy cập Operation Module | | **Staff (Sale)** | Access Operation Module |
| **Manager** | Truy cập Setup Module | | **Manager** | Access Setup Module |
| **Admin** | Toàn quyền, bao gồm User & Security | | **Admin** | Full access, including User & Security |
### Quy tắc bảo mật: ### Security Rules:
- Nhân viên & admin bắt buộc phải login - Staff & admin must login
- Quyền thao tác phụ thuộc vào role - Operation permissions depend on role
- Session timeout sau 30 phút không hoạt động - Session timeout after 30 minutes of inactivity
--- ---
# 3. Tóm tắt theo góc nhìn Admin # 3. Summary from Admin Perspective
| Module | Quyền Admin | Nội dung | | Module | Admin Permissions | Content |
|--------|-------------|----------| |--------|-------------|----------|
| Room Setup | Full | CRUD phòng | | Room Setup | Full | CRUD rooms |
| Service Setup | Full | CRUD dịch vụ | | Service Setup | Full | CRUD services |
| Promotion Setup | Full | CRUD khuyến mãi | | Promotion Setup | Full | CRUD promotions |
| Booking Management | Full | Xem, duyệt, hủy booking | | Booking Management | Full | View, approve, cancel bookings |
| Check-in / Check-out | Full | Quản lý vận hành | | Check-in / Check-out | Full | Operations management |
| Service Usage | Full | Ghi log dịch vụ | | Service Usage | Full | Service logging |
| Reports | Full | Thống kê, xuất file | | Reports | Full | Statistics, export files |
| User Management | Full | Quản lý nhân viên | | User Management | Full | Staff management |
| Security | Full | Role, phân quyền | | Security | Full | Roles, permissions |
--- ---
# 4. Kết luận # 4. Conclusion
Phân tích trên giúp xác định đầy đủ các chức năng cần triển khai cho **Admin / Manager / Staff** trong hệ thống quản lý khách sạn. The above analysis helps identify all the functions that need to be implemented for **Admin / Manager / Staff** in the hotel management system.
Tài liệu có thể được sử dụng để xây dựng database, API, UI/UX, và phân quyền hệ thống. This document can be used to build the database, API, UI/UX, and system permissions.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -222,7 +222,7 @@ const resetPassword = async (req, res, next) => {
} }
if ( if (
error.message.includes('must be different') || error.message.includes('must be different') ||
error.message.includes('Mật khẩu mới') error.message.includes('New password')
) { ) {
return res.status(400).json({ return res.status(400).json({
status: 'error', status: 'error',

View File

@@ -19,7 +19,7 @@ const addFavorite = async (req, res, next) => {
if (!room) { if (!room) {
return res.status(404).json({ return res.status(404).json({
status: 'error', status: 'error',
message: 'Không tìm thấy phòng', message: 'Room not found',
}); });
} }
@@ -34,7 +34,7 @@ const addFavorite = async (req, res, next) => {
if (existingFavorite) { if (existingFavorite) {
return res.status(400).json({ return res.status(400).json({
status: 'error', status: 'error',
message: 'Phòng đã có trong danh sách yêu thích', message: 'Room already in favorites list',
}); });
} }
@@ -46,7 +46,7 @@ const addFavorite = async (req, res, next) => {
res.status(201).json({ res.status(201).json({
status: 'success', status: 'success',
message: 'Đã thêm vào danh sách yêu thích', message: 'Added to favorites list',
data: { data: {
favorite, favorite,
}, },
@@ -75,7 +75,7 @@ const removeFavorite = async (req, res, next) => {
if (!favorite) { if (!favorite) {
return res.status(404).json({ return res.status(404).json({
status: 'error', status: 'error',
message: 'Không tìm thấy phòng trong danh sách yêu thích', message: 'Room not found in favorites list',
}); });
} }
@@ -83,7 +83,7 @@ const removeFavorite = async (req, res, next) => {
res.status(200).json({ res.status(200).json({
status: 'success', status: 'success',
message: 'Đã xóa khỏi danh sách yêu thích', message: 'Removed from favorites list',
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

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