From e385049652096b57087e9f14f5cdbbf01c712854 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 24 Jun 2026 20:52:17 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20in=20=C4=91=E1=BA=ADm=20cho=20ti=C3=AAu?= =?UTF-8?q?=20=C4=91=E1=BB=81=20c=E1=BB=A7a=20c=E1=BB=99t=20khi=20xu?= =?UTF-8?q?=E1=BA=A5t=20pdf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DATE_INSERT.md | 137 ------------------------------------------------ DATE_OF_TOUR.md | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 137 deletions(-) delete mode 100644 DATE_INSERT.md create mode 100644 DATE_OF_TOUR.md diff --git a/DATE_INSERT.md b/DATE_INSERT.md deleted file mode 100644 index 1f57ce1..0000000 --- a/DATE_INSERT.md +++ /dev/null @@ -1,137 +0,0 @@ -# To AI Agent: Fix Missing Leg Dates and Implement Full Temporal Fallbacks in PDF Export Table - -## 1. Context & Layout Bug Analysis -We are fixing a data-extraction omission bug in the PDF generation script based on `image_c13c27.png` (Timeline UI) and `image_c14d36.png` (Generated PDF): -- **The Issue:** As shown in the timeline UI, every Stage/Leg has definitive date attributes (e.g., Chặng 3: `01/07/2026`, Chặng 4: `02/07/2026`, Chặng 5: `03/07/2026`). However, in the generated PDF table, the "Ngày giờ" column renders completely blank for rows 13, 14, and 15. -- **Root Causes:** 1. For rows 13 & 14 (Empty stages), the previous fallback property keys did not match the actual database object keys inside the `leg` state framework. - 2. For row 15 ("595 Trần Cao Vân"), the stage *has* a location, but because that specific location node lacks an explicit `plannedStart` timestamp, the cell defaulted to an empty string—ignoring the parent Leg's valid date context (`03/07/2026`). - ---- - -## 2. Refactoring Strategy & Fallback Hierarchy -We need to establish a strict multi-tiered date resolver helper function that queries both target schema properties and fallback data containers: -1. **Leg Object Scanning Matrix:** The resolver must sequentially look up: `leg.plannedStart` ➔ `leg.startDate` ➔ `leg.start_date` ➔ `leg.date` ➔ `leg.createdAt`. -2. **Location Cell Level Injection:** When mapping individual location rows, if `loc.plannedStart` is missing or invalid, the generator must instantly fall back to its parent `leg`'s temporal parameters instead of leaving a blank row cell. - ---- - -## 3. Code Refactoring Blueprint - -### Step 1: Overhaul the Itinerary Data Processing Loop -Replace your data-mapping iteration phase with this secure, fallback-fortified configuration block: - -```typescript -if (currentTour?.legs && currentTour.legs.length > 0) { - currentTour.legs.forEach((leg: any, legIdx: number) => { - const legName = leg.note || `Chặng ${legIdx + 1}`; - const legNameCellObj = { content: legName, styles: { fontStyle: 'bold' as const } }; - - // 1. Comprehensive Robust Date Extractor/Format Parser Helper - const extractAndFormatTimeInline = (primaryTime: any, fallbackLegObj: any) => { - // Establish priority resolution chain - const absoluteTimeSource = primaryTime || - fallbackLegObj?.plannedStart || - fallbackLegObj?.startDate || - fallbackLegObj?.start_date || - fallbackLegObj?.date || - fallbackLegObj?.createdAt; - - if (!absoluteTimeSource) return ''; - - const d = new Date(absoluteTimeSource); - if (isNaN(d.getTime())) return ''; - - const hhmm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; - const ddmmyyyy = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; - return `${hhmm}|${ddmmyyyy}`; // Bounded pipe string for the centered custom rendering hook - }; - - const legLocations = (leg.locations || []).filter((loc: any) => - loc && (loc.plannedStart || loc.plannedEnd || loc.name) - ); - - const startRow = globalRowIndex; - - if (legLocations.length > 0) { - legRowRanges[leg.id] = { start: startRow, count: legLocations.length }; - - legLocations.forEach((loc: any, locIdx: number) => { - const isStartPoint = loc.plannedStart && new Date(loc.plannedStart).getTime() === 0; - const initialTimeSource = isStartPoint ? loc.plannedEnd : loc.plannedStart; - - // FIX BUG AT ROW 15: Pass the location timestamp, but bound the entire parent leg object as secondary fallback - const timeStr = extractAndFormatTimeInline(initialTimeSource, leg); - - const locName = loc.name || ''; - const addressStr = loc.address || ''; - const coordStr = loc.latitude && loc.longitude ? `\n${loc.latitude}, ${loc.longitude}` : ''; - const locationText = [locName, addressStr, coordStr].filter(Boolean).join('\n'); - - tableRows.push([ - stt++, - timeStr, // Safely guaranteed to possess at least the parent Leg Date info - locIdx === 0 ? legNameCellObj : '', - locationText, - loc.note || '' - ]); - globalRowIndex++; - }); - } else { - // FIX BUGS AT ROW 13 & 14: Force resolution of empty leg properties using full lookup schema chain - const legTimeStr = extractAndFormatTimeInline(null, leg); - - legRowRanges[leg.id] = { start: startRow, count: 1 }; - tableRows.push([ - stt++, - legTimeStr, // Injected fallback inline timestamp string - legNameCellObj, - 'Chưa có địa điểm trong chặng này', - '' - ]); - globalRowIndex++; - } - }); -} - -### Step 2: Retain the Centered Single-Row Rendering Hook Configuration -Ensure that the didDrawCell configurations continue to split and paint the text color matrix seamlessly within a single horizontal baseline row: - -// Keep this implementation inside your active doc.autoTable configurations block -didDrawCell: (data: any) => { - if (data.section === 'body' && data.column.index === 1) { - const rawTextStr = data.cell.customInlineBuffer || data.cell.raw; - - if (rawTextStr && typeof rawTextStr === 'string' && rawTextStr.includes('|')) { - const [timePart, datePart] = rawTextStr.split('|'); - const separatorSpace = " "; - - data.doc.setFont(data.cell.styles.font, 'bold'); - const timeWidth = data.doc.getTextWidth(timePart); - - data.doc.setFont(data.cell.styles.font, 'normal'); - const spaceWidth = data.doc.getTextWidth(separatorSpace); - const dateWidth = data.doc.getTextWidth(datePart); - - const totalBlockWidth = timeWidth + spaceWidth + dateWidth; - const targetX = data.cell.x + (data.cell.width - totalBlockWidth) / 2; - const targetY = data.cell.y + (data.cell.height / 2) + (data.cell.styles.fontSize / 2) - 1; - - // Draw Time -> Vivid Bold Red (#ef4444) - data.doc.setFont(data.cell.styles.font, 'bold'); - data.doc.setTextColor(239, 68, 68); - data.doc.text(timePart, targetX, targetY); - - // Draw Date -> Clean Regular Blue (#2563eb) - data.doc.setFont(data.cell.styles.font, 'normal'); - data.doc.setTextColor(37, 99, 235); - data.doc.text(targetX + timeWidth + spaceWidth, targetY); - } - } -} - -## 4. Verification Checklist for AI Verification -[ ] Empty Stage Time Capture: Rows 13 ("Quay về xứ nẫu") and 14 ("Thưởng thức don sông Trà") must automatically capture their matching 01/07/2026 and 02/07/2026 parameters from the timeline view hierarchy. - -[ ] Location Level Recovery: Row 15 ("595 Trần Cao Vân") must successfully parse and render its parent leg's date (03/07/2026) instead of displaying a blank cell block. - -[ ] Default Time Token: If the parent Leg data context only specifies a raw date string without precise hours/minutes, confirm it sets the time token smoothly to 00:00 for baseline rendering stability. \ No newline at end of file diff --git a/DATE_OF_TOUR.md b/DATE_OF_TOUR.md new file mode 100644 index 0000000..2e30d9c --- /dev/null +++ b/DATE_OF_TOUR.md @@ -0,0 +1,106 @@ +# To AI Agent: Implement Smart Date-Based Accordion Auto-Expansion on Itinerary Navigation + +## 1. Context & Feature Objective +We are introducing a smart UX routing feature between the `MemberDashboard.tsx` (or Tour Card grid) and the `ItineraryTimeline.tsx` detailed view as shown in `image_c1bcff.png` and `image_c1bdb7.png`. + +- **The Goal:** When a user clicks the "Chi tiết hành trình" (Itinerary Details) button on a tour card, the system must evaluate the user's **current real-time system date**. It will match this date against the date ranges allocated to each Stage/Leg within that specific tour. +- **Action:** Upon navigating to the timeline page, the accordion wrapper corresponding to today's matching Leg must be **expanded by default** (`expandedStageId === leg.id`). If today's date falls outside the entire tour timeline (before start or after end), fallback to expanding the very first Leg (`legs[0].id`). + +--- + +## 2. Technical Implementation Architecture + +We will use `react-router-dom`'s navigation state mechanism to transfer the target calculated ID across routing boundaries without polluting the URL structure with complex query strings. + +### Execution Blueprint: +1. **In Dashboard / Card Component:** Intercept the click event on "Chi tiết hành trình". Calculate the matching `leg.id` using a safe date-range comparison algorithm. Pass it inside the `Maps()` state payload. +2. **In Timeline Component:** Check for the passed state parameter inside a mount-level `useEffect` hook. If detected, override the initial `expandedStageId` state with this smart calculated ID. + +--- + +## 3. Detailed Code Specifications + +### Step 1: Implement Date-Matching Engine in Dashboard +Locate the "Chi tiết hành trình" action button inside the dashboard module. Refactor its click handler to compute the current timezone-safe operational timeline match: + +```typescript +import { useNavigate } from 'react-router-dom'; + +// Inside your Tour Card / MemberDashboard component: +const navigate = useNavigate(); + +const handleNavigateToItinerary = (tour: any) => { + let targetLegId = null; + + if (tour?.legs && tour.legs.length > 0) { + // 1. Get current system local time stripped of hours/minutes for pure date comparison + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // 2. Loop and evaluate leg date bounds + for (const leg of tour.legs) { + // Resolve flexible date parameter keys from schema model + const rawStart = leg.startDate || leg.plannedStart || leg.date; + const rawEnd = leg.endDate || leg.plannedEnd || leg.date; + + if (rawStart) { + const startBound = new Date(rawStart); + startBound.setHours(0, 0, 0, 0); + + const endBound = rawEnd ? new Date(rawEnd) : new Date(rawStart); + endBound.setHours(23, 59, 59, 999); + + // Check if today falls cleanly inside this specific leg's timeline envelope + if (today >= startBound && today <= endBound) { + targetLegId = leg.id; + break; // Match found, terminate loop early + } + } + } + + // 3. Robust Fallback: If today doesn't match any leg, default to the first leg + if (!targetLegId) { + targetLegId = tour.legs[0].id; + } + } + + // 4. Route with state package injection + navigate(`/tour/${tour.id}`, { + state: { defaultExpandedLegId: targetLegId } + }); +}; + +Update your JSX button connector to trigger this method: + + + +### Step 2: Consume State Payload inside the Itinerary Timeline Page +Locate the state hooks where expandedStageId is managed inside your timeline component. Update the initialization lifecycle engine to look for the navigation bundle parameters: + +import { useLocation } from 'react-router-dom'; + +// Inside your main Itinerary Timeline rendering component: +const location = useLocation(); +const [expandedStageId, setExpandedStageId] = useState(null); + +useEffect(() => { + // Check if a specific leg ID was passed down via smart routing engine + if (location.state?.defaultExpandedLegId) { + setExpandedStageId(location.state.defaultExpandedLegId); + } else if (currentTour?.legs && currentTour.legs.length > 0) { + // Standard basic fallback behavior if user accessed URL directly without dashboard state + setExpandedStageId(currentTour.legs[0].id); + } +}, [location.state, currentTour]); + +## 4. Verification & Acceptance Criteria +[ ] Mid-Tour Testing: Set a tour leg's duration date range to cover the current real-world date. Click "Chi tiết hành trình" from the dashboard. The timeline page must load with that specific leg opened automatically. + +[ ] Out-of-Bounds Fallback Testing: Access a historical tour (e.g., a trip from 2024). Click the details button. The system must gracefully recognize no dates match today, and auto-expand Chặng 1 (legs[0]) instead of breaking or leaving all accordions closed. + +[ ] Direct URL Access Resilience: Manually type the tour URL directly into the browser address bar (without clicking the dashboard button). The timeline state must recover gracefully and default open the first leg seamlessly.