// ========================================================= // מנוע סקרים אוניברסלי מבוסס גיליון - גרסה משופרת, נקייה וידידותית למשתמש // כולל שיפורי ביצועים (Batch), טיפול בסקייל (TextFinder) ומניעת קריסות // ========================================================= /** * יצירת תפריט מותאם אישית ברגע שפותחים את הגיליון */ function onOpen() { SpreadsheetApp.getUi().createMenu('מערכת סקרים 📊') .addItem('1. התקנה ועיצוב אוטומטי 🏗️', 'setupSystem') .addSeparator() .addItem('2. הוסף מילון לסקר חדש ➕', 'addNewPollDictionary') .addSeparator() .addItem('3. שלח קמפיין לרשימת התפוצה 🚀', 'triggerSendEmails') .addSeparator() .addItem('4. הפק דשבורד גרפים 📈', 'generateGraphs') .addToUi(); } // --------------------------------------------------------- // פונקציית התקנה אוטומטית - בונה, מעצבת ומנגישה את המערכת // --------------------------------------------------------- function setupSystem() { const ss = SpreadsheetApp.getActiveSpreadsheet(); // --- 1. גיליון הצבעות --- let sheetVotes = ss.getSheetByName("הצבעות"); if (!sheetVotes) sheetVotes = ss.insertSheet("הצבעות"); sheetVotes.setTabColor("#34a853"); // צבע ירוק if (sheetVotes.getLastRow() === 0) { sheetVotes.getRange("A1:E1").setValues([["מייל הבוחר", "מזהה סקר", "הצבעה", "תאריך ושעה", "דפדפן/מכשיר"]]) .setFontWeight("bold").setBackground("#d9ead3").setBorder(true, true, true, true, true, true); sheetVotes.setFrozenRows(1); sheetVotes.setColumnWidths(1, 5, 150); } // --- 2. רשימת תפוצה --- let sheetList = ss.getSheetByName("רשימת_תפוצה"); if (!sheetList) sheetList = ss.insertSheet("רשימת_תפוצה"); sheetList.setTabColor("#4285f4"); // צבע כחול if (sheetList.getLastRow() === 0) { sheetList.getRange("A1:C1").setValues([["כתובת מייל (חובה)", "שם הנמען (אופציונלי)", "סטטוס שליחה"]]) .setFontWeight("bold").setBackground("#c9daf8").setBorder(true, true, true, true, true, true); sheetList.setFrozenRows(1); sheetList.setColumnWidths(1, 3, 200); // דוגמה למילוי sheetList.getRange("A2:B2").setValues([["example@gmail.com", "ישראל ישראלי"]]); } // --- 3. גיליון הגדרות --- let sheetSettings = ss.getSheetByName("הגדרות"); if (!sheetSettings) sheetSettings = ss.insertSheet("הגדרות"); sheetSettings.setTabColor("#fbbc04"); // צבע כתום if (sheetSettings.getLastRow() === 0) { // תבנית HTML ברירת מחדל מעוצבת ומוכנה מראש const defaultHtml = `

שלום {{NAME}}, נשמח לשמוע את דעתך!

אנא לחץ על התשובה המתאימה לכל שאלה. שימו לב: לאחר כל לחיצה, אנא חזרו למייל כדי לענות על השאלה הבאה.

1. איך היית מדרג את השירות הכללי שקיבלת?

