Last active
August 29, 2025 14:49
-
-
Save pritamsharma45/3aa38433ab043dad3c31f8be5b9c767b to your computer and use it in GitHub Desktop.
Data entry with multiple files upload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const DATA_ENTRY_SHEET_NAME = "Sheet1"; | |
| const TIME_STAMP_COLUMN_NAME = "Timestamp"; | |
| const FOLDER_ID = ""; | |
| // == IMAGE UPLOAD CONFIGURATION == | |
| // Add or edit image column names here - this is the only place you need to modify | |
| const IMAGE_COLUMNS = [ | |
| { name: "before", label: "Before Image" }, | |
| { name: "on process", label: "On Process Image" }, | |
| { name: "after", label: "After Image" }, | |
| ]; | |
| // == CONFIGURATION END == | |
| function doPost(e) { | |
| try { | |
| const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(DATA_ENTRY_SHEET_NAME); | |
| if (!sheet) { | |
| throw new Error(`Sheet '${DATA_ENTRY_SHEET_NAME}' not found`); | |
| } | |
| const formData = e.postData.contents ? JSON.parse(e.postData.contents) : {}; | |
| // Handle multiple image uploads if present | |
| let imageInfo = {}; | |
| if (formData.imageData) { | |
| // Process each image type based on IMAGE_COLUMNS configuration | |
| IMAGE_COLUMNS.forEach((column) => { | |
| if (formData.imageData[column.name]) { | |
| imageInfo[column.name] = saveFile(formData.imageData[column.name]); | |
| } | |
| }); | |
| delete formData.imageData; // Remove image data from form data | |
| } | |
| // Prepare data for sheet | |
| const rowData = { | |
| ...formData, | |
| [TIME_STAMP_COLUMN_NAME]: new Date().toISOString(), | |
| }; | |
| // Add image information to row data using HYPERLINK formula | |
| IMAGE_COLUMNS.forEach((column) => { | |
| if (imageInfo[column.name]) { | |
| // Use HYPERLINK formula for clickable links in Google Sheets | |
| // Use the actual filename as the display text instead of the label | |
| rowData[column.name] = `=HYPERLINK("${imageInfo[column.name].url}","${imageInfo[column.name].name}")`; | |
| } | |
| }); | |
| appendToGoogleSheet(rowData, sheet); | |
| return ContentService.createTextOutput( | |
| JSON.stringify({ | |
| status: "success", | |
| message: "Data submitted successfully", | |
| }) | |
| ).setMimeType(ContentService.MimeType.JSON); | |
| } catch (error) { | |
| console.error(error); | |
| return ContentService.createTextOutput( | |
| JSON.stringify({ | |
| status: "error", | |
| message: error.toString(), | |
| }) | |
| ).setMimeType(ContentService.MimeType.JSON); | |
| } | |
| } | |
| /** | |
| * Saves a file to Google Drive | |
| */ | |
| function saveFile(fileData) { | |
| try { | |
| const blob = Utilities.newBlob(Utilities.base64Decode(fileData.data), fileData.mimeType, fileData.fileName); | |
| const folder = DriveApp.getFolderById(FOLDER_ID); | |
| const file = folder.createFile(blob); | |
| file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); | |
| return { | |
| url: `https://drive.google.com/uc?export=view&id=${file.getId()}`, | |
| name: fileData.fileName, | |
| }; | |
| } catch (error) { | |
| console.error("File upload error:", error); | |
| throw new Error("Failed to upload file: " + error.toString()); | |
| } | |
| } | |
| /** | |
| * Appends data to the Google Sheet | |
| */ | |
| function appendToGoogleSheet(data, sheet) { | |
| let headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; | |
| // If sheet is empty, create headers | |
| if (headers.length === 0 || headers[0] === "") { | |
| const newHeaders = Object.keys(data); | |
| sheet.getRange(1, 1, 1, newHeaders.length).setValues([newHeaders]); | |
| headers = newHeaders; | |
| } else { | |
| // Check if we need to add new columns for the new image fields | |
| const existingHeaders = new Set(headers); | |
| const newColumns = []; | |
| // Add new image columns if they don't exist | |
| IMAGE_COLUMNS.forEach((column) => { | |
| if (!existingHeaders.has(column.name)) { | |
| newColumns.push(column.name); | |
| } | |
| }); | |
| // Add new columns to the sheet if needed | |
| if (newColumns.length > 0) { | |
| const lastCol = sheet.getLastColumn(); | |
| sheet.getRange(1, lastCol + 1, 1, newColumns.length).setValues([newColumns]); | |
| headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; | |
| } | |
| } | |
| // Map data to header columns | |
| const rowData = headers.map((header) => data[header] || ""); | |
| sheet.appendRow(rowData); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" /> | |
| <title>Data Entry Form with File Upload</title> | |
| </head> | |
| <body> | |
| <section class="hero is-primary is-bold"> | |
| <div class="hero-body"> | |
| <div class="container"> | |
| <h1 class="title">Data Entry Form</h1> | |
| </div> | |
| </div> | |
| </section> | |
| <form id="form" class="container m-4 pl-4"> | |
| <div class="field"> | |
| <label class="label">Client Name</label> | |
| <div class="control"> | |
| <input class="input" type="text" placeholder="Your Client Name" name="Client Name" /> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Email</label> | |
| <div class="control"> | |
| <input class="input" type="email" placeholder="Your Email" name="Email" /> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Password</label> | |
| <div class="control"> | |
| <input class="input" type="password" placeholder="Your Password" name="Password" /> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Date of Birth</label> | |
| <div class="control"> | |
| <input class="input" type="date" placeholder="Your Date of Birth" name="DOB" /> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Gender</label> | |
| <div class="control"> | |
| <label class="radio"> <input type="radio" name="Gender" value="male" /> Male </label> | |
| <label class="radio"> <input type="radio" name="Gender" value="female" /> Female </label> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Image Uploads</label> | |
| <div class="control" id="imageUploadsContainer"> | |
| <!-- Image upload fields will be dynamically created here --> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Agree to Terms</label> | |
| <div class="control"> | |
| <label class="checkbox"> <input type="checkbox" name="Agree To Terms" value="yes" /> I agree to the terms and conditions </label> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <label class="label">Additional Information</label> | |
| <div class="control"> | |
| <textarea class="textarea" placeholder="Any additional information" name="Notes"></textarea> | |
| </div> | |
| </div> | |
| <div class="field is-grouped"> | |
| <div class="control"> | |
| <button class="button is-primary" type="submit" id="submit-button">Sign Up</button> | |
| </div> | |
| <div class="control"> | |
| <button class="button is-danger" type="button" onclick="document.getElementById('form').reset(); setupImageInputListeners();">Cancel</button> | |
| </div> | |
| </div> | |
| </form> | |
| <div id="message" style="display: none; margin: 20px; padding: 10px; border-radius: 4px; font-weight: bold"></div> | |
| <script> | |
| // == IMAGE UPLOAD CONFIGURATION == | |
| // Add or edit image upload fields here - this should match the IMAGE_COLUMNS in code.js | |
| const IMAGE_UPLOAD_CONFIG = [ | |
| { name: "before", label: "Before Image", placeholder: "Choose before image…" }, | |
| { name: "on process", label: "On Process Image", placeholder: "Choose on process image…" }, | |
| { name: "after", label: "After Image", placeholder: "Choose after image…" }, | |
| ]; | |
| // == CONFIGURATION END == | |
| const form = document.getElementById("form"); | |
| const submitButton = document.getElementById("submit-button"); | |
| const messageDiv = document.getElementById("message"); | |
| // Dynamically create image upload fields based on configuration | |
| function createImageUploadFields() { | |
| const imageFieldsContainer = document.getElementById("imageUploadsContainer"); | |
| // Clear existing content | |
| imageFieldsContainer.innerHTML = ""; | |
| // Create new image upload fields | |
| IMAGE_UPLOAD_CONFIG.forEach((config) => { | |
| const fieldDiv = document.createElement("div"); | |
| fieldDiv.className = "field"; | |
| fieldDiv.innerHTML = ` | |
| <label class="label">${config.label}</label> | |
| <div class="control"> | |
| <div class="file has-name is-fullwidth"> | |
| <label class="file-label"> | |
| <input class="file-input" type="file" name="${config.name}" id="${config.name}Input" accept="image/*" /> | |
| <span class="file-cta"> | |
| <span class="file-icon"> | |
| <i class="fas fa-upload"></i> | |
| </span> | |
| <span class="file-label"> ${config.placeholder} </span> | |
| </span> | |
| <span class="file-name" id="${config.name}Display"> No file selected </span> | |
| </label> | |
| </div> | |
| </div> | |
| `; | |
| imageFieldsContainer.appendChild(fieldDiv); | |
| }); | |
| } | |
| // Initialize image upload fields | |
| createImageUploadFields(); | |
| // Get references to dynamically created elements | |
| function getImageInputs() { | |
| const inputs = {}; | |
| IMAGE_UPLOAD_CONFIG.forEach((config) => { | |
| inputs[config.name] = { | |
| input: document.getElementById(`${config.name}Input`), | |
| display: document.getElementById(`${config.name}Display`), | |
| }; | |
| }); | |
| return inputs; | |
| } | |
| // Add event listeners to image inputs | |
| function setupImageInputListeners() { | |
| const imageInputs = getImageInputs(); | |
| IMAGE_UPLOAD_CONFIG.forEach((config) => { | |
| const { input, display } = imageInputs[config.name]; | |
| input.addEventListener("change", function () { | |
| if (this.files && this.files.length > 0) { | |
| display.textContent = this.files[0].name; | |
| } else { | |
| display.textContent = "No file selected"; | |
| } | |
| }); | |
| }); | |
| } | |
| // Setup listeners after fields are created | |
| setupImageInputListeners(); | |
| // Function to handle file upload | |
| async function uploadFile(file) { | |
| return new Promise((resolve, reject) => { | |
| const fr = new FileReader(); | |
| fr.onload = (e) => { | |
| const data = e.target.result.split(","); | |
| const obj = { | |
| fileName: file.name, | |
| mimeType: data[0].match(/:(\w.+);/)[1], | |
| data: data[1], | |
| }; | |
| resolve(obj); | |
| }; | |
| fr.onerror = reject; | |
| fr.readAsDataURL(file); | |
| }); | |
| } | |
| form.addEventListener("submit", async function (e) { | |
| e.preventDefault(); | |
| messageDiv.textContent = "Submitting..."; | |
| messageDiv.style.display = "block"; | |
| messageDiv.style.backgroundColor = "beige"; | |
| messageDiv.style.color = "black"; | |
| submitButton.disabled = true; | |
| submitButton.classList.add("is-loading"); | |
| try { | |
| const formData = new FormData(this); | |
| const formDataObj = {}; | |
| // Convert FormData to object | |
| for (let [key, value] of formData.entries()) { | |
| formDataObj[key] = value; | |
| } | |
| // Handle image uploads if files are selected | |
| const imageData = {}; | |
| const imageInputs = getImageInputs(); | |
| for (const config of IMAGE_UPLOAD_CONFIG) { | |
| const input = imageInputs[config.name].input; | |
| if (input.files && input.files.length > 0) { | |
| const imageObj = await uploadFile(input.files[0]); | |
| imageData[config.name] = imageObj; | |
| } | |
| } | |
| if (Object.keys(imageData).length > 0) { | |
| formDataObj.imageData = imageData; | |
| } | |
| const scriptURL = "https://script.google.com/macros/s/AKfycbw0l6Dq_FSPD2rPp8QFOYT8QQw3NoD4LoZg7WBzy9MwsC6aSklQqMQD6dOIHlKt9IZm4Q/exec"; | |
| const response = await fetch(scriptURL, { | |
| redirect: "follow", | |
| method: "POST", | |
| body: JSON.stringify(formDataObj), | |
| headers: { | |
| "Content-Type": "text/plain;charset=utf-8", | |
| }, | |
| }); | |
| const data = await response.json(); | |
| if (data.status === "success") { | |
| messageDiv.textContent = data.message || "Data submitted successfully!"; | |
| messageDiv.style.backgroundColor = "#48c78e"; | |
| messageDiv.style.color = "white"; | |
| form.reset(); | |
| setupImageInputListeners(); | |
| } else { | |
| throw new Error(data.message || "Submission failed"); | |
| } | |
| } catch (error) { | |
| console.error("Error:", error); | |
| messageDiv.textContent = "Error: " + error.message; | |
| messageDiv.style.backgroundColor = "#f14668"; | |
| messageDiv.style.color = "white"; | |
| } finally { | |
| submitButton.disabled = false; | |
| submitButton.classList.remove("is-loading"); | |
| setTimeout(() => { | |
| messageDiv.textContent = ""; | |
| messageDiv.style.display = "none"; | |
| }, 4000); | |
| } | |
| }); | |
| const cancelButton = form.querySelector("button.is-danger"); | |
| cancelButton.addEventListener("click", function () { | |
| form.reset(); | |
| setupImageInputListeners(); | |
| messageDiv.style.display = "none"; | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment