update
This commit is contained in:
@@ -1,138 +1,138 @@
|
||||
# Routing Configuration - Hướng dẫn Test
|
||||
# Routing Configuration - Testing Guide
|
||||
|
||||
## ✅ Đã hoàn thành Chức năng 2
|
||||
## ✅ Function 2 Completed
|
||||
|
||||
### Components đã tạo:
|
||||
### Components Created:
|
||||
|
||||
1. **ProtectedRoute** - `src/components/auth/ProtectedRoute.tsx`
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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 và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 và 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, đã có 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -191,7 +191,7 @@ const DashboardPage: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
||||
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -227,7 +227,7 @@ const DashboardPage: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-center py-8">Không có dữ liệu</p>
|
||||
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 có 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 có dữ liệu</p>
|
||||
<p className="text-gray-500 text-center py-8">No data available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 lý khuyến mãi</h1>
|
||||
<p className="text-gray-500 mt-1">Quản lý mã giảm giá và chương trình khuyến mãi</p>
|
||||
<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">
|
||||
Mã 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">
|
||||
Mã 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">
|
||||
Mô 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 lý phòng</h1>
|
||||
<p className="text-gray-500 mt-1">Quản lý 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>
|
||||
|
||||
@@ -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">
|
||||
Mô 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">
|
||||
Mô 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 bạn đã đượ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ử lý...
|
||||
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ỉ có hiệu lực trong 1 giờ</li>
|
||||
<li>Mật khẩu được mã 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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
Mã đặ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ọ và 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>
|
||||
|
||||
@@ -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 lý và 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>
|
||||
*/}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
Mã đặ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ọ và 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 mã 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"
|
||||
>
|
||||
Mã 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 mã đặt phòng
|
||||
vào nội dung chuyển khoản để chúng tôi
|
||||
có 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>
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
|
||||
@@ -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 bạn đã đượ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">
|
||||
Mã 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 mã 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 mã 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 mã QR
|
||||
Download QR Code
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 có 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 nào vào danh sách
|
||||
yêu thích. Hãy khám phá và 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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -218,7 +218,7 @@ const PaymentResultPage: React.FC = () => {
|
||||
font-medium"
|
||||
>
|
||||
<Home className="w-5 h-5" />
|
||||
Về trang chủ
|
||||
Go to home
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
Mô 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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 vào header
|
||||
// Request interceptor - Add token to header
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// Normalize request URL: if a request path accidentally begins
|
||||
|
||||
@@ -45,12 +45,12 @@ export interface ResetPasswordData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth Service - Xử lý các API calls liên quan
|
||||
* đế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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user