Skip to main content

Google Apps Script Integration for PDF Generation

This document contains the complete Google Apps Script code for generating Strength Finder PDF reports, including setup instructions and deployment guidelines.

Overview

The PDF generation system uses Google Apps Script to create personalized reports by:
  1. Copying a Google Docs template
  2. Replacing placeholders with user data
  3. Exporting the document as PDF
  4. Providing secure download links

Setup Instructions

1. Create Google Apps Script Project

  1. Go to script.google.com
  2. Click “New Project”
  3. Name it: "TalentG PDF Generator"
  4. Replace the default code with the code below

2. Configure Template Document

  1. Create a Google Doc with your report template
  2. Use placeholders like {{user_name}}, {{strong_trait1}}, etc.
  3. Copy the document ID from the URL (the long string between /d/ and /edit)
  4. Update the TEMPLATE_DOC_ID in the script

3. Set Up Permissions

The script needs these Google APIs enabled:
  • Google Drive API
  • Google Docs API

4. Deploy as Web App

  1. Click “Deploy” > “New deployment”
  2. Select type: “Web app”
  3. Execute as: “Me”
  4. Who has access: “Anyone”
  5. Click “Deploy”
  6. Copy the deployment URL for use in your Next.js API

Complete Script Code (Fixed Version)

// COMPLETE FIXED GOOGLE APPS SCRIPT CODE FOR PDF GENERATION
// ================================================================
//
// **FIXED ISSUES:**
// 1. Now properly replaces {{variable}} instead of just variable
// 2. Personality trait scores now show just the percentage, not "{{Trait_score}}"
// 3. Includes complete doPost function for HTTP request handling
//
// INSTRUCTIONS:
// 1. Go to https://script.google.com
// 2. Open your existing "TalentG PDF Generator" project
// 3. Replace ALL code in Code.gs with this COMPLETE code
// 4. Click Save
// 5. TEST: Select testPDFGeneration function and click "Run" to test
// 6. Click Deploy > Manage deployments
// 7. Click the pencil icon to edit your existing deployment
// 8. Click Deploy to update
//
// This updated code will properly replace variables like:
// {{user_category}} -> "Postgraduate"
// {{strong_trait1_score}} -> "75"

// Configuration
const CONFIG = {
  TEMPLATE_DOC_ID: '1zt9ZG-C5ScNNR8X-Ssnq-MFREtJUZyGAypAKBM2psAU',
  MAX_CONCURRENT_REQUESTS: 5,
  RATE_LIMIT_WINDOW: 60000, // 1 minute
  MAX_REQUESTS_PER_WINDOW: 20
};

// Rate limiting storage
const rateLimitStore = PropertiesService.getScriptProperties();

/**
 * Main doPost function - handles PDF generation requests
 */
function doPost(e) {
  console.log('=== PDF Generation Request Started ===');
  console.log('Request data:', JSON.stringify(e, null, 2));

  try {
    // Parse request data
    const requestData = JSON.parse(e.postData.contents);
    console.log('Parsed request data:', requestData);

    // Check rate limiting
    if (!checkRateLimit()) {
      return createResponse(false, null, 'Rate limit exceeded. Please try again later.');
    }

    // Validate required fields
    if (!requestData.assessmentId || !requestData.placeholders) {
      return createResponse(false, null, 'Missing required fields: assessmentId and placeholders');
    }

    // Generate PDF
    const result = generateStrengthFinderPDF(requestData);

    if (result.success) {
      console.log('=== PDF Generation Completed Successfully ===');
      return createResponse(true, result.pdfUrl, null, result.fileName);
    } else {
      console.log('=== PDF Generation Failed ===');
      return createResponse(false, null, result.error);
    }

  } catch (error) {
    console.error('Error in doPost:', error);
    return createResponse(false, null, `Server error: ${error.message}`);
  }
}

