diff --git a/.env b/.env
new file mode 100644
index 0000000..c955405
--- /dev/null
+++ b/.env
@@ -0,0 +1,14 @@
+DATABASE_URL="postgresql://postgres:password@localhost:5432/travel_db?schema=public"
+PORT=3001
+REDIS_URL=redis://localhost:6379
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_SECURE=false
+SMTP_USER=yotripadmin@gmail.com
+SMTP_PASS="ahhh suif kfcp waie"
+GOOGLE_CLIENT_ID=639044397050-7hair384u6lle941h033trut1s3q47l4.apps.googleusercontent.com
+FRONTEND_URL=https://yotrip.labz.io.vn
+ADMIN_SECRET_KEY=Yotrip@Bitabub1
+# Development: Backend URL for vite dev server proxy (only used with npm run dev)
+# For production: Nginx handles proxying, this is not used
+VITE_BACKEND_URL=http://localhost:3001
diff --git a/.gitignore b/.gitignore
index b89e93d..8f80c7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-.env
node_modules
server/dist
dist
diff --git a/ANDROID_BUILD_GUIDE..md b/ANDROID_BUILD_GUIDE..md
new file mode 100644
index 0000000..1b8dcdc
--- /dev/null
+++ b/ANDROID_BUILD_GUIDE..md
@@ -0,0 +1,201 @@
+# 🗺️ Hướng Dẫn Đóng Gói & Kiểm Thử Ứng Dụng Android (YoTrip)
+
+Tài liệu này hướng dẫn chi tiết quy trình thiết lập môi trường máy tính Windows Local để biên dịch, kiểm thử dự án Frontend (React + Vite) qua Capacitor, kết nối tới hệ thống Docker Production (`https://yotrip.labz.io.vn`) và chuẩn bị phát hành lên Google Play Store.
+
+---
+
+## 📋 1. Điều Kiện Tiên Quyết (Môi Trường Windows)
+
+Trước khi chạy lệnh, đảm bảo máy tính local đã cài đặt và cấu hình đầy đủ các công cụ sau:
+
+* **Node.js:** Phiên bản v18 hoặc v20+.
+* **Java JDK:** Phiên bản 17 hoặc 21 (Temurin hoặc Microsoft OpenJDK).
+ * Biến môi trường hệ thống: `JAVA_HOME` trỏ tới thư mục cài đặt JDK.
+ * Biến `Path` hệ thống: Bổ sung `%JAVA_HOME%\bin`.
+* **Android Studio:** * Đã cài đặt **Android SDK**, **Android SDK Command-line Tools**.
+ * Biến môi trường hệ thống: `ANDROID_HOME` trỏ tới `AppData\Local\Android\Sdk`.
+ * Đã khởi tạo 1 thiết bị ảo (Android Simulator) qua *Virtual Device Manager*.
+
+---
+
+## ⚙️ 2. Cấu Hình Mã Nguồn Frontend (Local)
+
+### 2.1 Cấu hình file `.env.production`
+Tạo hoặc cập nhật file `.env.production` nằm tại thư mục gốc của `frontend/`:
+
+```text
+VITE_BACKEND_URL=[https://yotrip.labz.io.vn](https://yotrip.labz.io.vn)
+
+### 2.2 Cấu hình Axios / API Instance (src/api/axios.ts)
+Cập nhật logic baseURL để tự động phân tách môi trường chạy Web Dev (sử dụng Proxy của Vite) và môi trường chạy App Native (gọi trực tiếp URL tuyệt đối):
+
+import axios from 'axios';
+import { Capacitor } from '@capacitor/core';
+
+const API = axios.create({
+ baseURL: Capacitor.isNativePlatform()
+ ? import.meta.env.VITE_BACKEND_URL
+ : '',
+ timeout: 15000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+export default API;
+
+## 3. Cấu Hình Nền Tảng Android Native
+
+### 3.1 Thiết lập Biểu tượng Ứng dụng (App Icon)
+
+Để sử dụng frontend/public/favicon.ico làm icon của app trên Android, chúng ta cần chuyển đổi nó sang định dạng .png độ phân giải cao và sử dụng công cụ của Capacitor để tự động tạo các kích thước cần thiết cho Android.
+
+Chuẩn bị ảnh: Chuyển đổi file favicon.ico của bạn thành file .png (khuyên dùng độ phân giải ít nhất 1024x1024 pixel để có chất lượng tốt nhất trên các thiết bị đời mới) và lưu tên là icon-only.png.
+
+Cài đặt công cụ: Chạy lệnh sau tại thư mục frontend/ để cài đặt công cụ quản lý tài nguyên của Capacitor:
+
+Bash
+npm install @capacitor/assets --save-dev
+Khởi tạo thư mục: Tạo thư mục assets ở thư mục gốc của frontend/ (cùng cấp với src) và đặt file icon-only.png vào đó.
+
+Bash
+mkdir assets
+# Sau đó di chuyển file icon-only.png của bạn vào thư mục assets/
+Tạo Icon: Chạy lệnh sau để tự động tạo và đặt các icon vào đúng vị trí trong dự án Android:
+
+Bash
+npx capacitor-assets generate --android
+
+### 3.2 File capacitor.config.json
+
+Định danh chính xác gói ứng dụng (App ID) dùng để đăng ký trên Google Play Console:
+
+{
+ "appId": "com.yotrip.app",
+ "appName": "YoTrip",
+ "webDir": "dist",
+ "plugins": {
+ "SplashScreen": {
+ "launchShowDuration": 2000
+ }
+ }
+}
+
+### 3.3 File capacitor.config.json
+Định danh chính xác gói ứng dụng (App ID) dùng để đăng ký trên Google Play Console:
+
+JSON
+{
+ "appId": "com.yotrip.app",
+ "appName": "YoTrip",
+ "webDir": "dist",
+ "plugins": {
+ "SplashScreen": {
+ "launchShowDuration": 2000
+ }
+ }
+}
+### 3.4 Cấu hình quyền trong AndroidManifest.xml
+Mở đường dẫn android/app/src/main/AndroidManifest.xml, thêm các quyền truy cập Internet, định vị GPS, và các quyền cần thiết cho tính năng chụp ảnh và lưu ảnh vào bộ nhớ máy:
+
+XML
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+
+
+## 4. Tích Hợp Tính Năng Chụp Ảnh và Lưu Ảnh (React Code)
+Sử dụng Plugin của Capacitor để tích hợp trực tiếp vào code React của bạn.
+
+Cài đặt Plugin Camera: Chạy lệnh sau tại thư mục frontend/:
+
+Bash
+npm install @capacitor/camera
+npx cap update
+Ví dụ code: Dưới đây là cách implement tính năng chụp ảnh và tự động lưu ảnh gốc vào thư viện ảnh của điện thoại trong một React component (ví dụ src/components/PhotoTaker.tsx):
+
+JavaScript
+import React, { useState } from 'react';
+import { IonButton, IonIcon, IonContent, IonPage } from '@ionic/react';
+import { camera } from 'ionicons/icons';
+import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
+
+const PhotoTaker: React.FC = () => {
+ const [photoUri, setPhotoUri] = useState();
+
+ const takeAndSavePhoto = async () => {
+ try {
+ const image = await Camera.getPhoto({
+ quality: 90,
+ allowEditing: false, // Giữ nguyên ảnh gốc, không qua chỉnh sửa
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera, // Mở camera trực tiếp
+ saveToGallery: true, // YÊU CẦU MỚI: Tự động lưu ảnh gốc vào thư viện điện thoại
+ });
+
+ // Bạn có thể sử dụng image.webPath để hiển thị xem trước
+ setPhotoUri(image.webPath);
+ console.log('Ảnh đã được chụp và lưu tại:', image.path);
+
+ } catch (error) {
+ console.error('Lỗi khi chụp hoặc lưu ảnh:', error);
+ }
+ };
+
+ return (
+
+
+
+```
+
+#### 3. Nút X — Fixed ở góc trên phải (hiện tại đúng, cần giữ nguyên)
+
+Nút X hiện đang dùng `fixed` — **giữ nguyên**, chỉ điều chỉnh `top` để tính safe-area:
+```
+className="fixed top-[calc(1rem+env(safe-area-inset-top,0px))] right-4 z-50 ..."
+```
+
+#### 4. Phần ảnh (Left Side Photo) — Mobile: full width, không giới hạn chiều cao, aspect ratio tự nhiên
+
+**Hiện tại** (line 377):
+```
+
+```
+
+**Thay thành** — Trên mobile: ảnh full width, chiều cao theo tỷ lệ ảnh (aspect-ratio), không giới hạn, chạm mép trên màn hình (không có padding trên):
+```
+
+```
+
+Với ảnh bên trong:
+```
+
+```
+
+#### 5. Nút Like — Di chuyển từ góc trên trái sang góc dưới phải của vùng ảnh
+
+**Hiện tại**: `absolute top-[...] left-4` — góc trên bên trái.
+
+**Thay thành**: `absolute bottom-4 right-4` — góc dưới bên phải của ảnh, không bị che bởi thanh địa chỉ và không xung đột với nút X:
+```
+className="absolute bottom-4 right-4 z-40 ..."
+```
+
+#### 6. Thông tin người đăng — Di chuyển thành overlay góc trên trái trên ảnh
+
+Hiện tại "Mobile Uploader strip" là một block riêng biệt giữa ảnh và comment (`block md:hidden p-6 ...`).
+
+**Thay thành**: Overlay trực tiếp trên ảnh ở góc **trên bên trái**, nằm trong vùng ảnh, có safe-area padding:
+```html
+
+
+
+
+
+ {photo.uploader?.name || 'Ẩn danh'}
+
+```
+
+#### 7. "Mobile Uploader strip" block — Xóa block cũ
+
+Block `{/* Mobile Only: Uploader details & divider */}` ở lines 597–623 sẽ được **xóa** vì thông tin người đăng đã được hiển thị trực tiếp trên ảnh ở bước 6.
+
+Nếu `isAuthorized && photo.originalUrl` (nút Tải ảnh gốc): sẽ được giữ lại nhưng chuyển vào trong phần metadata/overlay của ảnh hoặc đặt trước phần comments.
+
+#### 8. Phần Comments — Cuộn cùng với nội dung (không bị kẹt trong flex)
+
+Trên mobile, toàn bộ trang cuộn — phần comment không cần `overflow-y-auto` riêng nữa (vì parent đã cuộn):
+
+**Hiện tại** (line 626):
+```
+
+```
+
+**Thay thành**:
+```
+
+```
+
+Danh sách bình luận (line 640):
+```
+
+```
+→ Trên mobile không giới hạn `overflow-y-auto`:
+```
+
+```
+
+#### 9. Comment Input — Sticky ở cuối trang trên mobile
+
+Thay vì nằm cố định cuối flex column, ô nhập bình luận sẽ sticky ở bottom:
+```
+
+```
+
+---
+
+## Tóm tắt layout cuối cùng trên mobile
+
+```
+┌─────────────────────────────────────────────┐ ← màn hình (fixed inset-0)
+│ [X nút đóng] (góc trên phải, fixed/z-50)
+│─────────────────────────────────────────────│
+│ │
+│ [Người đăng] (overlay TL) │
+│ │ ← Vùng ảnh (full width,
+│ ẢNH │ height theo tỷ lệ)
+│ │
+│ [❤ Like + số] │
+│─────────────────────────────────────────────│
+│ 📷 Timeline ảnh (nếu có nhiều ảnh) │ ← Cuộn cùng trang
+│─────────────────────────────────────────────│
+│ Tiêu đề, mô tả, ngày chụp, địa điểm │ ← Info overlay panel
+│─────────────────────────────────────────────│
+│ [Tải ảnh gốc] (nếu là chủ sở hữu) │
+│─────────────────────────────────────────────│
+│ 💬 Comment 1 │ ← Cuộn cùng trang
+│ 💬 Comment 2 │
+│ 💬 Comment 3 │
+│ ... │
+│─────────────────────────────────────────────│
+│ [Viết bình luận...] [▶ Gửi] (sticky bottom)│
+└─────────────────────────────────────────────┘
+```
+
+---
+
+## Verification Plan
+
+### Automated Tests
+- `npm run build -w frontend` — không có lỗi compilation.
+
+### Manual Verification
+1. Mở app trên Chrome mobile (Android) → modal mở full screen, ảnh chạm top, không có padding.
+2. Nút X hiển thị góc trên phải, luôn visible.
+3. Tên người đăng hiển thị overlay góc trên trái, không nhầm với người bình luận.
+4. Nút like hiển thị góc dưới phải của ảnh.
+5. Vuốt lên → ảnh và bình luận cuộn cùng nhau một cách liền mạch.
+6. Ô bình luận sticky ở dưới cùng.
+7. Trên desktop (≥768px) layout 2 cột giữ nguyên hoàn toàn.
+8. Safe area insets hoạt động đúng trên iPhone (notch/Dynamic Island) dùng Safari.
diff --git a/yotrip_system_fix_plan.md b/yotrip_system_fix_plan.md
new file mode 100644
index 0000000..50ce739
--- /dev/null
+++ b/yotrip_system_fix_plan.md
@@ -0,0 +1,171 @@
+# 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ẻ `