diff --git a/DATE_INSERT.md b/DATE_INSERT.md new file mode 100644 index 0000000..1f57ce1 --- /dev/null +++ b/DATE_INSERT.md @@ -0,0 +1,137 @@ +# 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/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index 6315e11..36601e1 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -513,24 +513,35 @@ export const TourDetailPage = ({ const legRowRanges: Record = {}; const pdfMapLinks: { [key: number]: string } = {}; - const formatDateTime = (dateStr: string) => { - if (!dateStr) return ''; - const d = new Date(dateStr); - if (isNaN(d.getTime())) return ''; - const hours = String(d.getHours()).padStart(2, '0'); - const minutes = String(d.getMinutes()).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const year = d.getFullYear(); - return `${hours}:${minutes} ${day}/${month}/${year}`; - }; - let stt = 1; let globalRowIndex = 0; + const extractAndFormatTimeInline = (primaryTime: any, fallbackLegObj: any) => { + 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}`; + }; + 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 } + }; + const legLocations = (leg.locations || []).filter((loc: any) => loc && (loc.plannedStart || loc.plannedEnd || loc.name) ); @@ -542,14 +553,13 @@ export const TourDetailPage = ({ legLocations.forEach((loc: any, locIdx: number) => { const isStartPoint = loc.plannedStart && new Date(loc.plannedStart).getTime() === 0; - const timeSource = isStartPoint ? loc.plannedEnd : loc.plannedStart; - const timeStr = timeSource ? formatDateTime(timeSource) : ''; + const initialTimeSource = isStartPoint ? loc.plannedEnd : loc.plannedStart; + + 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 coordStr = loc.latitude && loc.longitude ? `\n${loc.latitude}, ${loc.longitude}` : ''; const locationText = [locName, addressStr, coordStr].filter(Boolean).join('\n'); const noteText = loc.note || ''; @@ -573,18 +583,20 @@ export const TourDetailPage = ({ tableRows.push([ stt++, timeStr, - locIdx === 0 ? legName : '', + locIdx === 0 ? legNameCellObj : '', locationCellObj, noteText ]); globalRowIndex++; }); } else { + const legTimeStr = extractAndFormatTimeInline(null, leg); + legRowRanges[leg.id] = { start: startRow, count: 1 }; tableRows.push([ stt++, - '', - legName, + legTimeStr, + legNameCellObj, 'Chưa có địa điểm trong chặng này', '' ]); @@ -646,7 +658,44 @@ export const TourDetailPage = ({ } } }, + willDrawCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + if (data.cell.text && data.cell.text.length > 0) { + data.cell.customInlineBuffer = data.cell.text.join(''); + } + data.cell.text = []; + } + }, 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; + + data.doc.setFont(data.cell.styles.font, 'bold'); + data.doc.setTextColor(239, 68, 68); + data.doc.text(timePart, targetX, targetY); + + data.doc.setFont(data.cell.styles.font, 'normal'); + data.doc.setTextColor(37, 99, 235); + data.doc.text(datePart, targetX + timeWidth + spaceWidth, targetY); + } + } + if (data.section === 'body' && data.column.index === 3) { const activeRowIndex = data.row.index; const targetMapUrl = pdfMapLinks[activeRowIndex];