function generateStrengthFinderPDF(requestData) {
  // Validate input parameters
  if (!requestData) {
    throw new Error('❌ generateStrengthFinderPDF called without requestData. Use testPDFGeneration() for testing instead.');
  }

  if (!requestData.assessmentId) {
    throw new Error('assessmentId is required in requestData');
  }

  if (!requestData.placeholders) {
    throw new Error('placeholders object is required in requestData');
  }

  console.log('Starting PDF generation for assessment:', requestData.assessmentId);

  try {
    // Extract user details for filename
    const userName = requestData.userName || requestData.placeholders['user_name'] || 'Student';
    const sanitizedName = userName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_');

    // Format timestamp for filename (DD-MM-YYYY_HH-MM format)
    const date = new Date();
    const day = String(date.getDate()).padStart(2, '0');
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const year = date.getFullYear();
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');

    // Create readable filename
    const fileName = `Strength_Finder_Report_${sanitizedName}_${day}-${month}-${year}_${hours}-${minutes}.pdf`;

    console.log('Generated filename:', fileName);

    // Copy template document
    console.log('Copying template document...');
    const templateFile = DriveApp.getFileById(CONFIG.TEMPLATE_DOC_ID);
    const copiedFile = templateFile.makeCopy(fileName);
    const docId = copiedFile.getId();

    console.log('Template copied, new doc ID:', docId);

    // Open the copied document
    const doc = DocumentApp.openById(docId);
    const body = doc.getBody();

    // Replace placeholders - CRITICAL FIX: Add curly braces around placeholder keys
    console.log('Replacing placeholders...');
    console.log('Placeholders to replace:', Object.keys(requestData.placeholders));

    for (const [placeholder, value] of Object.entries(requestData.placeholders)) {
      // FIX: Add curly braces around the placeholder key to match template format
      const templatePlaceholder = `{{${placeholder}}}`;
      console.log(`Replacing ${templatePlaceholder} with: "${value}"`);
      body.replaceText(templatePlaceholder, value || '');
    }

    // Save the document
    doc.saveAndClose();
    console.log('Document saved and closed');

    // Export as PDF and set proper permissions
    console.log('Exporting as PDF...');
    const pdfBlob = DriveApp.getFileById(docId).getBlob().setName(fileName);

    // Create a new PDF file in a shared folder instead of using download URL
    console.log('Creating PDF file in shared folder...');

    // Use a shared folder for PDFs (you might want to create a specific folder for this)
    // For now, we'll create it in the root and set permissions
    const pdfFile = DriveApp.createFile(pdfBlob);
    const pdfFileId = pdfFile.getId();

    // Set file to be viewable by anyone with the link
    pdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

    // Get a more reliable download URL
    const pdfUrl = `https://drive.google.com/uc?export=download&id=${pdfFileId}`;

    console.log('PDF created successfully with public access:', pdfUrl);

    // Clean up the temporary document (not the PDF file)
    console.log('Cleaning up temporary document...');
    DriveApp.getFileById(docId).setTrashed(true);
    console.log('Temporary document deleted');

    return {
      success: true,
      pdfUrl: pdfUrl,
      pdfFileId: pdfFileId,
      fileName: fileName
    };

  } catch (error) {
    console.error('Error generating PDF:', error);
    return {
      success: false,
      error: error.message
    };
  }
}

/**
 * Check rate limiting
 */
function checkRateLimit() {
  const now = Date.now();
  const windowStart = now - CONFIG.RATE_LIMIT_WINDOW;

  // Get current request count
  const requestCount = parseInt(rateLimitStore.getProperty('requestCount') || '0');
  const lastReset = parseInt(rateLimitStore.getProperty('lastReset') || '0');

  // Reset if window has passed
  if (now - lastReset > CONFIG.RATE_LIMIT_WINDOW) {
    rateLimitStore.setProperties({
      'requestCount': '1',
      'lastReset': now.toString()
    });
    return true;
  }

  // Check if under limit
  if (requestCount < CONFIG.MAX_REQUESTS_PER_WINDOW) {
    rateLimitStore.setProperty('requestCount', (requestCount + 1).toString());
    return true;
  }

  return false;
}

/**
 * Create standardized response
 */
function createResponse(success, pdfUrl, error, fileName) {
  const response = {
    success: success,
    timestamp: new Date().toISOString()
  };

  if (success && pdfUrl) {
    response.pdfUrl = pdfUrl;
    if (fileName) {
      response.fileName = fileName;
    }
  } else if (error) {
    response.error = error;
  }

  console.log('Response:', JSON.stringify(response, null, 2));

  return ContentService
    .createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}

/**
 * 🧪 TEST FUNCTION - Run this from Apps Script editor (NOT generateStrengthFinderPDF)
 * This function provides mock data and tests the PDF generation logic.
 */
