fix: hiển thị ngày ở các chặng
This commit is contained in:
+137
@@ -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.
|
||||
@@ -513,24 +513,35 @@ export const TourDetailPage = ({
|
||||
const legRowRanges: Record<string, { start: number; count: number }> = {};
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user