7.6 KiB
7.6 KiB
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ạiisCreating: điều khiển hiển thị form tạo/sửa ghi chúeditingNoteId: xác định đang sửa ghi chú nàonoteForm: lưu trữ form data{ title, content }searchQuery: từ khóa tìm kiếm
- Render có 3 nhánh:
- 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"
isCreating = true: hiển thị form nhập tiêu đề + ReactQuill + nút Lưu/HủyfilteredNotes.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 trongnotesgọi API PUT - Nếu
createmới: tạotemp_id, thêm vào đầu mảngnotes, gọi API POST để lấy id thật - Sau khi API thành công: cập nhật
idthật và đánh dấusynced: true
- Nếu đang
- 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
- Xử lý tuần tự 3 nhóm:
- Fetch & Cache (fetchServerNotes + useEffect):
- Khi load trang: đọc
localStoragekeymy_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
createdAtgiảm dần (mới nhất lên đầu) - Lưu lại
localStoragesau mỗi thay đổi
- Khi load trang: đọc
- 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-reactvàoQuill.import('ui/icons') - Table handler: dùng
promptnhập số hàng/cột → gọiquill.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-containerlấyh-full, min-h-0;.ql-editorlấyflex-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-500cho 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
useTourStorelà state chính: lưucurrentTour,legs,locations,userRole,mapCenter- OSRM Routes: kết quả từ backend đa segment,
combineSegmentRoutes()gộp thành 1 polyline - Sync data:
addLocation,editLocation,deleteLocationgọi API và cập nhật store - PDF Export: dùng
jsPDF+jspdf-autotablexuấ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,TourChatMapContextMenu: menu right-click trên map để bắt đầu/kết thúc/add to legMapRotationHandler: 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ểmonEditLocation(location): callback mở modal sửa địa điểmonQuickNote(name): callback mở modal ghi chú nhanhonNavigate(location): callback điều hướng trên maponSuccess(): callback sau khi xóa/edit thành côngisPublicView: 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ênisEditModalOpen,editingLegData: modal sửa chặngisCommentModalOpen,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 (
allLocationsflat) - 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"
- Tính haversine giữa 2 điểm liên tiếp (
- Dwell time: thời gian dừng tại mỗi điểm =
plannedEnd - plannedStart - Marker milestones:
- Start point:
plannedStarttimestamp = 0 - End point:
plannedEndtimestamp = 0 TimeVariance: so sánh actual vs planned, hiển thị "Trễ X phút" / "Đúng giờ" / "Sớm X phút"
- Start point:
- Accordion CSS: dùng
grid-template-rows: 0fr → 1frtransition (folder-child-content-box) để animate mở/đóng mượt - Comment count:
handleCommentIncrement/Decrementtrực tiếp mutateuseTourStoređể 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ởAddLocationModalonEditLocation(loc)↔ pre-fill modal editonNavigate(loc)↔ mở notification/guide "Đang điều hướng" rồi gọi map navigateonSuccess?.()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-newdùng thay choreact-quilldo tương thích React 18+