מצוין בינוני טעון שיפור
`; // הנתונים להזנה const settingsData = [ ["🛠️ הגדרות כלליות", "הזן את הערכים בעמודה זו 👇", ""], ["כתובת השרת (Web App URL)\n*חובה לייצר פריסה ולהדביק כאן", "", ""], ["נושא המייל", "סקר שביעות רצון - נשמח לדעתך", ""], ["הודעת הצלחה (בסיום ההצבעה)", "תודה רבה! ההצבעה נקלטה בהצלחה. תוכלו כעת לסגור חלון זה ולחזור למייל כדי לענות על שאר השאלות.", ""], ["הודעת שגיאה (לחיצה כפולה)", "שמנו לב שכבר הצבעת בשאלה זו. אנא חזרו למייל להמשך המענה.", ""], ["כתובת הפניה (URL) - אופציונלי\n*אם מלא, יעביר את הלקוח לאתר שלך במקום הודעה", "", ""], ["", "", ""], ["✉️ תבנית מייל (קוד HTML)", defaultHtml, ""], ["", "", ""], ["🔠 מזהה סקר (poll_id)", "🔢 קוד (vote)", "📝 טקסט מתורגם"], ["כללי", "1", "מצוין"], ["כללי", "2", "בינוני"], ["כללי", "3", "טעון שיפור"] ]; // הכנסת הנתונים לתאים ב-3 עמודות sheetSettings.getRange(1, 1, settingsData.length, 3).setValues(settingsData); // מיזוג תאים כדי שההגדרות העליונות ייראו טוב sheetSettings.getRange("B1:C1").merge(); sheetSettings.getRange("B2:C2").merge(); sheetSettings.getRange("B3:C3").merge(); sheetSettings.getRange("B4:C4").merge(); sheetSettings.getRange("B5:C5").merge(); sheetSettings.getRange("B6:C6").merge(); sheetSettings.getRange("B8:C8").merge(); // עיצוב הגיליון החדש sheetSettings.setColumnWidth(1, 200); sheetSettings.setColumnWidth(2, 200); sheetSettings.setColumnWidth(3, 300); // עיצוב כותרות sheetSettings.getRange("A1:C1").setFontWeight("bold").setBackground("#ffd966").setFontSize(14).setBorder(true, true, true, true, null, null); sheetSettings.getRange("A10:C10").setFontWeight("bold").setBackground("#ffd966").setFontSize(14).setBorder(true, true, true, true, null, null); // עיצוב שורת ה-HTML sheetSettings.getRange("A8").setFontWeight("bold").setBackground("#e0e0e0"); const htmlCell = sheetSettings.getRange("B8"); htmlCell.setWrap(true).setBackground("#f8f9fa").setFontFamily("Courier New"); sheetSettings.setRowHeight(8, 200); // עידון הטקסט והגבולות sheetSettings.getRange("A2:A6").setFontWeight("bold"); sheetSettings.getRange("A2:C6").setBorder(true, true, true, true, true, true); sheetSettings.getRange("A11:C13").setBorder(true, true, true, true, true, true); } // --- 4. ניקוי גיליונות ריקים --- const defaultSheet = ss.getSheetByName("גיליון1") || ss.getSheetByName("Sheet1"); if (defaultSheet && ss.getSheets().length > 1) { ss.deleteSheet(defaultSheet); } SpreadsheetApp.getUi().alert( "ההתקנה הושלמה בהצלחה! 🎉", "1. הגיליונות סודרו ונצבעו לנוחותך.\n2. נוצרה תבנית מייל אוטומטית בגיליון 'הגדרות'.\n3. כדי להתחיל, עליך להפיק קישור לפריסה (Deploy) ולהדביק אותו בתא B2 בגיליון ההגדרות.", SpreadsheetApp.getUi().ButtonSet.OK ); } // --------------------------------------------------------- // פונקציית מעטפת לשליחת המיילים (כולל חלונית אישור ובדיקות) // --------------------------------------------------------- function triggerSendEmails() { const ui = SpreadsheetApp.getUi(); const ss = SpreadsheetApp.getActiveSpreadsheet(); const settingsSheet = ss.getSheetByName("הגדרות"); const serverUrl = settingsSheet.getRange("B2").getValue(); if (!serverUrl || serverUrl.trim() === "") { ui.alert('⚠️ שגיאה לפני שליחה', 'לא הזנת את "כתובת השרת" בגיליון ההגדרות (תא B2).\nיש לבצע פריסה (Deploy -> Web App) ולהדביק את הקישור שם.', ui.ButtonSet.OK); return; } const response = ui.alert('האם להתחיל בשליחה?', 'פעולה זו תשלח כעת מייל לכל מי שמופיע ב"רשימת_תפוצה".\nהאם אתה בטוח שברצונך להמשיך?', ui.ButtonSet.YES_NO); if (response == ui.Button.YES) { ss.toast("מתחיל בשליחת המיילים (זה עשוי לקחת כמה רגעים), אנא המתן...", "מערכת סקרים", 8); const result = sendVotingEmails(); ui.alert('סטטוס שליחה 📬', result, ui.ButtonSet.OK); } } // --------------------------------------------------------- // שרת קליטה (doGet) - משופר עם LockService ו-TextFinder לעומסים גדולים // --------------------------------------------------------- function doGet(e) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName("הצבעות"); const settingsSheet = ss.getSheetByName("הגדרות"); if (!sheet || !settingsSheet) return createResponseHtml('שגיאה: חסרות לשוניות. אנא הרץ התקנה אוטומטית.'); const successMsg = settingsSheet.getRange("B4").getValue() || "תודה! ההצבעה נקלטה."; const errorMsg = settingsSheet.getRange("B5").getValue() || "כבר הצבעת בסקר זה."; const redirectUrl = settingsSheet.getRange("B6").getValue(); const uid = e.parameter.uid || "unknown"; const pollId = e.parameter.poll_id || "general"; const vote = e.parameter.vote || "none"; const cacheKey = uid + "_" + pollId; const cache = CacheService.getScriptCache(); // [שיפור ביצועים] מנגנון נעילה - מונע דריסת נתונים אם המון משתמשים מצביעים באותה שנייה const lock = LockService.getScriptLock(); try { // ממתין עד 10 שניות לשחרור הגיליון (אם תפוס על ידי לחיצה אחרת) lock.waitLock(10000); // 1. חסימת לחיצה כפולה ממטמון (Cache) - מהיר מאוד if (cache.get(cacheKey)) return handleFinish(errorMsg, redirectUrl); // 2. [שיפור לבעיית ה-Scale] - במקום לטעון את כל הגיליון (שעלול להיות עצום), מחפשים נקודתית let alreadyVoted = false; // חיפוש ה-UID בעמודה A בלבד באמצעות מנוע חיפוש פנימי (מהיר פי כמה) const uidMatches = sheet.getRange("A:A").createTextFinder(uid).matchEntireCell(true).findAll(); for (let i = 0; i < uidMatches.length; i++) { const row = uidMatches[i].getRow(); // בודקים אם באותה השורה שמצאנו, הסקר הספציפי כבר קיים const existingPollId = sheet.getRange(row, 2).getValue(); if (String(existingPollId).trim() === String(pollId).trim()) { alreadyVoted = true; break; } } if (alreadyVoted) { cache.put(cacheKey, "1", 21600); return handleFinish(errorMsg, redirectUrl); } // 3. תרגום הערך למילה קריאה let voteText = decodeURIComponent(vote); const dictionary = settingsSheet.getRange("A11:C50").getValues(); let globalTranslation = null; for (let i = 0; i < dictionary.length; i++) { const dictPollId = String(dictionary[i][0]).trim(); const dictVote = String(dictionary[i][1]).trim(); const dictTranslation = dictionary[i][2]; if (dictVote === String(vote).trim() && dictTranslation) { if (dictPollId === String(pollId).trim()) { voteText = dictTranslation; break; } else if (dictPollId === "כללי" || dictPollId === "") { globalTranslation = dictTranslation; } } } if (voteText === decodeURIComponent(vote) && globalTranslation) { voteText = globalTranslation; } // 4. שמירת ההצבעה החדשה sheet.appendRow([uid, pollId, voteText, new Date(), e.parameter.userAgent || ""]); cache.put(cacheKey, "1", 21600); return handleFinish(successMsg, redirectUrl, voteText); } catch (error) { // במקרה של שגיאה בלתי צפויה או חריגת זמן נעילה return createResponseHtml('אירעה שגיאה בעת עיבוד ההצבעה. נסה שנית בעוד מספר רגעים.'); } finally { // משחרר את הנעילה כדי לאפשר למצביע הבא להיכנס, גם אם הייתה שגיאה lock.releaseLock(); } } // --------------------------------------------------------- // עיצוב מסך הסיום (הפניה או הודעה מעוצבת) // --------------------------------------------------------- function handleFinish(message, redirectUrl, voteText = "") { if (redirectUrl && redirectUrl.indexOf("http") === 0) { return HtmlService.createHtmlOutput(`

