# Kế hoạch triển khai: Tính năng nâng cấp & Sửa lỗi hệ thống YoTrip

Bản kế hoạch này phác thảo chi tiết phương án kỹ thuật để thực hiện các tính năng mới và khắc phục các lỗi tồn tại trong dự án theo yêu cầu.

---

## User Review Required

> [!IMPORTANT]
> - **Cập nhật database schema (Prisma Migrations)**: Chúng ta cần thêm bảng mới `TourNote` và `RecommendedLocation`, đồng thời bổ sung các trường `isDeleted` và `deletedAt` cho bảng `Tour` và `Photo` để phục vụ tính năng Thùng rác (Trash Bin). Việc này yêu cầu chạy prisma migrations (`npx prisma migrate dev`).
> - **Thay đổi Nginx config**: Cấu hình Nginx trong Docker production cần bổ sung `client_max_body_size 50M;` để hỗ trợ tải ảnh lớn lên proxy trước khi nén trên client, tránh lỗi HTTP 413.
> - **Cơ chế Hybrid Lưu trữ Ghi chú (Notes)**: Ghi chú sẽ được lưu trữ song song (hybrid) cả ở `localStorage` (để hiển thị tức thời và hoạt động offline) lẫn đồng bộ lên server (để lưu trữ lâu dài và cho phép Admin kiểm duyệt/moderation). Khi mở trang ghi chú, ứng dụng sẽ tải dữ liệu từ cache `localStorage` trước, sau đó fetch dữ liệu mới nhất từ server để đồng bộ và cập nhật lại cache.

---

## Proposed Changes

### 1. Cơ sở dữ liệu (Database Schema)

#### [MODIFY] [schema.prisma](file:///home/locpham/travelplanning/backend/prisma/schema.prisma)
- **Bảng `Photo` & `Tour`**: Bổ sung hai trường `isDeleted Boolean @default(false)` và `deletedAt DateTime?` để hỗ trợ cơ chế soft-delete (chuyển vào thùng rác).
- **Bảng `ModerationSetting`**: Thêm trường `trashRetentionDays Int @default(30)` để lưu trữ số ngày lưu trữ trong thùng rác do admin quy định.
- **Thêm bảng `TourNote`**: Lưu trữ ghi chú của Tour liên kết với `Tour` và `User` sở hữu:
  ```prisma
  model TourNote {
    id        String    @id @default(uuid())
    tourId    String
    tour      Tour      @relation(fields: [tourId], references: [id], onDelete: Cascade)
    userId    String
    user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
    title     String
    content   String    @db.Text
    createdAt DateTime  @default(now())
    updatedAt DateTime  @updatedAt
    isDeleted Boolean   @default(false)
    deletedAt DateTime?
  }
  ```
- **Thêm bảng `RecommendedLocation`**: Lưu trữ các địa điểm đề xuất:
  ```prisma
  model RecommendedLocation {
    id          String   @id @default(uuid())
    type        String   // e.g. "RESTAURANT", "HOTEL", "HOMESTAY"
    name        String
    phone       String?
    email       String?
    address     String?
    latitude    Float?
    longitude   Float?
    description String   @db.Text
    stars       Int      @default(5) // Số sao đánh giá từ 1 đến 5
    isApproved  Boolean  @default(false)
    createdAt   DateTime @default(now())
  }
  ```

---

### 2. Backend API (`backend/src/main.ts`)

- **Soft-Delete cho Tours, Photos, và Notes**:
  - Cập nhật hàm `deleteTour`, `deletePhoto` để chỉ set `isDeleted: true` và `deletedAt: new Date()` thay vì xóa vĩnh viễn.
  - Cập nhật tất cả các truy vấn tour, ảnh (public & private) để loại trừ các bản ghi có `isDeleted: true`.
- **TourNote API**:
  - `GET /api/v1/tours/:tourId/notes`: Lấy danh sách ghi chú của một tour (phải là thành viên tour hoặc admin).
  - `POST /api/v1/tours/:tourId/notes`: Tạo ghi chú cho tour.
  - `PUT /api/v1/tours/:tourId/notes/:noteId`: Sửa ghi chú.
  - `DELETE /api/v1/tours/:tourId/notes/:noteId`: Soft-delete ghi chú của tour.
