133 lines
6.0 KiB
Markdown
133 lines
6.0 KiB
Markdown
# To AI Agent: Implement Clickable Google Maps Hyperlinks in PDF Export Table
|
|
|
|
## 1. Context & Feature Objective
|
|
We are upgrading the PDF export functionality within the Yotrip Travel Planner application. Currently, the PDF generates a static table containing location names, addresses, and coordinates.
|
|
**Objective:** Automatically turn every location row inside the PDF table into an interactive, clickable hyperlink. When a user clicks on the location cell in the generated PDF document, it must immediately open a browser tab navigating directly to that exact location on Google Maps.
|
|
|
|
---
|
|
|
|
## 2. Technical Architecture & Data Strategy
|
|
|
|
Because `jspdf-autotable` draws text onto a canvas layout, raw HTML tags like `<a>` will fail. We must implement a two-step rendering lifecycle:
|
|
1. **Extraction & Styling State:** During the data loop, verify coordinates or address data. Generate a standard universal Google Maps search URL, save it into a coordinate-tracking index object, and transform the raw text cell into a styled cell object (Yale Blue text `#28536b` to mimic an online link).
|
|
2. **Link Injection Layer:** Utilize the `didDrawCell` hook callback inside the `doc.autoTable` configuration to position a native invisible link window (`doc.link()`) precisely over the drawn dimensions of that specific location cell.
|
|
|
|
---
|
|
|
|
## 3. Code Refactoring Specification
|
|
|
|
### Step 1: Update Data Preparation Loop
|
|
Locate the loop processing `currentTour.legs` inside your PDF generation code block. Initialize a temporary lookup map array named `pdfMapLinks` and refactor the item distribution logic as follows:
|
|
|
|
```typescript
|
|
// Initialize an index mapping registry for hyperlinks before the loop execution
|
|
const pdfMapLinks: { [key: number]: string } = {};
|
|
|
|
if (currentTour?.legs && currentTour.legs.length > 0) {
|
|
currentTour.legs.forEach((leg: any, legIdx: number) => {
|
|
const legName = leg.note || `Chặng ${legIdx + 1}`;
|
|
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 timeSource = isStartPoint ? loc.plannedEnd : loc.plannedStart;
|
|
const timeStr = timeSource ? formatDateTime(timeSource) : '';
|
|
|
|
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');
|
|
const noteText = loc.note || '';
|
|
|
|
// 1. Generate standard universal Google Maps URL query pattern
|
|
let mapUrl = '';
|
|
if (loc.latitude && loc.longitude) {
|
|
// Absolute Precision using GPS coordinates
|
|
mapUrl = `https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}`;
|
|
} else if (addressStr || locName) {
|
|
// Text-search query fallback if GPS coordinates are missing
|
|
mapUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(addressStr || locName)}`;
|
|
}
|
|
|
|
// 2. Register current row array position to map lookup index
|
|
const currentRowPosition = tableRows.length;
|
|
if (mapUrl) {
|
|
pdfMapLinks[currentRowPosition] = mapUrl;
|
|
}
|
|
|
|
// 3. Convert raw string into custom styled autoTable Cell configuration object
|
|
const locationCellObj = {
|
|
content: locationText,
|
|
// Apply custom link colors matching theme style #28536b (Yale Blue)
|
|
styles: mapUrl ? { textColor: [40, 83, 107], fontStyle: 'bold' as const } : {}
|
|
};
|
|
|
|
tableRows.push([
|
|
stt++,
|
|
timeStr,
|
|
locIdx === 0 ? legName : '',
|
|
locationCellObj, // Injected as cell structure object
|
|
noteText
|
|
]);
|
|
globalRowIndex++;
|
|
});
|
|
} else {
|
|
legRowRanges[leg.id] = { start: startRow, count: 1 };
|
|
tableRows.push([
|
|
stt++,
|
|
'',
|
|
legName,
|
|
'Chưa có địa điểm trong chặng này',
|
|
''
|
|
]);
|
|
globalRowIndex++;
|
|
}
|
|
});
|
|
}
|
|
|
|
### Step 2: Inject Coordinate-Based Overlay inside doc.autoTable
|
|
Locate the core configuration module block where doc.autoTable({}) is invoked. Inject the didDrawCell event engine to deploy the link bounds:
|
|
|
|
doc.autoTable({
|
|
head: [['STT', 'Thời gian', 'Chặng', 'Địa điểm', 'Ghi chú']],
|
|
body: tableRows,
|
|
theme: 'grid',
|
|
|
|
// HOOK HANDLER: Overlays active coordinate zones on top of native cells after writing
|
|
didDrawCell: (data: any) => {
|
|
// Target explicitly: body section only + column index 3 (Location Column)
|
|
if (data.section === 'body' && data.column.index === 3) {
|
|
const activeRowIndex = data.row.index;
|
|
const targetMapUrl = pdfMapLinks[activeRowIndex];
|
|
|
|
if (targetMapUrl) {
|
|
// Build active link container utilizing jsPDF coordinates framework
|
|
data.doc.link(
|
|
data.cell.x,
|
|
data.cell.y,
|
|
data.cell.width,
|
|
data.cell.height,
|
|
{ url: targetMapUrl }
|
|
);
|
|
}
|
|
}
|
|
},
|
|
styles: { font: 'Roboto' } // Retain existing styling structure
|
|
});
|
|
|
|
## 4. Quality Control & Acceptance Criteria
|
|
[ ] Visual Differentiation: Location text cells containing map URLs must render cleanly in bold dark-blue ([40, 83, 107]) while empty state texts stay muted.
|
|
|
|
[ ] Coordinate Precision: Clicking a location card possessing explicit coordinates (latitude, longitude) must directly map to those absolute markers instead of doing an inaccurate keyword address query.
|
|
|
|
[ ] Boundary Accuracy: The click targets must fit perfectly inside the grid cell borders. Clicking near the edges of column 3 must register properly, while clicking column 2 (Leg name) or column 4 (Notes) must remain non-reactive.
|