diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79ebfc9..b39467a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -111,4 +111,147 @@ Lưu trữ tài nguyên đa phương tiện gắn với bối cảnh chuyến đ 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`). \ No newline at end of file + * 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ế UI Layout: Màn hình Tổng quan Chuyến đi (Tour Dashboard) + +Đâ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 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 danh sách 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`. Đây là tab mặc định khi vào Tour. + 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 (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 + +---------------------------------------+ + | | + | (Nội dung tab hiện tại) | + | | + +---------------------------------------+ +``` ++---------------------------------------+ + +### 6.3. Tab Lộ Trình: Cấu trúc Cây Phân Cấp (Timeline View) + +Đây là phần phức tạp nhất vì dữ liệu có cấu trúc lồng nhau: **Tour ➔ Leg (Chặng) ➔ Location (Địa điểm)**. Giao diện sử dụng mô hình Timeline đứng (Vertical Timeline) để hiển thị lịch trình di chuyển một cách chuẩn mực. + +#### A. 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:** Thay vì kéo một danh sách dài vô tận, người dùng có thể chọn nhanh các Chặng (ví dụ: Chặng 1, Chặng 2, Chặng 3 hoặc Ngày 1, Ngày 2...). +* **Logic:** Khi người dùng nhấn vào một `Leg`, danh sách các `Location` tương ứng sẽ được hiển thị phía dưới dựa vào trường `sequence`. + +#### B. 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]”**. + * Điều này giúp người dùng nắm bắt nhanh khoản tiền này được tiêu dùng tại vị trí nào trong hành trình. + +#### 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 chọn sẵn `Leg` hoặc `Location` dựa trên ngữ cảnh người dùng đang xem để 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 tận dụng tốt hai trường này để tối ưu hóa việc lưu giữ và quản lý kỷ niệm chuyến đi. + +#### 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, ảnh chụp tại đó sẽ hiện ngay bên dưới địa điểm đó hoặc gom thành một cụm để dễ nhớ kỷ niệm gắn liền với lộ trình di chuyển. +* **Xem dạng lưới (Grid View):** Hiển thị dạng lưới ảnh tiêu chuẩn, tương tự các ứng dụng quản lý ảnh (Photos) thông thường trên điện thoại. + +#### B. Bộ lọc Quyền riêng tư (PrivacyLevel) +Logic hiển thị tuân thủ nghiêm ngặt theo phân quyền: +* 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. + +#### C. Chỉ báo UI (Privacy Indicators) +Trên góc mỗi ảnh hiển thị một icon nhỏ giúp người dùng dễ dàng kiểm soát trạng thái chia sẻ của mình: +* **Hình ổ khóa:** Dành cho ảnh ở chế độ `PRIVATE`. +* **Hình con mắt:** Dành cho ảnh ở chế độ `TOUR_ONLY` hoặc `PUBLIC`. + +--- + +## 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 && } +``` diff --git a/AddLocationModal.tsx b/AddLocationModal.tsx new file mode 100644 index 0000000..2bf11d6 --- /dev/null +++ b/AddLocationModal.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { X, MapPin, Loader2, Clock } from 'lucide-react'; +import { useTourStore } from './useTourStore.js'; + +export const AddLocationModal = ({ isOpen, onClose, tourId }: { isOpen: boolean, onClose: () => void, tourId: string }) => { + const [formData, setFormData] = useState({ + name: '', + address: '', + latitude: 10.7769, + longitude: 106.7009, + type: 'VISIT', + plannedStart: '', + plannedEnd: '' + }); + const [isLoading, setIsLoading] = useState(false); + const addLocation = useTourStore(state => state.addLocation); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + await addLocation(tourId, { + ...formData, + latitude: parseFloat(formData.latitude as any), + longitude: parseFloat(formData.longitude as any), + }); + onClose(); + } catch (error) { + alert('Lỗi khi thêm địa điểm'); + } finally { + setIsLoading(false); + } + }; + + return ( +
Tổng chi tiêu hiện tại
++
"{randomQuote}"
+Tính năng quản lý thành viên đang được cập nhật...
+