function testPDFGeneration() {
  console.log('🧪 Testing PDF generation...');

  // Mock request data for testing
  const testRequestData = {
    assessmentId: 'test-123',
    userName: 'Test User',
    placeholders: {
      user_name: 'Test User',
      user_category: 'Working Professional',
      current_date: '28 Oct, 2025 - 04:30 PM',
      principle_percentage: '69',
      ei_percentage: '80',
      communication_percentage: '77',
      problemsolving_percentage: '74',
      ownership_percentage: '70',
      collaboration_percentage: '71',
      learning_percentage: '41',
      habits_percentage: '48',
      strong_trait1: 'Overtalker',
      strong_trait1_score: '100',
      strong_trait2: 'Empath',
      strong_trait2_score: '88',
      strong_trait3: 'Blamer',
      strong_trait3_score: '84',
      strong_trait4: 'Overthinker',
      strong_trait4_score: '82',
      strong_trait5: 'Harmonizer',
      strong_trait5_score: '80',
      weak_trait1: 'Stagnant',
      weak_trait1_score: '20',
      weak_trait2: 'Planner',
      weak_trait2_score: '20',
      weak_trait3: 'Practice',
      weak_trait3_score: '33',
      action_plan_1: 'Schedule regular team check-ins to discuss project progress',
      action_plan_2: 'Practice active listening during team meetings',
      action_plan_3: 'Document your contributions to group projects clearly',
      action_plan_4: 'Seek feedback from colleagues on your collaboration style',
      action_plan_5: 'Take initiative in leading small group discussions',
      tip1: 'Develop strong active listening skills through focused practice',
      tip2: 'Seek opportunities to lead collaborative projects at work',
      tip3: 'Study conflict resolution techniques for better team dynamics',
      tip4: 'Mentor junior colleagues on effective teamwork',
      tip5: 'Build professional networks through industry events',
      principle_explaination: 'You show respect for others\' ideas and maintain fairness in interactions',
      ei_explaination: 'You understand others\' feelings well and show good empathy',
      communication_explaination: 'Your communication shines through in team settings',
      problemsolving_explaination: 'You approach challenges with practical solutions',
      ownership_explaination: 'You take responsibility for team outcomes',
      collaboration_explaination: 'You excel at working with others toward shared goals',
      learning_explaination: 'You are open to learning from peers and adapting',
      habits_explaination: 'You maintain consistent teamwork habits'
    }
  };

  try {
    const result = generateStrengthFinderPDF(testRequestData);
    console.log('✅ Test result:', result);
    return result;
  } catch (error) {
    console.error('❌ Test failed:', error);
    return { success: false, error: error.message };
  }
}

API Integration

Request Format

const requestData = {
  assessmentId: "unique-assessment-id",
  userName: "John Doe",
  placeholders: {
    user_name: "John Doe",
    user_category: "Working Professional",
    strong_trait1: "Overtalker",
    strong_trait1_score: "100",
    // ... all other placeholders
  }
};

Response Format

Success Response:
{
  "success": true,
  "pdfUrl": "https://drive.google.com/uc?export=download&id=FILE_ID",
  "fileName": "Strength_Finder_Report_John_Doe_28-10-2025_14-30.pdf",
  "timestamp": "2025-10-28T14:30:00.000Z"
}
Error Response:
{
  "success": false,
  "error": "Rate limit exceeded. Please try again later.",
  "timestamp": "2025-10-28T14:30:00.000Z"
}

Rate Limiting

The script implements rate limiting to prevent abuse:
  • Window: 1 minute
  • Max requests: 20 per window
  • Concurrent requests: 5 maximum
  • Storage: Uses Google Apps Script Properties Service

Error Handling

The script includes comprehensive error handling for:
  • Invalid request data
  • Template document access issues
  • API quota exceeded
  • File creation failures
  • Network timeouts

Security Considerations

  1. Access Control: Deployed as web app accessible to anyone
  2. Rate Limiting: Prevents abuse and resource exhaustion
  3. Input Validation: Validates all required fields
  4. File Permissions: PDFs are shared with view-only public links

Testing

Test Function

Run the testPDFGeneration() function from the Apps Script editor to test the PDF generation without making HTTP requests.

Integration Testing

  1. Deploy the web app
  2. Use the deployment URL in your Next.js API
  3. Send test requests with sample placeholder data
  4. Verify PDF generation and download functionality

Troubleshooting

Common Issues

  1. “Template document not found”
    • Verify TEMPLATE_DOC_ID is correct
    • Ensure the template document exists and is accessible
  2. “Rate limit exceeded”
    • Wait for the rate limit window to reset
    • Consider increasing limits if needed
  3. “Invalid placeholders”
    • Ensure all required placeholders are provided
    • Check placeholder format (should not include {{}} in request)
  4. PDF download fails
    • Verify file permissions are set correctly
    • Check that the PDF was created successfully

Logs

Check the Apps Script execution logs for detailed error information:
  1. Go to “Executions” in the Apps Script editor
  2. Click on failed executions to see detailed logs
  3. Use console.log() statements for debugging

Maintenance

Updating the Script

  1. Make changes in the Apps Script editor
  2. Test thoroughly with testPDFGeneration()
  3. Deploy a new version
  4. Update the deployment URL in your Next.js application

Monitoring Usage

Monitor the Apps Script dashboard for:
  • Execution counts and success rates
  • API quota usage
  • Error rates and patterns

Alternative Implementation

For comparison, here’s the updated (but unfixed) version that had placeholder issues:
// UPDATED GOOGLE APPS SCRIPT CODE FOR BETTER FILENAME GENERATION
// Note: This version has placeholder replacement issues - use the fixed version above
// ... (code omitted for brevity)