// Utility function to get settings from the 'Settings' tab
function getSettings_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Settings');
if (!sheet) {
throw new Error('Settings sheet not found.');
}
const settings = {};
const dataRange = sheet.getRange(1, 1, sheet.getLastRow(), 2); // Read keys and values from the first two columns
const data = dataRange.getValues();
data.forEach(([key, value]) => {
if (key) {
settings[key.trim()] = value ? value.toString().trim() : '';
}
});
Logger.log('Settings Object:', settings); // Log settings for debugging
return settings;
}
// Extract the actual file ID from a Google Doc link or a folder link
function getActualFileId(link) {
const docRegex = /\/d\/([a-zA-Z0-9-_]+)/;
const folderRegex = /\/folders\/([a-zA-Z0-9-_]+)/;
const docMatch = link.match(docRegex);
const folderMatch = link.match(folderRegex);
if (docMatch) {
Logger.log(`Extracted File ID: ${docMatch[1]}`);
return docMatch[1];
} else if (folderMatch) {
Logger.log(`Extracted Folder ID: ${folderMatch[1]}`);
return folderMatch[1];
}
throw new Error(`Invalid Google Doc or folder link: ${link}`);
}
// Replace placeholders and create a PDF
function createPdfWithPlaceholders(templateLink, valuesMap) {
try {
const templateId = getActualFileId(templateLink);
Logger.log(`Using Template ID: ${templateId}`);
const template = DriveApp.getFileById(templateId);
const fileName = `Document for Signature`;
const copy = template.makeCopy(fileName);
const doc = DocumentApp.openById(copy.getId());
// Replace placeholders in the document
replacePlaceholders(doc, valuesMap);
// Save changes to the document
doc.saveAndClose();
Logger.log(`Placeholders replaced successfully in document ID: ${copy.getId()}`);
return { docId: copy.getId() };
} catch (error) {
Logger.log(`Error in createPdfWithPlaceholders: ${error.message}`);
throw error;
}
}
// Replace placeholders in the document
function replacePlaceholders(doc, valuesMap) {
const body = doc.getBody();
Object.entries(valuesMap).forEach(([placeholder, value]) => {
const searchPlaceholder = `{{${placeholder}}}`;
body.replaceText(searchPlaceholder, value);
Logger.log(`Replaced placeholder {{${placeholder}}} with value: ${value}`);
});
}
// Save signature to the PDF and send to signer
function saveSignatureWithPdf(docId, dataURL, row) {
try {
// Get settings and validate folder
const settings = getSettings_();
const folderId = getActualFileId(settings.signedFolderLink);
const folder = DriveApp.getFolderById(folderId);
Logger.log(`Saving signed PDF to folder ID: ${folderId}`);
// Decode the signature data URL into a blob
const signatureBlob = Utilities.newBlob(
Utilities.base64Decode(dataURL.split(',')[1]),
'image/png',
'signature.png'
);
Logger.log('Signature blob created successfully.');
// Open the document and append the signature
const doc = DocumentApp.openById(docId);
const body = doc.getBody();
if (!body) {
throw new Error('Unable to access the document body to append the signature.');
}
const paragraph = body.appendParagraph('\nSigned by:');
const img = paragraph.appendInlineImage(signatureBlob);
img.setWidth(200).setHeight(100);
Logger.log('Signature appended to the document.');
// Save changes to the document
doc.saveAndClose();
// Convert the signed document to a PDF
const updatedDoc = DriveApp.getFileById(docId);
if (!updatedDoc) {
throw new Error(`Unable to access the document with ID: ${docId}`);
}
const pdfBlob = updatedDoc.getAs('application/pdf');
const pdfFile = folder.createFile(pdfBlob.setName(`Signed Document.pdf`));
Logger.log(`Signed PDF created: ${pdfFile.getName()} (${pdfFile.getId()})`);
// Update the Form Responses sheet
const signedDocLink = pdfFile.getUrl();
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Form Responses 1');
if (!sheet) {
throw new Error('Form Responses sheet not found.');
}
sheet.getRange(row, 11).setValue('Signed'); // Column K: Status
sheet.getRange(row, 12).setValue(new Date().toLocaleDateString('en-IL')); // Column L: Date Signed
sheet.getRange(row, 13).setValue(signedDocLink); // Column M: Signed Document Link
SpreadsheetApp.flush(); // Ensure that the updates are applied immediately
Logger.log(`Spreadsheet updated with signed document link: ${signedDocLink}`);
// Send a copy of the signed document to the signer if configured
if (settings.sendCopyToSigner && settings.sendCopyToSigner.toLowerCase() === 'true') {
const emailIndex = parseInt(settings['email'], 10) - 1;
const email = sheet.getRange(row, emailIndex + 1).getValue();
if (email) {
MailApp.sendEmail({
to: email,
subject: 'Signed Document Copy',
body: 'Attached is a copy of your signed document.',
attachments: [pdfBlob]
});
Logger.log(`Sent a copy of the signed document to: ${email}`);
}
}
// Show popup to confirm successful signature
return HtmlService.createHtmlOutput('<script>alert("Signature saved successfully!"); google.script.host.close();</script>');
} catch (error) {
Logger.log(`Error in saveSignatureWithPdf: ${error.message}`);
throw error;
}
}
// Retry operation with retries and delays
function retryOperation(func, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return func();
} catch (error) {
Logger.log(`Retry ${i + 1} failed: ${error.message}`);
if (i === retries - 1) throw error; // Rethrow after final attempt
Utilities.sleep(delay); // Wait before retrying
}
}
}
// Handle form submission and send the signing link
function onFormSubmit(e) {
try {
const formRow = e.range.getRow();
const formValues = e.values;
const settings = getSettings_();
const emailIndex = parseInt(settings['email'], 10) - 1;
const templateLink = settings['templateLink'];
const email = formValues[emailIndex];
if (!templateLink) {
throw new Error('Template link is not specified in settings.');
}
// Prepare the PDF for signing
const valuesMap = getValuesMap_(formValues, settings);
const pdfDetails = createPdfWithPlaceholders(templateLink, valuesMap);
// Send the signing link via email
const subject = "בקשה לחתימה על מסמך";
const body = `
<div style="text-align: right; direction: rtl; font-family: Arial, sans-serif;">
שלום,<br><br>
אנא לחצו על הקישור הבא כדי לצפות ולחתום על המסמך שלכם:<br>
<a href="https://script.google.com/macros/s/AKfycbz2r5A5p6kjxyBLJHNG38tBFX99gqO7Aixikm29eCGXtRhYFXrRdp6ZSr5DXx_RvHf3mA/exec?id=${pdfDetails.docId}&row=${formRow}">לחצו כאן כדי לחתום</a><br><br>
תודה,<br>
צוות הניהול
</div>
`;
MailApp.sendEmail({
to: email,
subject: subject,
htmlBody: body,
});
Logger.log(`Email sent to ${email} with signing link.`);
} catch (error) {
Logger.log(`Error on form submission: ${error.message}`);
}
}
// Web app for capturing signature
function doGet(e) {
const { id, row } = e.parameter;
if (!id || !row) {
return HtmlService.createHtmlOutput('<h2>Invalid request. Missing parameters. Please check the link provided.</h2>');
}
// Construct the PDF preview URL using the Google Drive embedded view link
const pdfUrl = `https://drive.google.com/file/d/${id}/preview`;
const html = `
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
}
.iframe-wrapper {
position: relative;
width: 600px; /* Fixed iframe width */
height: 800px; /* Fixed iframe height */
overflow: hidden;
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.iframe-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%; /* Fit container width */
height: 100%; /* Fit container height */
transform: scale(1); /* Ensure scaling is uniform */
transform-origin: top left; /* Ensure scaling origin */
border: none; /* Remove default iframe border */
}
canvas {
border: 1px solid #000;
margin: 20px auto;
display: block;
}
#signature-options {
margin: 10px 0;
}
footer {
margin-top: 20px;
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<h2>View and Sign the Document</h2>
<div class="iframe-wrapper">
<iframe src="${pdfUrl}" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>
</div>
<h3>Signature Pad</h3>
<canvas id="signature-pad" width="600" height="150"></canvas>
<div id="signature-options">
<button onclick="clearSignature()">Clear</button>
<button onclick="saveSignature()">Save Signature</button>
</div>
<footer>Built by Meir Horwitz - meir...@gmail.com</footer>
<script>
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
let drawing = false;
canvas.addEventListener('mousedown', () => (drawing = true));
canvas.addEventListener('mouseup', () => (drawing = false));
canvas.addEventListener('mousemove', draw);
function draw(e) {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
}
function clearSignature() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function saveSignature() {
const dataURL = canvas.toDataURL();
google.script.run.withSuccessHandler(() => {
alert('Signature saved successfully!');
window.close();
}).saveSignatureWithPdf('${id}', dataURL, '${row}');
}
</script>
</body>
</html>
`;
return HtmlService.createHtmlOutput(html).setTitle('Document Signing');
}
// Get values map from form values and settings
function getValuesMap_(formValues, settings) {
const valuesMap = {};
Object.keys(settings).forEach((key) => {
if (key !== 'webAppUrl' && key !== 'signedFolderLink' && key.indexOf('Template') === -1) {
const index = parseInt(settings[key], 10) - 1;
if (!isNaN(index) && formValues[index]) {
valuesMap[key] = formValues[index];
}
}
});
Logger.log('Values Map:', valuesMap);
return valuesMap;
}