Files

461 lines
28 KiB
Markdown

# Kiến Trúc Hệ Thống: Tính Năng Lập Kế Hoạch Du Lịch (Travel Planning Feature)
Tài liệu này mô tả kiến trúc tổng thể, mô hình dữ liệu và các giải pháp kỹ thuật cho tính năng lập kế hoạch du lịch đa người dùng thuộc nền tảng **Travel**.
---
## 1. Cấu Trúc Thư Mục (Directory Structure)
```text
/home/locpham/travelplanning/
├── admin.guard.ts # Middleware kiểm tra quyền admin
├── App.tsx # Component chính của ứng dụng frontend
├── ARCHITECTURE.md # Tài liệu kiến trúc hệ thống
├── CreateTourModal.tsx # Modal cho tạo tour
├── dist/ # Thư mục build output
├── .env # Biến môi trường
├── ExpenseManager.tsx # Component quản lý chi phí
├── ExploreMap.tsx # Component bản đồ khám phá
├── index.tsx # Điểm vào ứng dụng frontend
├── ItineraryTimeline.tsx # Component timeline lịch trình
├── jwt-auth.guard.ts # Middleware xác thực JWT
├── jwt.strategy.ts # Strategy xử lý JWT
├── LandingPage.tsx # Trang landing page
├── LoginModal.tsx # Modal đăng nhập
├── main.ts # Điểm vào ứng dụng backend
├── migrations/ # Migration cũ của ORM khác
│ ├── 20260613030135_init_travel_planning_schema/
│ │ └── migration.sql
│ ├── 20260613091754_add_is_admin_field/
│ │ └── migration.sql
│ ├── 20260613112158/
│ │ └── migration.sql
│ ├── 20260613114646/
│ │ └── migration.sql
│ └── migration_lock.toml
├── nest-cli.json # Cấu hình NestJS CLI
├── node_modules/ # Thư mục dependencies
├── package.json # Cấu hình project Node.js
├── package-lock.json # Lockfile dependencies
├── postcss.config.js # Cấu hình PostCSS
├── prisma/ # Thư mục Prisma ORM
│ ├── migrations/
│ │ └── 0_init/
│ │ └── migration.sql
│ ├── schema.prisma # Schema dữ liệu chính
│ └── prisma.service.ts # Service Prisma
├── rbac.middleware.ts # Middleware phân quyền RBAC
├── README.md # Tài liệu hướng dẫn dự án
├── schema.sql # File SQL schema
├── seed.ts # File seed dữ liệu mẫu
├── SignupPage.tsx # Trang đăng ký tài khoản
├── tailwind.config.ts # Cấu hình TailwindCSS
├── TourDetailPage.tsx # Trang chi tiết tour
├── tsconfig.build.json # Cấu hình TypeScript build
├── tsconfig.json # Cấu hình TypeScript
├── UITourDesign.md # Tài liệu thiết kế UI
├── useTourStore.ts # State management Zustand cho Tour
├── vite.config.ts # Cấu hình Vite bundler
├── .gitignore # Git ignore rules
├── .vscode/
│ └── settings.json # Cấu hình VS Code
└── index.css # Styles CSS toàn cục
```
### Giải thích Cấu trúc Thư mục
**Frontend Layer:**
- Các file `.tsx` (React components) xử lý giao diện người dùng: `LandingPage`, `TourDetailPage`, `CreateTourModal`, `ExpenseManager`, `ItineraryTimeline`, `ExploreMap`, `UserManagementModal`, `SignupPage`, `LoginModal`, `AddLocationModal`
- Cấu hình TailwindCSS (`tailwind.config.ts`, `index.css`) cho styling
- `useTourStore.ts` - State management với Zustand
**Backend Layer:**
- `main.ts` - Entry point ứng dụng NestJS/Express
- `expense-engine.service.ts` - Service tính toán và chia chi phí
- `prisma.service.ts` - Service kết nối cơ sở dữ liệu
- Guards & Middleware: `admin.guard.ts`, `jwt-auth.guard.ts`, `rbac.middleware.ts`
- `jwt.strategy.ts` - Xác thực người dùng qua JWT
**Database Layer:**
- `prisma/schema.prisma` - Định nghĩa schema dữ liệu
- `prisma/migrations/` - Lịch sử thay đổi schema của Prisma
- `migrations/` - Migration cũ (không dùng Prisma)
- `seed.ts` - Chèn dữ liệu mẫu vào cơ sở dữ liệu
**Configuration:**
- `tsconfig.json`, `tsconfig.build.json` - Cấu hình TypeScript
- `vite.config.ts` - Config bundler Vite
- `nest-cli.json` - Cấu hình NestJS CLI
- `postcss.config.js` - Cấu hình PostCSS cho TailwindCSS
- `.env` - Biến môi trường ứng dụng
**Documentation:**
- `ARCHITECTURE.md` - Tài liệu kiến trúc hệ thống
- `UITourDesign.md` - Tài liệu thiết kế giao diện người dùng
- `README.md` - Hướng dẫn cài đặt và chạy dự án
**Tools & IDE:**
- `.vscode/settings.json` - Cấu hình VS Code workspace
- `.gitignore` - Quy tắc bỏ qua file Git
### Công nghệ trong Dự án
```json
{
"frontend": {
"framework": "React + Vite",
"styling": "TailwindCSS + PostCSS",
"state_management": "Zustand",
"map_engine": "Yêu cầu tích hợp Leaflet/Google Maps API"
},
"backend": {
"framework": "NestJS/Express với TypeScript",
"auth": "JWT Strategy + Guards",
"orm": "Prisma",
"database": "PostgreSQL + PostGIS"
},
"build_tools": {
"bundler": "Vite",
"compiler": "TypeScript"
}
}
```
---
Hệ thống được thiết kế theo mô hình **Client-Server** kết hợp với kiến trúc **Modular Monolith** hoặc **Microservices** (tùy quy mô triển khai), chia làm 3 tầng chính:
* **Frontend (Presentation Layer):** Đảm nhiệm việc tương tác trực tiếp với người dùng, hiển thị bản đồ trực quan, quản lý luồng tạo tour/chặng và xử lý tải ảnh/hiển thị album.
* **Backend (Business Logic Layer):** Xử lý xác thực bảo mật, tính toán chi phí (quy tắc phân chia tiền), định tuyến đường đi ngắn nhất, lập lịch kiểm tra task (Cron job/Event-driven) và kiểm tra quyền truy cập (RBAC).
* **Database & Storage (Data Layer):** Lưu trữ dữ liệu quan hệ (PostgreSQL với extension PostGIS để xử lý tọa độ địa lý) và lưu trữ tệp tin hình ảnh riêng biệt (Object Storage như AWS S3 hoặc MinIO).
---
## 2. Công Nghệ Đề Xuất (Tech Stack)
| Thành phần | Công nghệ đề xuất | Lý do lựa chọn |
| :--- | :--- | :--- |
| **Frontend** | React / Next.js + TailwindCSS | Tối ưu cho ứng dụng Single Page (SPA), quản lý trạng thái tốt, hỗ trợ SSR cho SEO nếu cần. |
| **State Management** | Zustand / Redux Toolkit | Quản lý state phức tạp của Tour, Chặng (Leg) và Chi phí. |
| **Map Engine** | Mapbox GL JS / Google Maps API / Leaflet | Hỗ trợ hiển thị bản đồ trực quan, marker kéo thả và tính toán định tuyến (Routing). |
| **Backend** | Node.js (NestJS hoặc Express) | Xử lý bất đồng bộ tốt, phù hợp hệ thống nhiều I/O và tích hợp mDNS/IoT nếu mở rộng sau này. |
| **Database** | PostgreSQL + **PostGIS** | Hệ quản trị cơ sở dữ liệu mạnh mẽ, PostGIS giúp tính toán khoảng cách và tọa độ địa lý chính xác. |
| **File Storage** | AWS S3 / MinIO | Lưu trữ ảnh tách biệt hoàn toàn khỏi Database để tối ưu hiệu năng. |
---
## 3. Mô Hình Dữ Liệu (Database Schema)
Dưới đây là các thực thể cốt lõi phục vụ tính năng:
### 3.1. User (Người dùng)
Lưu trữ thông tin định danh và trạng thái quản trị. Toàn bộ ID sử dụng định dạng **UUID**.
* `User`: `id`, `email`, `passwordHash`, `name`, `avatar`, `isAdmin`, `isBlocked`, `createdAt`.
### 3.2. Tour & Phân quyền (RBAC)
Hệ thống sử dụng bảng trung gian để quản lý thành viên cho từng chuyến đi.
* `Tour`: `id`, `title`, `startDate`, `endDate`, `totalCost` (Decimal), `createdById` (FK -> User).
* `TourParticipant`: Quản lý vai trò thành viên trong Tour qua enum `ParticipantRole`:
* `OWNER`: Toàn quyền quản lý.
* `MANAGER`: Quản lý nội dung và thành viên.
* `MEMBER`: Thành viên chính thức (Xem được chi phí).
* `MEMBER_NO_FINANCE`: Thành viên không được xem thông tin tài chính.
* `VIEWER_ONLY`: Chỉ xem lộ trình và ảnh.
### 3.3. Lộ trình (Leg & Location)
Một Tour được chia thành nhiều chặng di chuyển (Leg), mỗi chặng chứa danh sách các điểm đến (Location).
* `Leg`: `id`, `tourId` (FK), `sequence` (Thứ tự chặng), `note`.
* `Location`: Tích hợp tính năng theo dõi tiến độ (Thay thế khái niệm `Task` cũ).
* Tọa độ: `latitude`, `longitude`.
* Thời gian: `plannedStart`, `plannedEnd`, `actualStart`, `actualEnd`.
* Trạng thái (`LocationStatus`): `PENDING` (Chờ), `COMPLETED` (Hoàn thành).
* Loại địa điểm (`LocationType`): `MOVE`, `VISIT`, `REST`, `EAT`.
### 3.4. Chi phí (Expense)
Quản lý tài chính cho từng chặng hoặc gắn trực tiếp vào một địa điểm cụ thể.
* `Expense`: `id` (UUID, PK), `leg_id` (UUID, FK), `location_id` (UUID, FK, Nullable), `category` (Enum: `ACCOMMODATION`, `FOOD`, `TRANSPORT`, `TICKET`, `OTHER`), `amount` (Decimal), `description` (Text).
### 3.5. Album Ảnh (Photo)
Lưu trữ tài nguyên đa phương tiện gắn với bối cảnh chuyến đi.
* `Photo`: `id`, `tourId` (FK), `locationId` (FK, nullable), `uploaderId` (FK), `imageUrl`, `privacy`.
* Cấp độ bảo mật (`PrivacyLevel`): `PUBLIC`, `TOUR_ONLY`, `PRIVATE`.
---
## 4. Giải Pháp Kiến Trúc Cho Các Tính Năng Core
### 4.1. Thuật Toán Tìm Đường Ngắn Nhất (Shortest Path Routing)
* **Cơ chế:** Khi người dùng thêm các địa điểm vào một chặng, Frontend sẽ gửi danh sách tọa độ hình học (Lat, Lng) về Backend.
* **Xử lý:** Backend tích hợp với **Mapbox Direction API** hoặc chạy một instance **OSRM (Open Source Routing Machine)** cục bộ để giải bài toán Người đi giao hàng (TSP - Travelling Salesman Problem). Hệ thống sẽ sắp xếp lại thứ tự `sequence_in_leg` của các `places` để tối ưu tổng quãng đường hoặc thời gian di chuyển, sau đó trả về mảng tọa độ đường đi (Polyline) để Frontend vẽ lên bản đồ.
### 4.2. Engine Tính Toán Chi Phí & Chia Tiền (Expense Splitting Engine)
* **Công thức tự động:** * Tổng chi phí Tour ($T$) = $\sum \text{Expenses}$ của tất cả các chặng.
* Hệ thống cho phép cấu hình số lượng Người lớn ($N_{adult}$) và Trẻ em ($N_{child}$), kèm phần trăm giảm giá của trẻ em ($P_{child\_discount}$).
* Gọi $X$ là chi phí của 1 người lớn:
$$T = N_{adult} \times X + N_{child} \times \left( X \times (1 - P_{child\_discount}) \right)$$
* Từ đó Backend tự động giải phương trình để tìm ra chi phí trung bình của người lớn ($X$) và trẻ em, hiển thị báo cáo chi tiết ở cuối mỗi chặng và kết thúc tour.
### 4.3. Theo Dõi Tiến Độ & Tính Toán Độ Lệch Thời Gian (Time Variance Tracking)
* **Tự động (Auto-trigger):** Một Worker/Cron-job chạy ngầm mỗi phút quét các `tasks` có cài đặt `AUTO_BY_TIME`. Nếu `current_time >= planned_timestamp` và chưa hoàn thành, hệ thống tự động chuyển `is_completed = true` và ghi nhận `completed_at = planned_timestamp`.
* **Thủ công (Manual-trigger):** Khi user bấm tích chọn hoàn thành, hệ thống ghi nhận ngay `completed_at = current_time`.
* **Báo cáo thời gian (Time Summary):** Backend tính toán hiệu số:
$$\Delta t = \text{completed\_at} - \text{planned\_timestamp}$$
Đồng thời group dữ liệu theo loại hoạt động để xuất ra tổng thời gian: *Di chuyển (dựa trên khoảng cách bản đồ), Tham quan (thời gian ở các địa điểm giải trí), Nghỉ ngơi (thời gian ở chỗ nghỉ).*
### 4.4. Bảo Mật & Tách Biệt Album Ảnh (Photo Album Isolation)
* **Tách biệt logic:** Cơ sở dữ liệu ảnh liên kết qua mã `tour_id``place_id` nhưng API endpoint kiểm tra quyền (`/api/v1/tours/:id/photos`) hoạt động độc lập với endpoint kế hoạch (`/api/v1/tours/:id/plans`).
* **Bảo mật phân quyền (Privacy Middleware):** Trước khi trả về dữ liệu hình ảnh hoặc kế hoạch, hệ thống sẽ chạy qua một tầng Middleware để check quyền trong bảng `tour_members`:
```javascript
// Giả lập Logic kiểm tra quyền truy cập thông tin tài chính/kế hoạch
if (member.role === 'MEMBER_PHOTO_ONLY' && request.path.includes('/plans')) {
throw new ForbiddenException("Bạn không có quyền xem kế hoạch và chi phí của tour này.");
}
```
---
## 5. Thiết Kế Luồng Đi (Workflow Diagrams)
### Luồng tạo chặng và tính toán đường đi (Frontend <=> Backend)
1. **User** tìm kiếm/chọn địa điểm trên bản đồ -> **Frontend** cập nhật state cục bộ.
2. **User** nhấn "Tối ưu đường đi" -> **Frontend** gửi danh sách `coords[]` lên `/api/v1/routing/optimize`.
3. **Backend** gọi Mapbox/OSRM API -> Sắp xếp lại thứ tự tối ưu -> Lưu vào DB -> Trả về JSON chứa `optimized_places` và `polyline_geometry`.
4. **Frontend** render lại danh sách địa điểm theo thứ tự mới và vẽ nét đứt/đường đi trên Map.
### Luồng chia sẻ hình ảnh và kiểm tra bảo mật
1. **User** tải ảnh lên tại một địa điểm trong Tour.
2. **Frontend** gửi file đến dịch vụ lưu trữ (S3) nhận về `image_url`, sau đó lưu thông tin vào DB kèm metadata (ngày giờ, địa điểm).
3. Khi một **User khác** truy cập link Tour:
* Hệ thống check `tour_members`.
* Nếu thuộc diện *External Viewer* hoặc *Photo Only*: Ẩn hoàn toàn tab "Kế hoạch chặng hành trình" và "Tổng số tiền", chỉ hiển thị giao diện Grid hình ảnh (`album`).
# 6. THIẾT KẾ GIAO DIỆN NGƯỜI DÙNG (TOUR DASHBOARD & MAP ITINERARY)
Đây là màn hình Hub chính sau khi người dùng truy cập vào một Tour cụ thể. Giao diện được thiết kế để tối ưu hóa trải nghiệm trên thiết bị di động (Mobile-first) và kiểm soát hiển thị nội dung động dựa trên vai trò của thành viên (`ParticipantRole`).
## 6.1. Thành phần Giao diện chính
* **Top Banner (Khu vực tiêu đề):**
* **Cover Image:** Ảnh bìa của tour (lấy từ ảnh đầu tiên trong Album hoặc ảnh phong cảnh mặc định).
* **Thông tin Tour:** Hiển thị tên Tour và khoảng thời gian diễn ra (`startDate` -> `endDate`).
* **Member Avatars:** Danh sách avatar các thành viên tham gia (`TourParticipant`). Cho phép nhấn vào để xem chi tiết hoặc mời thêm người (nếu có quyền).
* **Financial Quick-View Widget (Khối tài chính nhanh):**
* Hiển thị `totalCost` hiện tại của toàn bộ chuyến đi.
* **Logic Ẩn/Hiện động (RBAC):**
* *Hiển thị:* Với các role `OWNER`, `MANAGER`, `MEMBER`.
* *Ẩn:* Với các role `MEMBER_NO_FINANCE` hoặc `VIEWER_ONLY`. Thay thế khối này bằng một câu quote truyền cảm hứng du lịch (Ví dụ: *"Đừng nghe họ nói, hãy tự mình đi xem"*).
* **Bottom Navigation Bar hoặc Tab View:** Hệ thống chia làm 4 tab chính để tránh nhồi nhét dữ liệu:
1. **Lộ trình (Itinerary):** Hiển thị danh sách `Leg` & `Location` tích hợp Bản đồ tương tác. *Đây là tab mặc định.*
2. **Chi phí (Finance):** Quản lý các `Expense`. Chỉ hiển thị cho các role có quyền truy cập tài chính.
3. **Album ảnh (Gallery):** Hiển thị lưới hình ảnh `Photo` được chia sẻ trong chuyến đi.
4. **Thành viên & Cài đặt (Settings):** Khu vực dành riêng cho `OWNER` hoặc `MANAGER` để quản lý thành viên, phân quyền và cấu hình tour.
---
## 6.2. Sơ đồ bố cục tổng thể (Wireframe mô phỏng)
```text
+---------------------------------------+
| [ < ] Tên Chuyến Đi [ . . ] | <-- Header bar
+---------------------------------------+
| |
| IMAGE COVER PHOTO | <-- Top Banner
| |
| [Avatar][Avatar][Avatar] +3 |
+---------------------------------------+
| |
| { TỔNG CHI PHÍ: 2.500.000 VND } | <-- Finance Widget (Hoặc Quote)
| |
+---------------------------------------+
| [ Lộ trình ] [ Chi phí ] [ Ảnh ] [S] | <-- Tab Navigation
+---------------------------------------+
| |
| TAB LỘ TRÌNH (BẢN ĐỒ + DANH SÁCH) |
| |
+---------------------------------------+
```
---
## 6.3. Tab Lộ Trình: Bản Đồ Tương Tác & Cấu Trúc Chặng (Map-Driven Timeline)
Để tối ưu hóa UX trên di động, Tab Lộ trình được chia thành 2 chế độ hiển thị bằng một nút bấm chuyển đổi nhanh (Toggle Switch): **Chế độ Bản đồ (Map View)** và **Chế độ Danh sách (Timeline View)**. Khi chỉnh sửa/khởi tạo, Chế độ Bản đồ sẽ làm chủ đạo.
### A. Luồng Khởi Tạo & Định Vị Lộ Trình (Google Maps Style)
Đối với các tài khoản có quyền chỉnh sửa (`OWNER`, `MANAGER`), luồng thiết lập lộ trình không gian được thực hiện qua các bước tương tác trực quan:
#### Giai đoạn 1: Đóng khung điểm Đầu - Cuối
1. **Khai báo Tour:** User nhập thông tin cơ bản ở bảng cấu hình.
2. **Xác định Điểm Đầu:** Tìm kiếm trên ô Search của bản đồ hoặc rê chuột/di tâm màn hình đến vị trí mong muốn ➔ **Click chuột phải** (hoặc **Nhấn giữ** nếu dùng mobile) ➔ Chọn **"Bắt đầu từ đây"**.
* *Phản hồi UI:* Xuất hiện Marker màu Xanh lá `[S]`. Tạo bản ghi `Location` đầu tiên.
3. **Xác định Điểm Cuối:** Di chuyển đến điểm đích ➔ **Click chuột phải / Nhấn giữ** ➔ Chọn **"Kết thúc ở đây"**.
* *Phản hồi UI:* Xuất hiện Marker màu Đỏ `[E]`.
4. **Khai báo số lượng Chặng:** Một ô thông báo nổi (Pop-up) yêu cầu nhập: *"Chuyến đi này chia làm bao nhiêu chặng?"*. Khi nhập số $N$, hệ thống tự động sinh ra $N$ bản ghi `Leg` với `sequence` từ $1$ đến $N$ ở trạng thái chờ rỗng.
#### Giai đoạn 2: Ghim điểm tham quan vào Chặng (Map-to-Leg)
1. **Chấm điểm:** Người dùng gõ tìm kiếm tọa độ/địa danh hoặc click trực tiếp lên bản đồ.
2. **Phân bổ vào chặng:** Click chuột phải / Nhấn giữ vào điểm vừa chọn ➔ Menu ngữ cảnh hiện ra danh sách các chặng đã khai báo ở Giai đoạn 1 ➔ Chọn số thứ tự chặng (Ví dụ: `Thêm vào Chặng 1`).
3. **Tự động sắp xếp & Vẽ tuyến:** * Dữ liệu địa điểm lập tức được xếp vào mảng con của `Leg` có `sequence: 1`.
* Hệ thống tự động sắp xếp vị trí hiển thị theo thứ tự thêm vào (có thể kéo thả đổi thứ tự thủ công sau).
* Bản đồ tự động gọi API Routing để vẽ đường nối liền mạch: **`[S]` ➔ Các điểm Chặng 1 ➔ Các điểm Chặng 2 ➔ `[E]**`.
#### Giai đoạn 3: Tối ưu Lịch trình & Thời gian (Data Refinement)
Sau khi "bộ khung" không gian trên bản đồ đã hoàn tất, người dùng chuyển sang giao diện danh sách để cấu hình chi tiết tuyến tính thời gian:
* Bổ sung ngày giờ kế hoạch (`plannedStart`, `plannedEnd`) cho từng `Location`.
* Chọn loại địa điểm (`LocationType`: `MOVE`, `VISIT`, `REST`, `EAT`) để hệ thống đồng bộ Icon hiển thị.
* Thêm ghi chú (`note`) tổng quan cho từng `Leg`.
```text
WIREFRAME CHẾ ĐỘ BẢN ĐỒ (MAP VIEW MODE):
+---------------------------------------+
| [ Tìm kiếm địa điểm trên bản đồ... ] |
+---------------------------------------+
| |
| [S] (Điểm xuất phát) |
| \ |
| \____ [Icon: EAT] Nhà hàng A |
| \ |
| \____ [E] (Điểm cuối) |
| |
| CONTEXT MENU KHI NHẤN GIỮ/CHUỘT PHẢI: |
| +---------------------------------+ |
| | o Bắt đầu từ đây | |
| | o Kết thúc ở đây | |
| | o Thêm vào lộ trình > [Chặng 1]| |
| +------------------------[Chặng 2]| |
+---------------------------------------+
```
---
### B. Chế Độ Hiển Thị Timeline Đứng (Dành cho việc Theo Dõi Tiến Độ)
Sau khi lưu lộ trình, khi đi du lịch thực tế, người dùng sẽ chủ yếu xem ở giao diện **Timeline đứng** để kiểm tra tiến độ:
#### Tầng 1 - Chọn Chặng (Leg Picker)
* **UI Component:** Sử dụng **Horizontal Tab Scroller** đặt ở phía trên cùng của tab.
* **Chức năng:** Người dùng có thể chọn nhanh các Chặng (ví dụ: Chặng 1, Chặng 2, Chặng 3...) để lọc dữ liệu hiển thị, tránh kéo màn hình quá dài.
* **Logic:** Nhấn vào `Leg` nào, danh sách các `Location` được xếp theo đúng `sequence` của chặng đó mới hiển thị phía dưới.
#### Tầng 2 - Danh sách Địa điểm (Location Cards)
Mỗi địa điểm là một thẻ (card) nằm trên đường timeline đứng, bao gồm các đặc điểm nhận diện:
* **Icon nhận diện theo loại (LocationType):**
* `MOVE`: Icon Phương tiện (Xe bus, Máy bay).
* `VISIT`: Icon Tham quan (Kính thiên văn, Lá cờ).
* `REST`: Icon Nghỉ ngơi (Giường ngủ).
* `EAT`: Icon Ẩm thực (Dao dĩa).
* **Trạng thái & Tiến độ (LocationStatus):**
* **Trạng thái PENDING:** * *UI:* Card có viền nét đứt (dashed), màu xám nhạt. Hiển thị giờ dự kiến (`plannedStart`).
* *Hành động:* Nút "Bắt đầu" (Chỉ hiển thị cho vai trò `OWNER` hoặc `MANAGER`).
* **Trạng thái Đang diễn ra (Active):**
* *UI:* Card được làm sáng (highlight), hiển thị giờ bắt đầu thực tế (`actualStart`).
* *Hành động:* Nút "Hoàn thành".
* **Trạng thái COMPLETED:**
* *UI:* Card có dấu tích xanh, màu nền chuyển sang tone dịu (ví dụ: xanh lá nhạt), hiển thị giờ kết thúc thực tế (`actualEnd`).
---
## 6.4. Tab Chi Phí (Expense Management)
Cấu trúc dữ liệu của hệ thống cho phép gắn `Expense` (Chi phí) vào một `Leg` (Chặng) hoặc gắn trực tiếp vào một `Location` (Địa điểm) cụ thể.
### A. Phân tích trực quan
* **Biểu đồ tròn (Donut Chart):** Hiển thị ở đầu tab để phân tích chi phí theo danh mục (`category`: FOOD, TRANSPORT, ACCOMMODATION, TICKET...). Giúp người dùng nhìn trực quan ngân sách đang đổ vào đâu nhiều nhất.
### B. Danh sách hiển thị (List View)
* **Hiển thị dòng tiền:** Liệt kê các khoản chi tiêu kèm mô tả (`description`) và số tiền chính xác (`amount`).
* **Nhãn bối cảnh (Context Tag):** Mỗi dòng chi phí có một nhãn nhỏ đi kèm giúp người dùng xác định bối cảnh chi tiêu:
* Nếu `location_id` có dữ liệu: Hiển thị nhãn **“Tại: [Tên địa điểm]”**.
* Nếu chỉ có `leg_id`: Hiển thị nhãn **“Thuộc: [Tên Chặng]”**.
### C. Nút thêm nhanh (Floating Action Button)
* **Nút +:** Nằm ở góc dưới màn hình để thêm mới chi phí.
* **Logic thông minh:** Khi nhấn vào, hệ thống sẽ tự động bắt bối cảnh (Context-aware). Nếu user đang đứng xem ở `Chặng 2`, form thêm mới sẽ tự động chọn sẵn `leg_id` của Chặng 2 để giảm thiểu các bước nhập liệu thủ công.
---
## 6.5. Tab Album Ảnh (Contextual Photo Gallery)
Bảng `Photo` có trường `locationId` (nullable) và `privacy`. Giao diện cần dạng hóa việc lọc để tối ưu trải nghiệm xem lại kỷ niệm.
### A. Chế độ xem thông minh (Smart View)
Cung cấp 2 chế độ lọc thông qua nút gạt (Toggle Switch):
* **Xem theo dòng thời gian (Timeline Photo):** Ảnh được gom nhóm (`group by`) theo từng `Location`. Đi tới địa điểm nào trên bản đồ/timeline, ảnh chụp tại đó sẽ hiện ngay bên dưới địa điểm đó.
* **Xem dạng lưới (Grid View):** Hiển thị dạng lưới ảnh tiêu chuẩn (3x3 hoặc 4x4), tương tự các ứng dụng quản lý ảnh gốc trên điện thoại.
### B. Bộ lọc Quyền riêng tư (PrivacyLevel) & Chỉ báo UI
* Các ảnh có tag `PRIVATE` chỉ hiển thị duy nhất với chính người tải lên (`uploaderId`).
* Ảnh `TOUR_ONLY` hiển thị cho mọi thành viên có trong Tour.
* **Chỉ báo UI (Privacy Indicators):** Trên góc mỗi ảnh hiển thị một icon nhỏ (Hình ổ khóa cho `PRIVATE`, Hình con mắt cho `TOUR_ONLY` hoặc `PUBLIC`) giúp người dùng dễ dàng kiểm soát trạng thái chia sẻ.
---
## 7. Tối ưu hóa State Management & Hiệu năng Frontend (Crucial)
Vì cấu trúc dữ liệu của dự án sử dụng rất nhiều ID nối nhau (Foreign Keys), để frontend chạy mượt và không bị giật lag khi render, các kỹ thuật sau được ưu tiên áp dụng:
### 7.1. Chuẩn hóa dữ liệu ở Frontend (Data Normalization)
Khi gọi API lấy chi tiết Tour, cấu trúc cây sẽ được biến đổi thành cấu trúc phẳng (Flatten State) bằng cách lưu dữ liệu dưới dạng Object Key-Value (Dùng ID làm Key).
```javascript
// Ví dụ cấu trúc State tối ưu ở Frontend
const locationsState = {
"uuid-location-1": { latitude: 16.0, longitude: 108.0, status: "PENDING", ... },
"uuid-location-2": { ... }
};
```
Khi một Location thay đổi trạng thái từ `PENDING` sang `COMPLETED`, hệ thống chỉ cần cập nhật đúng object đó thông qua ID, giúp các thành phần khác của UI không bị re-render vô ích, tối ưu hóa hiệu năng render của React.
### 7.2. Xử lý Real-time và Optimistic Updates (Cập nhật lạc quan)
* **Đồng bộ Real-time:** Sử dụng **Websocket** để đồng bộ trạng thái giữa các thành viên trong Tour. Khi một người bấm "Hoàn thành" địa điểm hoặc "Thêm chi phí", những người khác sẽ nhận được cập nhật ngay lập tức.
* **Optimistic Update:** Khi một thành viên thêm một `Expense`, Frontend sẽ lập tức cộng số tiền đó vào `totalCost` hiển thị trên màn hình trước khi nhận phản hồi từ server. Nếu API trả về lỗi, hệ thống sẽ thực hiện roll-back trạng thái dữ liệu. Điều này tạo cảm giác ứng dụng phản hồi tức thì.
### 7.3. Phân quyền UI động (Dynamic UI Rendering based on Role)
Sử dụng các hàm Helper tại Frontend để kiểm tra quyền hạn trước khi render các thành phần tương tác, đảm bảo tính bảo mật và trải nghiệm người dùng:
```typescript
const canEdit = ['OWNER', 'MANAGER'].includes(userRole);
// Trên UI Component:
{canEdit && <Button onClick={handleEditLocation}>Chỉnh sửa lộ trình</Button>}
```