Files

6.0 KiB

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:

// 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.