Files
travelplanning/NOTE_EDIT.md
T

130 lines
7.6 KiB
Markdown

# NOTE_EDIT.md
## MyNotePage.tsx
**Đường dẫn:** `frontend/src/pages/MyNotePage.tsx`
### Mục đích
Trang quản lý ghi chú cá nhân (My Notes) của người dùng trong một tour. Hỗ trợ tạo, sửa, xóa, tìm kiếm ghi chú và đồng bộ dữ liệu giữa client và server.
### Cấu trúc & Luồng hoạt động chính
- **State quản lý:**
- `notes`: danh sách ghi chú hiện tại
- `isCreating`: điều khiển hiển thị form tạo/sửa ghi chú
- `editingNoteId`: xác định đang sửa ghi chú nào
- `noteForm`: lưu trữ form data `{ title, content }`
- `searchQuery`: từ khóa tìm kiếm
- **Render có 3 nhánh:**
1. Không tạo ghi chú + không có ghi chú: hiển thị màn hình trống + nút "Tạo ghi chú mới"
2. `isCreating = true`: hiển thị form nhập tiêu đề + ReactQuill + nút Lưu/Hủy
3. `filteredNotes.length > 0`: hiển thị danh sách ghi chú dạng grid card
- **Lưu ghi chú (handleSaveNote):**
- Nếu đang `edit`: cập nhật note trong `notes` gọi API PUT
- Nếu `create` mới: tạo `temp_` id, thêm vào đầu mảng `notes`, gọi API POST để lấy id thật
- Sau khi API thành công: cập nhật `id` thật và đánh dấu `synced: true`
- **Xóa ghi chú:** hỏi confirm → đánh dấu `deletedLocally: true` → gọi API DELETE → xóa khỏi UI
- **Đồng bộ offline (syncLocalNotesToServer):**
- Xử lý tuần tự 3 nhóm: `deletedLocally``temp_` (chưa đồng bộ) → `edited` (synced: false)
- Mỗi nhóm có retry riêng, lỗi chỉ log console
- **Fetch & Cache (fetchServerNotes + useEffect):**
- Khi load trang: đọc `localStorage` key `my_journey_notes_{tourId}` làm cache ban đầu
- Gọi API GET `/api/v1/tours/${tourId}/notes` để lấy dữ liệu server
- Merge: giữ `unsyncedNotes` (temp_, synced:false, deletedLocally) + lọc khỏi server
- Sort theo `createdAt` giảm dần (mới nhất lên đầu)
- Lưu lại `localStorage` sau mỗi thay đổi
- **Tìm kiếm (filteredNotes):**
- Lọc theo title HOẶC content (strip HTML tag)
- Không lọc note đã `deletedLocally`
### ReactQuill configuration
- Module: table, header(1,2), bold/italic/underline/strike, align, indent, bullet/check list, link/image/table/clean
- Custom Icons: gán SVG từ `lucide-react` vào `Quill.import('ui/icons')`
- Table handler: dùng `prompt` nhập số hàng/cột → gọi `quill.getModule('table').insertTable()`
### Theme/Class cần lưu ý
- Wrapper ReactQuill: `bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm min-h-[500px] flex flex-col`
- CSS class ở ReactQuill đã fix: `.ql-container` lấy `h-full, min-h-0`; `.ql-editor` lấy `flex-1, overflow-y-auto` để nội dung dài cuộn trong khung
- Note card: `bg-[var(--surface)] dark:bg-[var(--surface-muted)] p-5 rounded-3xl`
- Màu chính: `text-amber-500` cho buttons, header
---
## TourDetailPage.tsx
**Đường dẫn:** `frontend/src/pages/TourDetailPage.tsx`
### Mục đích
Trang chi tiết tour, tổng hợp toàn bộ thông tin: bản đồ interactive, lộ trình, gallery ảnh, manage members, quản lý chi phí, chat, và chức năng share. Trang này rất nặng (~3400 dòng) nên cần chú ý các captured props và callback pattern.
### Cấu trúc chính
- **Header:** sticky top bar có các tab: Timeline | Spot / Location | Expenses | Chat
- **Tabs switch:** điều khiển hiển thị `ItineraryTimeline`, `ExpenseManager`, `TourChat`
- **Map section:** leaflet map kết hợp MarkerCluster, Polyline vẽ lộ trình OSRM, Recenter button
### Key logic/Data flow
- `useTourStore` là state chính: lưu `currentTour`, `legs`, `locations`, `userRole`, `mapCenter`
- **OSRM Routes:** kết quả từ backend đa segment, `combineSegmentRoutes()` gộp thành 1 polyline
- **Sync data:** `addLocation`, `editLocation`, `deleteLocation` gọi API và cập nhật store
- **PDF Export:** dùng `jsPDF` + `jspdf-autotable` xuất hóa đơn/nhật ký tour
- **ShareJourney button:** tạo public link hoặc mở share modal
- **Socket.IO:** `io()` kết nối realtime cho chat và comment
- **Offline-first:** nhiều actions gọi API nhưng không block UI; có try/catch và fallback
### Các modal/components con được gọi
- `AddLocationModal`, `MembersTab`, `ExpenseManager`, `CommentModal`, `AddPhotoModal`, `TourChat`
- `MapContextMenu`: menu right-click trên map để bắt đầu/kết thúc/add to leg
- `MapRotationHandler`: xoay map theo hướng di chuyển (cumulative rotation để tránh nhảy -360→0)
- `MapHoverTip`: tooltip xuất hiện sau 3s khi hover trên map (cho user biết tip right-click)
### Theme & Styling cần lưu ý
- Map marker icon: dùng `unpkg.com/leaflet@1.9.4` (fixed URL) do Vite bundler không nhận asset mặc định
- Màu primary tour: `text-blue-600`, accent: `text-amber-500`
- Sticky header: `bg-[var(--surface)]/80 backdrop-blur-md`
---
## ItineraryTimeline.tsx
**Đường dẫn:** `frontend/src/components/ItineraryTimeline.tsx`
### Mục đích
Component hiển thị lộ trình đa chặng (multi-leg itinerary) dạng timeline accordion. Mỗi `leg` là 1 section có thể expand/collapse độc lập (exclusive mode).
### Cấu trúc chính
- **props:**
- `onAddLocation(legId, isStart?, isEnd?)`: callback mở modal thêm địa điểm
- `onEditLocation(location)`: callback mở modal sửa địa điểm
- `onQuickNote(name)`: callback mở modal ghi chú nhanh
- `onNavigate(location)`: callback điều hướng trên map
- `onSuccess()`: callback sau khi xóa/edit thành công
- `isPublicView`: boolean, hide edit/delete buttons khi true
- **State:**
- `expandedStageId`: id của leg đang mở (chỉ 1 mở tại 1 thời điểm) → mặc định leg đầu tiên
- `isEditModalOpen`, `editingLegData`: modal sửa chặng
- `isCommentModalOpen`, `commentLocationId`, `commentLocationName`: modal comment
### Logic đặc biệt
- **Exclusive expansion:** `toggleStageExpanded` đảm bảo chỉ 1 leg mở tại 1 thời điểm (click lại để đóng)
- **Distance & Travel time:**
- Tính haversine giữa 2 điểm liên tiếp (`allLocations` flat)
- Convert giữa các chặng: lấy `prevLegLastLoc` → tính khoảng cách → hiển thị "Tiếp nối từ ..."
- `leg.totalDistance` + `formatTravelTime()` để đổi sang "X giờ Y phút"
- **Dwell time:** thời gian dừng tại mỗi điểm = `plannedEnd - plannedStart`
- **Marker milestones:**
- Start point: `plannedStart` timestamp = 0
- End point: `plannedEnd` timestamp = 0
- `TimeVariance`: so sánh actual vs planned, hiển thị "Trễ X phút" / "Đúng giờ" / "Sớm X phút"
- **Accordion CSS:** dùng `grid-template-rows: 0fr → 1fr` transition (`folder-child-content-box`) để animate mở/đóng mượt
- **Comment count:** `handleCommentIncrement/Decrement` trực tiếp mutate `useTourStore` để cập nhật UI gần như tức thì mà không cần re-fetch API
### Giao tiếp với cha (TourDetailPage)
- `onAddLocation?.(leg.id, isFirst, isLast)` ↔ mở `AddLocationModal`
- `onEditLocation(loc)` ↔ pre-fill modal edit
- `onNavigate(loc)` ↔ mở notification/guide "Đang điều hướng" rồi gọi map navigate
- `onSuccess?.()` gọi sau delete/edit thành công để refresh từ cha
---
## Lưu ý chung
- Cả 3 file đều dùng Tailwind CSS + theme variables từ `index.css`
- Offline/sync-first: ghi chú lưu localStorage trước, API sau
- Khi gọi API từ các hook, header gắn `Authorization: Bearer ${localStorage.getItem('token')}`
- `react-quill-new` dùng thay cho `react-quill` do tương thích React 18+