- **RecommendedLocation API**:
  - `GET /api/v1/recommendations`: Lấy danh sách địa điểm đề xuất đã duyệt (`isApproved: true`).
  - `POST /api/v1/recommendations`: Người dùng gửi đề xuất địa điểm mới.
  - `GET /api/v1/admin/recommendations`: Admin xem toàn bộ danh sách đề xuất.
  - `PATCH /api/v1/admin/recommendations/:id/approve`: Admin phê duyệt địa điểm đề xuất.
  - `DELETE /api/v1/admin/recommendations/:id`: Admin xóa đề xuất.
- **Admin Trash Bin API**:
  - `GET /api/v1/admin/trash`: Lấy danh sách các đối tượng trong thùng rác (phân loại Tours, Photos, Notes) kèm thông tin ngày xóa.
  - `POST /api/v1/admin/trash/restore`: Phục hồi các đối tượng được chọn (`isDeleted: false`).
  - `POST /api/v1/admin/trash/delete-permanent`: Xóa vĩnh viễn các đối tượng được chọn khỏi ổ đĩa và DB.
  - `PATCH /api/v1/admin/trash/retention-days`: Cập nhật cấu hình `trashRetentionDays`.
- **Cronjob dọn dẹp tự động (Background Service)**:
  - Khởi tạo một task chạy định kỳ (24 giờ một lần) quét qua các bảng `Tour`, `Photo`, `TourNote` có `isDeleted: true` và `deletedAt` đã quá hạn so với cấu hình `trashRetentionDays` để tự động xóa vĩnh viễn bản ghi và tệp vật lý.

---

### 3. Frontend & UI

#### [MODIFY] [nginx.conf](file:///home/locpham/travelplanning/frontend/nginx.conf)
- Thêm dòng `client_max_body_size 50M;` vào cấu hình block `server` của Nginx để khắc phục triệt để lỗi `413 (Request Entity Too Large)`.

