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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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