מעביר אותך ליעד...

`); } else { let voteDisplay = voteText ? `

הבחירה שנקלטה: ${voteText}

` : ''; return HtmlService.createHtmlOutput(`
✅

${message}

${voteDisplay}

תוכל כעת לסגור חלון זה ולחזור למייל.

`); } } function createResponseHtml(message) { return HtmlService.createHtmlOutput(`

${message}

`); } // --------------------------------------------------------- // מנוע שליחת מיילים אוטומטית - משופר עם Batch Operations ומניעת שגיאות אנוש // --------------------------------------------------------- function sendVotingEmails() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const settingsSheet = ss.getSheetByName("הגדרות"); const listSheet = ss.getSheetByName("רשימת_תפוצה"); if (!settingsSheet || !listSheet) return "שגיאה: הלשוניות חסרות. לחץ על 'התקנה ועיצוב אוטומטי' בתפריט."; const serverUrl = settingsSheet.getRange("B2").getValue(); const emailSubject = settingsSheet.getRange("B3").getValue() || "סקר חדש"; const rawHtmlTemplate = String(settingsSheet.getRange("B8").getValue() || ""); if (!serverUrl || !serverUrl.includes("exec")) return "הפעולה בוטלה: חסרה כתובת שרת תקינה בתא B2 בגיליון ההגדרות."; if (!rawHtmlTemplate.trim()) return "הפעולה בוטלה: חסר קוד תבנית המייל בתא B8 בגיליון ההגדרות."; // [מניעת שגיאות אנוש] - ולידציה שקיימות התגיות ההכרחיות בקוד if (rawHtmlTemplate.indexOf("{{SERVER_URL}}") === -1) { return "שגיאה קריטית: חסרה התגית {{SERVER_URL}} בתוך קוד ה-HTML בתא B8. התגית קריטית כדי שהכפתורים יעבדו."; } if (rawHtmlTemplate.indexOf("{{EMAIL}}") === -1) { return "שגיאה קריטית: חסרה התגית {{EMAIL}} בתוך קוד ה-HTML בתא B8. בלעדיה לא נוכל לדעת מי הצביע."; } const data = listSheet.getDataRange().getValues(); if (data.length <= 1) return "רשימת התפוצה ריקה. נא להזין כתובות מייל בגיליון 'רשימת_תפוצה'."; let sentCount = 0; let errorCount = 0; // [שיפור ביצועים - Batch Operations] במקום לעדכן שורה-שורה, נאגור הכל למערכים const statusUpdates = []; const colorUpdates = []; for (let i = 1; i < data.length; i++) { const email = data[i][0]; const name = data[i][1]; // וידוא שהמייל לא ריק ושנראה כמו כתובת תקינה בסיסית if (!email || String(email).indexOf("@") === -1) { statusUpdates.push(["כתובת לא תקינה"]); colorUpdates.push(["#ff0000"]); // צבע אדום continue; } let personalizedHtml = rawHtmlTemplate .replace(/{{SERVER_URL}}/g, serverUrl.trim()) .replace(/{{EMAIL}}/g, encodeURIComponent(String(email).trim())) .replace(/{{NAME}}/g, name || 'לקוח יקר'); try { MailApp.sendEmail({ to: String(email).trim(), subject: emailSubject, htmlBody: personalizedHtml }); statusUpdates.push(["נשלח ✔"]); colorUpdates.push(["#008000"]); // צבע ירוק sentCount++; } catch(err) { statusUpdates.push(["שגיאה: " + err.message]); colorUpdates.push(["#ff0000"]); // צבע אדום errorCount++; } } // עדכון גורף במכה אחת של כל הסטטוסים בגיליון (חוסך המון זמן ריצה) if (statusUpdates.length > 0) { const targetRange = listSheet.getRange(2, 3, statusUpdates.length, 1); targetRange.setValues(statusUpdates); targetRange.setFontColors(colorUpdates); } if (errorCount > 0) { return `פעולת השליחה הסתיימה.\nנשלחו בהצלחה: ${sentCount} מיילים.\nנכשלו או לא תקינים: ${errorCount} (ראה עמודת סטטוס).`; } return `מצוין! הקמפיין הסתיים בהצלחה.\n${sentCount} מיילים נשלחו אל הנמענים.`; } // --------------------------------------------------------- // פונקציה להוספת מילון כפתורים לסקר חדש - אוטומטית מתוך קוד ה-HTML // --------------------------------------------------------- function addNewPollDictionary() { const ui = SpreadsheetApp.getUi(); const ss = SpreadsheetApp.getActiveSpreadsheet(); const settingsSheet = ss.getSheetByName("הגדרות"); if (!settingsSheet) { ui.alert('⚠️ שגיאה', 'גיליון "הגדרות" לא קיים. אנא הרץ קודם התקנה ועיצוב אוטומטי.', ui.ButtonSet.OK); return; } const htmlTemplate = settingsSheet.getRange("B8").getValue(); if (!htmlTemplate) { ui.alert('⚠️ שגיאה', 'לא נמצא קוד HTML בתא B8 בגיליון ההגדרות.', ui.ButtonSet.OK); return; } const regex = /poll_id=([^&"']+)/g; let match; const foundPollIds = new Set(); while ((match = regex.exec(htmlTemplate)) !== null) { foundPollIds.add(match[1].trim()); } if (foundPollIds.size === 0) { ui.alert('ℹ️ מידע', 'לא זוהו מזהי סקרים (poll_id) בתוך קוד ה-HTML.', ui.ButtonSet.OK); return; } const lastRow = settingsSheet.getLastRow(); const existingPollIds = new Set(); if (lastRow >= 11) { const dictionaryData = settingsSheet.getRange(11, 1, lastRow - 10, 1).getValues(); dictionaryData.forEach(row => { if (row[0]) existingPollIds.add(String(row[0]).trim()); }); } const newPollIds = Array.from(foundPollIds).filter(id => !existingPollIds.has(id)); if (newPollIds.length === 0) { ui.alert('✅ הכל מעודכן', 'המערכת סרקה את קוד ה-HTML. כל מזהי הסקרים המופיעים בו כבר קיימים במילון למטה.', ui.ButtonSet.OK); return; } const newData = []; newPollIds.forEach(pollId => { newData.push([pollId, "1", "מצוין"]); newData.push([pollId, "2", "בינוני"]); newData.push([pollId, "3", "טעון שיפור"]); }); const range = settingsSheet.getRange(lastRow + 1, 1, newData.length, 3); range.setValues(newData); range.setBorder(true, true, true, true, true, true); range.setBackground("#ffffff"); ui.alert('✅ זיהוי והוספה הצליחו', 'זוהו ונוספו למילון ' + newPollIds.length + ' סקרים חדשים מתוך קוד ה-HTML:\n\n' + newPollIds.join('\n') + '\n\nכעת ניתן לערוך את הטקסטים בעמודה C במידת הצורך.', ui.ButtonSet.OK); } // --------------------------------------------------------- // מנוע לייצור דשבורד וגרפים אוטומטיים // --------------------------------------------------------- function generateGraphs() { const ui = SpreadsheetApp.getUi(); const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetVotes = ss.getSheetByName("הצבעות"); if (!sheetVotes || sheetVotes.getLastRow() <= 1) { ui.alert("⚠️ שגיאה", "אין עדיין מספיק נתונים בגיליון 'הצבעות' כדי ליצור גרפים.", ui.ButtonSet.OK); return; } let sheetGraphs = ss.getSheetByName("דשבורד_גרפים"); if (sheetGraphs) { ss.deleteSheet(sheetGraphs); } sheetGraphs = ss.insertSheet("דשבורד_גרפים"); sheetGraphs.setTabColor("#8e44ad"); sheetGraphs.getRange("A1").setValue("📊 דשבורד תוצאות סקרים").setFontSize(20).setFontWeight("bold"); const data = sheetVotes.getDataRange().getValues(); const pollStats = {}; for (let i = 1; i < data.length; i++) { const pollId = String(data[i][1]).trim(); const vote = String(data[i][2]).trim(); if (!pollId || !vote) continue; if (!pollStats[pollId]) { pollStats[pollId] = {}; } if (!pollStats[pollId][vote]) { pollStats[pollId][vote] = 0; } pollStats[pollId][vote]++; } let rowPos = 3; let colPos = 1; for (const pollId in pollStats) { const votes = pollStats[pollId]; const tableData = [["תשובה", "כמות הצבעות"]]; for (const vote in votes) { tableData.push([vote, votes[vote]]); } const range = sheetGraphs.getRange(rowPos, colPos, tableData.length, 2); range.setValues(tableData); range.setBorder(true, true, true, true, true, true); sheetGraphs.getRange(rowPos, colPos, 1, 2).setBackground("#d9ead3").setFontWeight("bold"); sheetGraphs.getRange(rowPos - 1, colPos).setValue("סקר: " + pollId).setFontWeight("bold").setFontSize(14).setFontColor("#2c3e50"); const chartBuilder = sheetGraphs.newChart() .setChartType(Charts.ChartType.PIE) .addRange(range) .setPosition(rowPos, colPos + 3, 0, 0) .setOption('title', 'התפלגות תגובות: ' + pollId) .setOption('is3D', true) .setOption('width', 450) .setOption('height', 280); sheetGraphs.insertChart(chartBuilder.build()); rowPos += Math.max(tableData.length + 2, 16); } sheetGraphs.setColumnWidth(1, 150); sheetGraphs.setColumnWidth(2, 100); ui.alert("✅ הדשבורד מוכן!", "הגרפים נוצרו בהצלחה. עבור ללשונית החדשה 'דשבורד_גרפים' כדי לצפות בהם.", ui.ButtonSet.OK); }