fix: in đậm cho tiêu đề của cột khi xuất pdf

This commit is contained in:
2026-06-24 20:52:17 +07:00
parent ef295e0994
commit e385049652
2 changed files with 106 additions and 137 deletions
-137
View File
@@ -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
View File
@@ -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.