#### [NEW] [image.ts](file:///home/locpham/travelplanning/frontend/src/utils/image.ts)
- Viết hàm tiện ích nén ảnh trên Client sử dụng Canvas API, giảm kích thước ảnh về chiều dài nhất tối đa **2048px (2K)**, chuyển định dạng sang `image/jpeg` chất lượng `0.85` trước khi tải lên.
- Áp dụng hàm nén này cho:
  - Chụp ảnh/Tải ảnh ẩn danh ở [LandingPage.tsx](file:///home/locpham/travelplanning/frontend/src/pages/LandingPage.tsx)
  - Tải ảnh lên trong Tour ở [AddPhotoModal.tsx](file:///home/locpham/travelplanning/frontend/src/components/AddPhotoModal.tsx)
  - Các vị trí upload ảnh khác.

#### [MODIFY] [LoginModal.tsx](file:///home/locpham/travelplanning/frontend/src/components/LoginModal.tsx)
- Thêm class CSS `max-h-[90vh] overflow-y-auto` vào container chính của modal để tránh bị che mất nút đăng nhập trên các trình duyệt di động có chiều cao màn hình nhỏ hoặc khi bàn phím ảo hiển thị.

#### [MODIFY] [TourDetailPage.tsx](file:///home/locpham/travelplanning/frontend/src/pages/TourDetailPage.tsx)
- **Khắc phục lỗi xuất PDF (`oklab` error)**:
  - Thay vì clone trực tiếp DOM timeline chứa màu oklab phức tạp của Tailwind v4, chúng ta sẽ xây dựng trực tiếp một phần tử HTML `div` đơn giản, dùng cấu trúc bảng hoặc danh sách căn lề cơ bản, sử dụng màu HEX tiêu chuẩn (`#ffffff`, `#3b82f6`, v.v.).
  - Trước khi gọi `html2pdf().from(element).save()`, tạm thời đặt `.disabled = true` cho tất cả các thẻ `<style>` và `<link rel="stylesheet">` trong tài liệu (ngoại trừ CSS tuỳ chỉnh cho PDF), sau đó phục hồi lại trạng thái cũ trong block `finally` của `try-catch`. Phương án này giúp `html2canvas` bỏ qua hoàn toàn các stylesheet chứa hàm màu `oklab` của Tailwind.
- **Xuất Google Sheets**:
  - Bổ sung nút "Xuất CSV (Google Sheets)".
  - Khi nhấn, tạo chuỗi nội dung CSV chi tiết lịch trình gồm: Chặng, Địa điểm, Thời gian bắt đầu, Kết thúc, Thời gian dừng, Ghi chú.
  - Ghép tiền tố UTF-8 BOM (`\uFEFF`) để hỗ trợ tốt font Tiếng Việt có dấu khi import trực tiếp vào Google Sheets / Excel, và kích hoạt download tệp dạng `.csv`.

#### [MODIFY] [MyNotePage.tsx](file:///home/locpham/travelplanning/frontend/src/pages/MyNotePage.tsx)
- **Tách biệt ghi chú theo Tour**: Nhận prop `tourId` từ component cha và thực hiện gọi các API `/api/v1/tours/:tourId/notes` để truy xuất và lưu trữ ghi chú trực tiếp lên máy chủ, giải quyết việc các ghi chú hiển thị chung.

#### [MODIFY] [LandingPage.tsx](file:///home/locpham/travelplanning/frontend/src/pages/LandingPage.tsx)
- **Lướt ảnh bằng tay (Swiping Carousel)**:
  - Thay thế hệ thống crossfade tĩnh bằng một wrapper flex container xếp ngang toàn bộ ảnh cộng đồng.
  - Sử dụng các sự kiện `onTouchStart`, `onTouchMove`, `onTouchEnd` để đo khoảng cách dịch chuyển ngón tay (`deltaX`), áp dụng hiệu ứng chuyển dịch `transform: translateX(...)` trực quan và chuyển ảnh (`currentBgIndex`) khi vuốt tay qua ngưỡng 50px.
- **Chặn tải/lưu ảnh cho Public Users**:
  - Với khách chưa đăng nhập, áp dụng `draggable="false"`, `onContextMenu={(e) => e.preventDefault()}` và CSS `pointer-events: none` lên các phần tử hình ảnh (ảnh nền, ảnh thumbnail) để chặn thao tác nhấn giữ lưu ảnh hoặc nhấp chuột phải.

#### [MODIFY] [ExploreMap.tsx](file:///home/locpham/travelplanning/frontend/src/pages/ExploreMap.tsx)
- **Chặn lưu ảnh**: Áp dụng chặn drag, chuột phải tương tự LandingPage cho khách vãng lai.
- **Xem Blacklist gần bạn**:
  - Trong widget Blacklist, bổ sung nút "Xem địa điểm gần tôi".
  - Tính toán khoảng cách giữa tọa độ của các địa điểm blacklist và `userPos` (GPS hiện tại), sắp xếp gần nhất lên trước. Khi click vào một địa điểm, tự động di chuyển bản đồ đến toạ độ đó.
- **Recommended List (Danh sách đề xuất)**:
  - Tạo Widget đề xuất (đối xứng hoặc đặt cạnh Blacklist).
  - Tải dữ liệu từ `/api/v1/recommendations`, hiển thị danh sách gồm tên cơ sở, loại hình, số sao đánh giá (sao vàng), mô tả.
  - Thêm nút "Định vị" trong widget để bay đến vị trí trên bản đồ, hiển thị marker đề xuất (ghim xanh lá/ngôi sao vàng) kèm nút "Xem địa điểm gần tôi" tương tự blacklist.

#### [MODIFY] [MemberDashboard.tsx](file:///home/locpham/travelplanning/frontend/src/pages/MemberDashboard.tsx)
- **Đồng bộ hóa Ngôn ngữ & Theme màu**:
  - Đưa phần tuỳ chọn chọn Ngôn ngữ và Theme màu ra ngoài thanh Sidebar chính (hoặc trên thanh header) để người dùng thay đổi trực tiếp bất cứ lúc nào, không bắt buộc phải bật nút chia sẻ cứu hộ.
  - Cập nhật CSS của `MemberDashboard.tsx` với đầy đủ tiền tố `dark:` để khi người dùng đổi sang theme sáng, toàn bộ dashboard sẽ chuyển sang giao diện Light Theme sang trọng, đẹp mắt.
- **Hiệu ứng chuông lắc trên Avatar**:
  - Định nghĩa keyframes lắc chuông `bell-ring` trong CSS.
  - Nếu `unreadChatSenders.length > 0 || unreadTourChats.length > 0 || receivedRequests.length > 0` (người dùng có thông báo mới), áp dụng class `animate-ring` giúp biểu tượng chiếc chuông nhỏ trên avatar của họ lắc lư để thu hút sự chú ý.

#### [MODIFY] [UserManagementModal.tsx](file:///home/locpham/travelplanning/frontend/src/components/UserManagementModal.tsx)
- **Tab quản lý Tours & Notes**: Bổ sung tab xem danh sách Tours và Notes đang hoạt động, cho phép chọn nhiều mục cùng lúc (Multi-select) để chuyển vào Thùng rác.
- **Tab quản lý Thùng rác (Trash Bin)**:
  - Chia làm 3 sub-tab: Tours rác, Ảnh rác, Ghi chú rác.
  - Hiển thị ngày bị xóa và số ngày đếm ngược còn lại trước khi tự động bị xóa vĩnh viễn.
  - Cho phép admin tích chọn nhiều mục để thực hiện **Khôi phục** hoặc **Xóa vĩnh viễn** ngay lập tức.
  - Bổ sung ô nhập số ngày lưu trữ (`retentionDays`) tại đầu tab và nút lưu cấu hình.
- **Tab quản lý địa điểm đề xuất**: Admin xem danh sách đề xuất từ người dùng gửi lên, chọn nhiều để Phê duyệt hoặc Xóa bỏ.

---

## Verification Plan

### Automated Tests
- Kiểm tra migration cơ sở dữ liệu:
  ```bash
  npx prisma migrate status --schema=backend/prisma/schema.prisma
  ```
- Kiểm tra biên dịch TypeScript trên toàn dự án:
  ```bash
  # Backend
  cd backend && npx tsc --noEmit
  # Frontend
  cd frontend && npx tsc --noEmit
  ```

### Manual Verification
1. **Kiểm tra lắc chuông**: Tạo một tin nhắn chưa đọc từ người dùng khác, xác nhận chuông ở avatar trên Dashboard bắt đầu lắc.
2. **Kiểm tra lướt ảnh LandingPage**: Mở LandingPage bằng giả lập thiết bị mobile trên trình duyệt, vuốt ngang màn hình và kiểm tra ảnh nền cuộn theo tay mượt mà.
3. **Kiểm tra chặn lưu ảnh**: Đăng xuất tài khoản, chuột phải vào ảnh nền/thumbnail ở LandingPage và ExploreMap, xác nhận không hiển thị menu ngữ cảnh lưu ảnh và không thể kéo thả ảnh.
4. **Kiểm tra xuất Excel & PDF**: Nhấn nút xuất PDF lịch trình và xác nhận không còn lỗi oklab, PDF hiển thị sạch sẽ không có giá tiền. Nhấn xuất CSV và import vào Google Sheets kiểm tra hiển thị font Tiếng Việt bình thường.
5. **Kiểm tra phân biệt Ghi chú**: Tạo ghi chú cho Tour A, chuyển sang Tour B và kiểm tra danh sách ghi chú của Tour B trống.
6. **Kiểm tra Blacklist & Recommended gần tôi**: Bật GPS giả lập trên trình duyệt, mở Widget Blacklist và Recommended trên bản đồ, kiểm tra danh sách địa điểm tự động sắp xếp theo thứ tự khoảng cách tăng dần.
7. **Kiểm tra Thùng rác Admin**: Đăng nhập tài khoản admin, xóa nhiều tour/note/ảnh, mở Thùng rác, kiểm tra danh sách đếm ngược ngày xoá, thử khôi phục hoặc xoá vĩnh viễn hàng loạt.
8. **Kiểm tra đổi Theme màu**: Click đổi theme sáng/tối trên dashboard, xác nhận giao diện đổi màu mượt mà theo đúng sắc độ.
