fix: in đậm cho tiêu đề của cột khi xuất pdf
This commit is contained in:
-137
@@ -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.
|
|
||||||
+106
@@ -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:
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleNavigateToItinerary(currentTour)}
|
||||||
|
className="flex-1 bg-yotrip-surface ... flex items-center justify-center"
|
||||||
|
>
|
||||||
|
Chi tiết hành trình
|
||||||
|
</button>
|
||||||
|
|
||||||
|
### 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<string | null>(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.
|
||||||
Reference in New Issue
Block a user