🎓 פתוח לרישום

הכשרת EMDR
מחזורי ההכשרה הקרובים

ההכשרה הבסיסית מורכבת משני שלבים. שלב 1 מתאים לטראומה נקודתית, ושלב 2 נותן כלים לאוכלוסיות מורכבות ולפרוטוקולים מתקדמים. ההכשרה מוכרת במסגרת החובות התיאורטיות להסמכה כמטפל/ת EMDR על ידי עמותת EMDR ישראל ואירופה.

🛠️ אתר הרישום החדש נמצא בהרצה. אם נתקלת בבעיה בהרשמה, או שלא התקבל אצלך מייל אישור, נשמח שתיצור/י קשר טלפוני עם איבון: 052-361-4672

איך זה עובד?

1

בדוק/י זכאות – וודא/י שאת/ה עומד/ת בתנאי הקבלה

2

בחר/י העדפות שיבוץ – בחר/י מועדים לקבוצת הזום

3

מלא/י טופס רישום – פרטים אישיים, מקצוע ותיעוד

4

שלם/י ועגן/י מקום – דמי רישום 355 ₪ (אין החזר)

פרטי הקורס

ההכשרה הבסיסית

מכון פשוט לפסיכותרפיה EMDR – ד"ר קרן מינץ מלחי. ההכשרה מוכרת על ידי עמותת EMDR ישראל ו-EMDR Europe. עמותת EMDR ישראל ←

ההכשרה הבסיסית מורכבת משני השלבים יחד. שלב 1 מתאים לטראומה נקודתית יותר, ושלב 2 נותן כלים לאוכלוסיות מורכבות ולפרוטוקולים מתקדמים.

תנאי קבלה

האם את/ה זכאי/ת?

על פי תנאי קבלה של עמותת EMDR ישראל ←

מה המקצוע שלך?

מי שאינו/ה עומד/ת בקריטריונים יכול/ה לפנות לוועדת החריגים של עמותת EMDR ישראל — נוהל הפנייה (PDF) ←
הרשמה לקורס

טופס רישום

מלא/י את כל השדות. לאחר הגשה נבדוק את הבקשה ונחזור אליך במייל.

🛠️ אתר הרישום החדש נמצא בהרצה. אם נתקלת בבעיה בהרשמה, או שלא התקבל אצלך מייל אישור, נשמח שתיצור/י קשר טלפוני עם איבון: 052-361-4672
נא לבחור למעלה את המסלול המבוקש — שלב 1, שלב 2, או שני השלבים יחד. טופס ההרשמה ייפתח בהתאם לבחירה.
1
פרטים
2
מקצוע
3
קבוצה
4
תשלום
5
אישור
פרטים אישיים
נא למלא פרטים כפי שמופיעים בתעודת זהות.
שדה חובה
שדה חובה
נא להזין מייל תקין
שדה חובה
שדה חובה
שדה חובה
פרטים מקצועיים
הקורס פועל בהתאם לתנאי עמותת EMDR ישראל.
נא לבחור מקצוע
העדפות שיבוץ לקבוצת הדרכה
הקורס משלב חלק תיאורטי פרונטלי עם שיבוץ לקבוצת פרקטיקום והדרכות זום – אותה קבוצה לאורך כל הקורס.

לחצ/י על הקבוצות לפי סדר העדפה – הלחיצה הראשונה תסומן כעדיפות 1, השנייה כעדיפות 2 וכן הלאה. יש לסמן לפחות 2 קבוצות כדי להגדיל את הסיכוי להשתבץ. לחיצה חוזרת מבטלת.
הערת עדיפות תילקח בחשבון בשיבוץ האוטומטי, ככל הניתן
תשלום ושמירת מקום
כך מתנהל התהליך:
1
הגשת הבקשה – ללא תשלום בשלב זה
2
בדיקת המסמכים – נוודא עמידה בתנאי הקבלה של עמותת EMDR ישראל
3
אישור וקישור לתשלום – לאחר אישור הבקשה יישלח אלייך קישור לתשלום דמי רישום בסך 355 ₪
4
השלמת התשלום – יתרת התשלום (3,535 ₪) תשולם כחודשיים לפני תחילת ההכשרה, ותקבלי קישור בנפרד
⚠️ שימי לב: מקומך בקורס יישמר רק לאחר אישור הבקשה והשלמת תשלום דמי הרישום. דמי הרישום אינם מוחזרים במקרה ביטול.
אישורים והצהרות
נא לקרוא ולאשר את כל הסעיפים.
📋 סיכום
☁️ —
ניהול EMDR · —
0 נרשמים
0 רישום
0 מלא
לוח בקרה
0
סה"כ בקשות
0
ממתינות לבדיקה
0
שילמו רישום
0
שילמו מלא
⚠️ דגשים
📊 קבוצות
👥 נרשמים
#שםאימיילטלפוןמקצועדמי רישוםתשלום מלאהעדפותקבוצהסטטוס תשלוםהערותתאריך
שיבוץ קבוצות
השיבוץ האוטומטי מחלק לפי העדפות, הערות עדיפות וקיבולת. אפשר להריץ, לבדוק, לנקות ולהריץ שוב — כל עוד לא נשלחו הודעות, איש אינו יודע על כך.
✉️ הודעות שיבוץ לנרשמים
לשליחה רק לאחר שהשיבוץ סופי. כל נרשם/ת מקבל/ת מייל אישי עם שם המדריך/ה, היום, השעה והמועדים. מי שכבר קיבל/ה הודעה לא יקבל/תקבל שוב.
שליחת הודעות
📋 נמענים
0 נבחרו
✍️ כתיבת הודעה
ניתן לשלב פרטים אישיים: {שם} {קורס} {תאריכים} {מיקום} {מדריך} {יום} {שעה} {תאריכי_הדרכה} {סכום_רישום} {יתרה} {לינק_רישום} {לינק_יתרה} — הם מוחלפים בנתונים של כל נרשם/ת בנפרד.

✏️ עריכת קורסים

כל שינוי כאן מתעדכן מיד באתר הציבורי — בכותרת, בפרטי הקורס ובטופס ההרשמה. הנרשמים הקיימים והשיבוצים שלהם נשמרים.
🖼️ לוגו המכון
מוצג בראש האתר
קובץ PNG או JPG, עד 400KB. רקע שקוף (PNG) נראה הכי טוב.
📢 טקסטים באתר
מתעדכנים באתר מיד
⚙️ הגדרות
🔗 חיבור Google Sheets + Gmail

New Project → מחק/י הטקסט → הדבק/י הסקריפט למטה

Project Settings → Script Properties → הוספת מאפיין בשם ANTHROPIC_API_KEY עם המפתח מ-console.anthropic.com. המפתח נשמר ב-Google בלבד ואינו נחשף לגולשים.

Deploy → New Deployment → Web App · Execute as: Me · Access: Anyone

העתק/י את ה-URL והדבק/י למטה

Triggers ⏰ (בסרגל הצד השמאלי) → Add Trigger → פונקציה: deleteExpiredDocuments · Event source: Time-driven · Day timer (פעם ביום) — בלי שלב זה, המסמכים שפג תוקף שמירתם לא נמחקים אוטומטית, בניגוד למה שמובטח בהצהרת הפרטיות בטופס.

🤖 בדיקת מסמכים אוטומטית
כאשר הבדיקה פעילה, עותק של כל מסמך שמועלה נשלח לשירות בינה מלאכותית חיצוני (Anthropic) הפועל מחוץ לישראל, ומתקבלת חוות דעת מייעצת בלבד. המסמך עשוי לכלול שם ומספר תעודת זהות. מדובר בהעברת מידע אישי לגורם שלישי מחוץ לישראל — יש לוודא שהדבר משקף את מדיניות הפרטיות של המכון.
כאשר הבדיקה כבויה, המסמכים נשארים ב-Google בלבד, והמזכירות בודקת אותם ידנית. המערכת עובדת במלואה בשני המצבים.
☁️ סנכרון בין מחשבים
רשימת הנרשמים נשמרת בלשונית מוסתרת בגיליון (__appdata), וכך אותה רשימה זמינה בכל מחשב. כל שינוי נשמר אוטומטית תוך שניות. בפתיחת המערכת נטענת הגרסה העדכנית.
חשוב: אין לערוך את הלשונית הזו ידנית.
🔐 אבטחת כניסת המזכירות
כשמוגדרת סיסמה בסקריפט, בדיקת הסיסמה מתבצעת בתוך Google ולא בדפדפן — כך שאי אפשר לעקוף אותה מתוך קוד האתר.
הגדרה חד-פעמית: בעורך ה-Apps Script → Project Settings (גלגל השיניים) → Script Properties → Add script property → שם: ADMIN_PASSWORD, ערך: הסיסמה שתבחרי → Save.
לאחר מכן יש ללחוץ Deploy → Manage deployments → עריכה → New version → Deploy.
מרגע זה מחליפים סיסמה שם, ולא כאן.
סיסמת חיבור ידנית (גיבוי בלבד)
בדרך כלל אין צורך — הסיסמה הזו מתקבלת אוטומטית בעת הכניסה. יש למלא רק אם ביקשתי זאת במפורש.
📋 קוד Google Apps Script
/*** EMDR – Google Apps Script · v11 ***
 * v11: הוספה ידנית במערכת נרשמת גם בגיליון · כל הכשרה נשארת באותה לשונית גם אם שמה משתנה ·
 *      חסימת שמירה מגרסאות ישנות של האתר (כדי שלשונית פתוחה ישנה לא תמחק נרשמים)
 * v10: נרשמים חדשים מהאתר נכנסים אוטומטית לרשימת מערכת הניהול
 * מבוסס על הסקריפט הקיים, בתוספת: בדיקת מסמכים אוטומטית, מייל אישור
 * מהמערכת, השלמת מסמכים בדיעבד, ובדיקת סיסמת הכניסה בצד Google.
 *
 * הגדרות חד-פעמיות ב-Project Settings → Script Properties:
 *   ADMIN_PASSWORD     – סיסמת הכניסה למערכת הניהול (חובה לאבטחת הכניסה)
 *   ANTHROPIC_API_KEY  – מפתח לבדיקת המסמכים (אופציונלי)
 * הערכים נשמרים בתוך Google ואינם נחשפים לגולשים באתר.
 *
 * נשמרו מהגרסה הקודמת: תיקיית Drive הקיימת, מבנה תת-התיקיות לכל נרשם/ת,
 * מבנה העמודות בגיליון, מחיקת מסמכים אוטומטית אחרי 3 חודשים, והתראת שגיאה במייל.
 */

const FOLDER_ID   = '1snkW53aZLI3zvKP3ZPL2DuW_tq842n-K';
const ADMIN_EMAIL = 'kmmemdr@gmail.com';
const AI_MODEL    = 'claude-sonnet-5-5';
const MAX_FILES   = 6;
const RETENTION_MONTHS = 3;

const HEADERS = ['תאריך בקשה','קוד','שם עברית','שם אנגלית','אימייל',
  'טלפון','עיר','ת.ז.','מקצוע','העדפות קבוצה','הערות',
  'סטטוס','תאריך החלטה','קישור למסמכים','תאריך מחיקה','בדיקת AI','הערת AI'];
const COL_LINK = 14, COL_DEL = 15, COL_AIV = 16, COL_AIR = 17;

/* ═══════════ ניתוב ═══════════ */
function doGet(e) {
  return ContentService.createTextOutput('ok');
}

function doPost(e) {
  var out = { ok: true };
  try {
    var raw;
    if (e.parameter && e.parameter.payload) raw = e.parameter.payload;   // form fallback
    else raw = e.postData.contents;                                       // fetch
    var d = JSON.parse(raw);
    var action = d.action || 'register';
    try { ensureCleanupTrigger_(); } catch (e) {}

    if (ADMIN_ONLY[action] && !tokenOk_(d)) return json_({ ok: false, err: 'unauthorized' });
    if (!sizeOk_(d)) return json_({ ok: false, err: 'too-large' });

    if (action === 'login')         out = handleLogin(d);
    else if (action === 'register') out = handleRegister(d);
    else if (action === 'addDocs')  out = handleAddDocs(d);
    else if (action === 'sendMail') out = handleSendMail(d);
    else if (action === 'recheck')  out = handleRecheck(d);
    else if (action === 'saveState') out = handleSaveState(d);
    else if (action === 'loadState') out = handleLoadState(d);
    else if (action === 'listTabs')  out = handleListTabs(d);
    else if (action === 'readSheet') out = handleReadSheet(d);
    else if (action === 'getPublicConfig') out = handleGetPublicConfig(d);
    else out = { ok: false, err: 'unknown-action' };

  } catch (err) {
    mail_({ to: ADMIN_EMAIL, subject: 'שגיאה במערכת ההרשמה',
      body: 'שגיאה: ' + err.toString() });
    out = { ok: false, err: String(err) };
  }
  return json_(out);
}

/* מחיקת מסמכים אוטומטית — הטיימר היומי מוגדר מעצמו, אין צורך להגדיר ידנית */
function ensureCleanupTrigger_() {
  var cache = CacheService.getScriptCache();
  if (cache.get('cleanup_trigger_ok')) return;
  var has = ScriptApp.getProjectTriggers().some(function (t) {
    return t.getHandlerFunction() === 'deleteExpiredDocuments';
  });
  if (!has) ScriptApp.newTrigger('deleteExpiredDocuments').timeBased().everyDays(1).atHour(3).create();
  cache.put('cleanup_trigger_ok', '1', 21600);
}

/* שליחת מייל בטוחה — כתובת שגויה של נרשם/ת לא תפיל את שאר התהליך */
function mail_(opts, label) {
  try { MailApp.sendEmail(opts); return true; }
  catch (err) {
    try {
      MailApp.sendEmail({ to: ADMIN_EMAIL, subject: 'שליחת מייל נכשלה: ' + (label || ''),
        body: 'לא נשלח מייל אל: ' + opts.to + '\n\nשגיאה: ' + String(err) +
              '\n\nנושא: ' + opts.subject + '\n\n' + opts.body });
    } catch (e2) {}
    return false;
  }
}

function json_(o) {
  return ContentService.createTextOutput(JSON.stringify(o))
    .setMimeType(ContentService.MimeType.JSON);
}

/* ═══════════ הרשאות ═══════════ */
const ADMIN_ONLY = { sendMail: true, recheck: true, saveState: true, loadState: true,
                     listTabs: true, readSheet: true };
const DATA_TAB   = '__appdata';
const MIN_CLIENT = 11;   /* גרסאות אתר ישנות מזה לא יכולות לשמור את הרשימה */
const CHUNK      = 40000;

function tokenOk_(d) {
  var want = PropertiesService.getScriptProperties().getProperty('ADMIN_TOKEN');
  if (!want) return true;                    /* טרם הוגדרה — המערכת ממשיכה לעבוד */
  return String(d.token || '') === want;
}

function sizeOk_(d) {
  var docs = d.documents || [];
  if (docs.length > MAX_FILES) return false;
  var total = 0;
  docs.forEach(function (doc) { total += String(doc.data || '').length; });
  return total < 40000000;
}

function handleLogin(d) {
  var props = PropertiesService.getScriptProperties();
  var want  = props.getProperty('ADMIN_PASSWORD');
  if (!want) return { ok: false, err: 'not-configured' };

  var cache = CacheService.getScriptCache();
  var fails = Number(cache.get('login_fails') || 0);
  if (fails > 8) return { ok: false, err: 'locked' };

  if (String(d.pw || '') !== want) {
    cache.put('login_fails', String(fails + 1), 900);
    Utilities.sleep(1500);
    return { ok: false, err: 'bad-password' };
  }
  cache.remove('login_fails');

  var t = props.getProperty('ADMIN_TOKEN');
  if (!t) { t = Utilities.getUuid().replace(/-/g, ''); props.setProperty('ADMIN_TOKEN', t); }
  return { ok: true, token: t };
}

/* להרצה חד-פעמית מתוך העורך: מאשרת לסקריפט לפנות לשירות חיצוני (נדרש לבדיקת המסמכים).
   בוחרים את הפונקציה למעלה, לוחצים Run, ומאשרים את ההרשאה שגוגל מבקשת. */
function authorizeExternalRequests() {
  var res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post', contentType: 'application/json',
    headers: { 'x-api-key': 'authorization-probe', 'anthropic-version': '2023-06-01' },
    payload: '{}', muteHttpExceptions: true
  });
  Logger.log('ההרשאה אושרה. קוד תגובה: ' + res.getResponseCode() + ' (401 הוא תקין כאן)');
  return 'ok';
}

function createAdminToken() {
  var t = Utilities.getUuid().replace(/-/g, '');
  PropertiesService.getScriptProperties().setProperty('ADMIN_TOKEN', t);
  Logger.log('ADMIN_TOKEN: ' + t);
  return t;
}

/* ═══════════ גיליון ותיקיות ═══════════ */
function sheetFor_(tabName) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(tabName) || ss.insertSheet(tabName);
  if (sh.getLastRow() === 0) {
    sh.appendRow(['#'].concat(HEADERS));   /* עמודת מספור בצד — כל נרשם/ת מקבל/ת מספר */
    sh.setFrozenRows(1);
  } else if (sh.getLastColumn() < HEADERS.length) {
    /* גיליון קיים מהגרסה הקודמת — משלימים את שתי הכותרות החדשות בלבד */
    sh.getRange(1, COL_AIV, 1, 2).setValues([['בדיקת AI', 'הערת AI']]);
  }
  return sh;
}

function getOrCreateFolder(parent, name) {
  var ex = parent.getFoldersByName(name);
  return ex.hasNext() ? ex.next() : parent.createFolder(name);
}

/* אותו מבנה כמו קודם: תיקיית שורש → תיקיית הכשרה → תיקייה לכל נרשם/ת */
function personFolder_(tabName, hebrewName, regId) {
  var root = DriveApp.getFolderById(FOLDER_ID);
  var tf = getOrCreateFolder(root, tabName || 'הרשמות');
  return getOrCreateFolder(tf, (hebrewName || '') + ' - ' + regId);
}

function saveDocs_(pf, docs) {
  (docs || []).forEach(function (doc) {
    var blob = Utilities.newBlob(Utilities.base64Decode(doc.data), doc.mimeType, doc.name);
    pf.createFile(blob);
  });
  return pf.getUrl();
}

/* היסט עמודות: אם מישהו הוסיף עמודה משמאל (למשל מספור), כל העמודות זזות — מזהים לפי הכותרת */
function off_(sheet) {
  try {
    var lastC = sheet.getLastColumn();
    if (!lastC) return 0;
    var h = sheet.getRange(1, 1, 1, lastC).getValues()[0];
    for (var c = 0; c < h.length; c++) if (String(h[c]).trim() === 'קוד') return Math.max(0, c - 1);
  } catch (e) {}
  return 0;
}

/* כתיבת נרשם/ת לגיליון. בלשונית עם עמודת מספור: ממלאים את השורה הממוספרת הפנויה הבאה
   (או שורה חדשה), ונותנים מספר רץ. בלשונית ישנה בלי מספור — מוסיפים בסוף כרגיל. */
function writeRow_(sheet, values) {
  var o = off_(sheet);
  if (o < 1) { sheet.appendRow(values); return; }
  var last = sheet.getLastRow();
  var width = o + values.length;
  var data = last >= 2 ? sheet.getRange(2, 1, last - 1, width).getValues() : [];
  var maxNum = 0, target = -1, targetNum = null;
  for (var i = 0; i < data.length; i++) {
    var a = data[i][0];
    var n = Number(a);
    if (a !== '' && !isNaN(n)) maxNum = Math.max(maxNum, n);
    if (target < 0) {
      var rest = data[i].slice(1).join('').trim();
      var aOk = (a === '' || !isNaN(n));
      if (!rest && aOk) { target = i + 2; targetNum = (a !== '' ? n : null); }
    }
  }
  if (target < 0) target = last + 1;
  var num = targetNum !== null ? targetNum : maxNum + 1;
  var row = [num];
  for (var p = 1; p < o; p++) row.push('');
  sheet.getRange(target, 1, 1, width).setValues([row.concat(values)]);
}

/* איתור שורה לפי קוד הבקשה, בכל לשוניות הגיליון */
function findRow_(regId) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheets = ss.getSheets();
  for (var i = 0; i < sheets.length; i++) {
    if (sheets[i].getName() === DATA_TAB) continue;
    var vals = sheets[i].getDataRange().getValues();
    var o = off_(sheets[i]);
    for (var r = 1; r < vals.length; r++) {
      if (String(vals[r][1 + o]) === String(regId)) {
        /* values מוחזר מיושר, כך שהאינדקסים הקבועים ממשיכים לעבוד */
        return { sheet: sheets[i], row: r + 1, values: vals[r].slice(o), tab: sheets[i].getName(), off: o };
      }
    }
  }
  return null;
}

/* ═══════════ בדיקת מסמכים ═══════════ */
function blocksFromDocs_(docs) {
  var blocks = [];
  (docs || []).slice(0, 5).forEach(function (doc) {
    if (doc.mimeType === 'application/pdf') {
      blocks.push({ type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: doc.data } });
    } else if (String(doc.mimeType).indexOf('image/') === 0) {
      blocks.push({ type: 'image', source: { type: 'base64', media_type: doc.mimeType, data: doc.data } });
    }
  });
  return blocks;
}

function blocksFromFolderUrl_(url) {
  var m = String(url).match(/folders\/([^\/\?]+)/);
  if (!m) return [];
  var docs = [];
  try {
    var files = DriveApp.getFolderById(m[1]).getFiles();
    while (files.hasNext() && docs.length < 5) {
      var f = files.next();
      docs.push({ name: f.getName(), mimeType: f.getMimeType(),
                  data: Utilities.base64Encode(f.getBlob().getBytes()) });
    }
  } catch (e) { return []; }
  return blocksFromDocs_(docs);
}

/* עטיפה: כישלון בבדיקה האוטומטית לעולם לא יפיל את ההרשמה */
function checkDocsSafe_(d, blocks) {
  try { return checkDocs_(d, blocks); }
  catch (err) {
    return { verdict: 'pending', reason: 'הבדיקה האוטומטית לא בוצעה: ' + String(err).slice(0, 150) };
  }
}

function checkDocs_(d, blocks) {
  /* המתג במסך ההגדרות גובר: אם הבדיקה כבויה, שום מסמך אינו יוצא מ-Google */
  if (d.aiEnabled === false) return { verdict: 'none', reason: 'הבדיקה האוטומטית כבויה — המסמכים נבדקים ידנית.' };
  var key = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
  if (!key) return { verdict: 'pending', reason: 'לא הוגדר מפתח API — הבדיקה האוטומטית לא בוצעה.' };
  if (!blocks || !blocks.length) return { verdict: 'review', reason: 'לא נמצא קובץ שניתן לבדוק אוטומטית.' };

  var prompt =
    'המבקש/ת הצהיר/ה על המקצוע: ' + (d.professionLabel || d.profession || 'לא צוין') + '.\n' +
    'המסמכים הנדרשים לפי תנאי הקבלה: ' + (d.docRequirement || 'לא הוגדרו') + '\n\n' +
    'בדוק/י אם המסמכים המצורפים אכן מהווים את התיעוד הנדרש. ' +
    'החזר/י JSON בלבד, ללא טקסט נוסף, במבנה: ' +
    '{"verdict":"ok|review|problem","reason":"משפט או שניים בעברית"} ' +
    'ok = המסמכים תואמים במלואם. review = חסר פרט או שיש ספק שדורש עין אנושית. ' +
    'problem = המסמכים אינם תואמים את הנדרש.';

  var content = blocks.slice(0);
  content.push({ type: 'text', text: prompt });

  var res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model: AI_MODEL,
      max_tokens: 600,
      system: 'את/ה עוזר/ת למזכירות הכשרת EMDR. תפקידך לתת חוות דעת מייעצת בלבד על התאמת מסמכי הסמכה. ' +
              'ההחלטה הסופית תמיד אנושית. אין לדחות בקשה, רק לסמן. ענה/י בעברית וב-JSON בלבד.',
      messages: [{ role: 'user', content: content }]
    }),
    muteHttpExceptions: true
  });

  var body = res.getContentText();
  if (res.getResponseCode() !== 200) {
    return { verdict: 'pending', reason: 'שגיאה בבדיקה: ' + body.slice(0, 200) };
  }
  try {
    var j = JSON.parse(body), txt = '';
    (j.content || []).forEach(function (b) { if (b.type === 'text') txt += b.text; });
    txt = txt.replace(/```json/g, '').replace(/```/g, '').trim();
    var parsed = JSON.parse(txt);
    return { verdict: parsed.verdict || 'review', reason: parsed.reason || '' };
  } catch (err) {
    return { verdict: 'review', reason: 'לא ניתן היה לפענח את תשובת הבדיקה.' };
  }
}

/* כל הכשרה נשמרת תמיד באותה לשונית — גם אם שם ההכשרה שונה מאוחר יותר */
function tabFor_(d) {
  var label = d.training || 'הרשמות';
  if (!d.trainingId) return label;
  var props = PropertiesService.getScriptProperties();
  var key = 'TAB_' + d.trainingId;
  var saved = props.getProperty(key);
  if (saved && SpreadsheetApp.getActiveSpreadsheet().getSheetByName(saved)) return saved;
  props.setProperty(key, label);
  return label;
}

/* ═══════════ רישום חדש ═══════════ */
function handleRegister(d) {
  /* הוספה ידנית מהמערכת — רק למנהל/ת מחובר/ת, בלי מיילים ובלי בדיקה אוטומטית */
  var manual = !!d.manual;
  if (manual && !tokenOk_(d)) return { ok: false, err: 'unauthorized' };
  var tabName = tabFor_(d);
  var sheet = sheetFor_(tabName);

  var docLink = '';
  if (d.documents && d.documents.length) {
    try { docLink = saveDocs_(personFolder_(tabName, d.hebrewName, d.regId), d.documents); }
    catch (err) { docLink = 'שגיאה בשמירת המסמכים: ' + String(err).slice(0, 120); }
  }

  var ai = manual
    ? { verdict: 'none', reason: 'נוסף/ה ידנית על ידי המזכירות.' }
    : (d.documents && d.documents.length)
      ? checkDocsSafe_(d, blocksFromDocs_(d.documents))
      : { verdict: 'none', reason: 'טרם הוגש תיעוד.' };

  var del = new Date();
  del.setMonth(del.getMonth() + RETENTION_MONTHS);
  /* בקשה שעוד לא הוחלט עליה לא תימחק לפני שהקורס התחיל (+חודש) — כדי שלא יימחקו מסמכים לפני ההחלטה */
  if (d.courseStart) {
    var cs = new Date(d.courseStart);
    if (!isNaN(cs.getTime())) { cs.setMonth(cs.getMonth() + 1); if (cs > del) del = cs; }
  }

  writeRow_(sheet, [new Date(), d.regId, d.hebrewName, d.englishName, d.email,
    d.phone, d.city, d.idNum, d.professionLabel || d.profession,
    (d.preferredGroupNames || []).join(', '),
    (d.supNotes || '') + (d.enrollBoth ? ' [נרשם/ה לשלב 1+2 — שלב 2 במחזור עתידי]' : ''),
    manual ? (d.status || 'ממתין לבדיקה') : (d.docsLater ? 'ממתין לתיעוד' : 'ממתין לבדיקה'), '', docLink, del, ai.verdict, ai.reason]);

  if (manual) {
    try { addRegToState_(d, ai, docLink); } catch (err) {}
    return { ok: true, ai: ai, files: [docLink], tab: tabName };
  }

  mail_({
    to: d.email,
    subject: 'בקשתך להכשרת EMDR התקבלה',
    body: 'שלום ' + d.hebrewName + ',\n\n'
      + 'בקשתך להכשרת EMDR התקבלה ונמצאת כעת בבדיקה.\n'
      + 'קוד הבקשה שלך: ' + d.regId + '\n\n'
      + (d.docsLater
          ? 'מקומך שמור בסטטוס "ממתין לתיעוד". להשלמת המסמכים נא להיכנס לקישור האישי:\n' + (d.returnLink || '') + '\n\n'
          : 'קיבלנו את מסמכיך, אנחנו בודקים ונחזור אלייך בקרוב.\n')
      + 'לאחר בדיקת המסמכים תישלח אלייך הודעה עם קישור לתשלום דמי הרישום.\n'
      + 'מקומך בקורס יישמר רק לאחר אישור הבקשה והשלמת התשלום.\n\n'
      + 'בברכה,\nד"ר קרן מינץ מלחי\nמכון פשוט לפסיכותרפיה EMDR'
  }, 'אישור קבלת בקשה לנרשם/ת');

  mail_({
    to: ADMIN_EMAIL,
    subject: 'בקשה חדשה: ' + d.hebrewName + ' · בדיקה: ' + ai.verdict,
    body: 'שם: ' + d.hebrewName + '\n'
      + 'קוד: ' + d.regId + '\n'
      + 'מקצוע: ' + (d.professionLabel || d.profession) + '\n'
      + 'אימייל: ' + d.email + '\n'
      + 'טלפון: ' + d.phone + '\n'
      + 'הכשרה: ' + tabName + (d.enrollBoth ? ' (שלב 1+2)' : '') + '\n'
      + 'העדפות: ' + (d.preferredGroupNames || []).join(', ') + '\n'
      + 'הערות: ' + (d.supNotes || '-') + '\n\n'
      + 'בדיקת מסמכים: ' + ai.verdict + '\n' + ai.reason + '\n\n'
      + 'מסמכים: ' + (docLink || 'לא הועלו') + '\n\n'
      + 'אין לשלוח אישור עד לבדיקה ידנית.'
  }, 'התראה למשרד');

  /* הוספת הנרשם/ת גם לרשימה המשותפת של מערכת הניהול — כך מופיע/ה שם אוטומטית */
  try { addRegToState_(d, ai, docLink); } catch (err) {
    mail_({ to: ADMIN_EMAIL, subject: 'נרשם/ת לא נוסף/ה לרשימת המערכת: ' + (d.hebrewName || d.regId),
      body: 'הבקשה נשמרה בגיליון (לשונית ' + tabName + ') אך לא נוספה אוטומטית למערכת הניהול.\nקוד: ' + d.regId + '\nשגיאה: ' + String(err) }, 'שגיאת סנכרון');
  }

  return { ok: true, ai: ai, files: [docLink] };
}

/* קריאה/כתיבה של מצב המערכת המשותף (__appdata) מצד השרת */
function readStateJson_(sh) {
  var last = sh.getLastRow();
  if (last < 2) return null;
  var vals = sh.getRange(2, 1, last - 1, 1).getValues();
  var json = '';
  vals.forEach(function (r) { json += String(r[0] || ''); });
  return json ? JSON.parse(json) : null;
}

function addRegToState_(d, ai, docLink) {
  var tid = d.trainingId;
  if (!tid || !d.regId) return;
  var lock = LockService.getScriptLock();
  lock.waitLock(20000);
  try {
    var sh = dataSheet_();
    var meta = readMeta_(sh);
    var st = readStateJson_(sh);
    if (!st || !st.trainings) return;           /* אין עדיין מצב שמור — הייבוא הידני יטפל */
    st.registrations = st.registrations || {};
    var list = st.registrations[tid] = st.registrations[tid] || [];
    for (var i = 0; i < list.length; i++) if (String(list[i].id) === String(d.regId)) return;

    var skip = { action: 1, token: 1, documents: 1, regId: 1, training: 1,
                 preferredGroupNames: 1, professionLabel: 1, returnLink: 1, aiEnabled: 1 };
    var rec = {};
    Object.keys(d).forEach(function (k) { if (!skip[k]) rec[k] = d[k]; });
    rec.id = d.regId;
    rec.trainingId = tid;
    rec.aiVerdict = ai ? ai.verdict : (rec.aiVerdict || 'pending');
    rec.aiReason = ai ? ai.reason : (rec.aiReason || '');
    if (docLink && String(docLink).indexOf('http') === 0) rec.driveLink = docLink;
    rec.source = d.manual ? 'manual' : 'web';
    if (!d.manual) {
      /* הטופס הציבורי לא יכול לקבוע סטטוס, תשלום או שיבוץ — רק המזכירות */
      rec.status = d.docsLater ? 'ממתין לתיעוד' : 'ממתין לבדיקה';
      rec.regPaid = false; rec.fullPaid = false;
      rec.assignedGroup = null; rec.assignedRank = null; rec.waitlist = false;
      delete rec.manual; delete rec.driveLink;
      if (docLink && String(docLink).indexOf('http') === 0) rec.driveLink = docLink;
      Object.keys(rec).forEach(function (k) {   /* מגבלת אורך לשדות טקסט */
        if (typeof rec[k] === 'string' && rec[k].length > 2000) rec[k] = rec[k].slice(0, 2000);
      });
    }
    list.push(rec);

    var json = JSON.stringify(st);
    var chunks = [];
    for (var j = 0; j < json.length; j += CHUNK) chunks.push([json.substr(j, CHUNK)]);
    sh.clear();
    var newMeta = { version: Number(meta.version || 0) + 1, savedAt: new Date().toISOString(),
                    savedBy: 'טופס ההרשמה באתר', rows: chunks.length };
    sh.getRange(1, 1).setValue(JSON.stringify(newMeta));
    if (chunks.length) sh.getRange(2, 1, chunks.length, 1).setValues(chunks);
  } finally {
    lock.releaseLock();
  }
}

/* ═══════════ השלמת מסמכים בדיעבד ═══════════ */
function handleAddDocs(d) {
  var found = findRow_(d.regId);
  /* פרטי הנרשם/ת נלקחים מהרשימה המשותפת — הקישור האישי נפתח לרוב במכשיר אחר, בלי נתונים מקומיים */
  var sheetEmail = found ? String(found.values[4] || '') : '';
  var info = findStateRec_(d.regId, d.email || sheetEmail) || {};
  var rec = info.rec || {};
  var name  = d.hebrewName || rec.hebrewName || (found ? String(found.values[2] || '') : '');
  var email = d.email || rec.email || sheetEmail;
  var tabName = found ? found.tab : (d.training || 'הרשמות');
  var docLink = saveDocs_(personFolder_(tabName, name, d.regId), d.documents);

  /* בדיקה אוטומטית רק אם המתג פעיל וגם הנרשם/ת לא ביקש/ה בדיקה ידנית בלבד */
  var check = {
    aiEnabled: d.aiEnabled !== false && rec.aiConsent !== false,
    professionLabel: d.professionLabel || rec.profession || '',
    docRequirement: d.docRequirement || rec.docRequirement ||
      (info.level === 'level2' && !rec.enrollBoth ? 'תעודת סיום שלב 1 בהכשרת EMDR.' : '')
  };
  var ai = checkDocsSafe_(check, blocksFromDocs_(d.documents));

  if (found) {
    var o = found.off || 0;
    found.sheet.getRange(found.row, COL_LINK + o).setValue(docLink);
    found.sheet.getRange(found.row, COL_AIV + o).setValue(ai.verdict);
    found.sheet.getRange(found.row, COL_AIR + o).setValue(ai.reason);
    found.sheet.getRange(found.row, 12 + o).setValue('ממתין לבדיקה');
    if (!found.values[COL_DEL - 1]) {
      var del = new Date(); del.setMonth(del.getMonth() + RETENTION_MONTHS);
      found.sheet.getRange(found.row, COL_DEL + o).setValue(del);
    }
  }

  /* עדכון הרשומה ברשימת המערכת — כדי שהמזכירות תראה מיד שהמסמכים הוגשו */
  try {
    updateStateRec_(d.regId, email, function (r) {
      r.docsLater = false;
      r.status = 'ממתין לבדיקה';
      r.aiVerdict = ai.verdict; r.aiReason = ai.reason;
      if (String(docLink).indexOf('http') === 0) r.driveLink = docLink;
    });
  } catch (err) {}

  mail_({
    to: ADMIN_EMAIL,
    subject: 'הושלמו מסמכים: ' + (name || d.regId) + ' · בדיקה: ' + ai.verdict,
    body: 'קוד: ' + d.regId + '\n' + ai.reason + '\n\nמסמכים: ' + docLink
  }, 'התראת השלמת מסמכים');
  if (email) {
    mail_({ to: email, subject: 'קיבלנו את מסמכיך – הכשרת EMDR',
      body: 'שלום ' + (name || '') + ',\n\nהמסמכים התקבלו ונמצאים בבדיקה. נחזור אלייך בקרוב.\n\nבברכה,\nמזכירות ההכשרה' }, 'אישור קבלת מסמכים');
  }
  return { ok: true, ai: ai, files: [docLink] };
}

/* איתור נרשם/ת ברשימה המשותפת לפי קוד, ואם לא נמצא — לפי אימייל */
function findStateRec_(regId, email) {
  var st = readStateJson_(dataSheet_());
  if (!st || !st.registrations) return null;
  var em = String(email || '').toLowerCase().trim();
  var hit = null;
  Object.keys(st.registrations).forEach(function (tid) {
    (st.registrations[tid] || []).forEach(function (r) {
      if (hit) return;
      if (String(r.id) === String(regId) || (em && String(r.email || '').toLowerCase().trim() === em)) {
        var t = (st.trainings || []).filter(function (x) { return x.id === tid; })[0];
        hit = { rec: r, tid: tid, level: t ? t.level : '' };
      }
    });
  });
  return hit;
}

function updateStateRec_(regId, email, fn) {
  var lock = LockService.getScriptLock();
  lock.waitLock(20000);
  try {
    var sh = dataSheet_();
    var meta = readMeta_(sh);
    var st = readStateJson_(sh);
    if (!st || !st.registrations) return false;
    var em = String(email || '').toLowerCase().trim(), done = false;
    Object.keys(st.registrations).forEach(function (tid) {
      (st.registrations[tid] || []).forEach(function (r) {
        if (done) return;
        if (String(r.id) === String(regId) || (em && String(r.email || '').toLowerCase().trim() === em)) { fn(r); done = true; }
      });
    });
    if (done) writeState_(sh, meta, st, 'טופס ההרשמה באתר');
    return done;
  } finally { lock.releaseLock(); }
}

function writeState_(sh, meta, st, by) {
  var json = JSON.stringify(st);
  var chunks = [];
  for (var j = 0; j < json.length; j += CHUNK) chunks.push([json.substr(j, CHUNK)]);
  sh.clear();
  var newMeta = { version: Number(meta.version || 0) + 1, savedAt: new Date().toISOString(),
                  savedBy: by, rows: chunks.length };
  sh.getRange(1, 1).setValue(JSON.stringify(newMeta));
  if (chunks.length) sh.getRange(2, 1, chunks.length, 1).setValues(chunks);
  return newMeta;
}

/* ═══════════ בדיקה חוזרת ═══════════ */
function handleRecheck(d) {
  var found = findRow_(d.regId);
  if (!found) return { ok: false, err: 'no-files', ai: { verdict: 'review', reason: 'הבקשה לא נמצאה בגיליון.' } };
  var link = found.values[COL_LINK - 1];
  if (!link) return { ok: false, err: 'no-files', ai: { verdict: 'none', reason: 'לא הוגשו מסמכים לבקשה זו.' } };

  if (d.aiEnabled === false) return { ok: true, files: [link], ai: { verdict: 'none', reason: '' } };
  var ai = checkDocsSafe_(d, blocksFromFolderUrl_(link));
  found.sheet.getRange(found.row, COL_AIV + (found.off || 0)).setValue(ai.verdict);
  found.sheet.getRange(found.row, COL_AIR + (found.off || 0)).setValue(ai.reason);
  return { ok: true, ai: ai, files: [link] };
}

/* ═══════════ שליחת מייל מהמערכת ═══════════ */
function handleSendMail(d) {
  /* עותק למשרד רק בהחלטות (אישור/דחייה) — כדי לא לשרוף את מכסת המיילים היומית של Gmail */
  var opts = { to: d.to, subject: d.subject, body: d.body };
  if (d.outcome) opts.bcc = ADMIN_EMAIL;
  var sent = mail_(opts, 'מייל מהמערכת');
  if (!sent) return { ok: false, err: 'mail-failed' };
  var found = (d.regId && d.outcome) ? findRow_(d.regId) : null;
  if (found) {
    var o = found.off || 0;
    found.sheet.getRange(found.row, 13 + o).setValue(new Date());   /* תאריך החלטה */
    if (d.outcome === 'accept') {
      found.sheet.getRange(found.row, 12 + o).setValue('מאושר – ממתין לתשלום');
      /* התקבל/ה — נשמר עד שנה מתום הקורס, בהתאם להצהרת הפרטיות */
      var keep = d.retainUntil ? new Date(d.retainUntil) : null;
      if (keep && !isNaN(keep.getTime())) found.sheet.getRange(found.row, COL_DEL + o).setValue(keep);
      else found.sheet.getRange(found.row, COL_DEL + o).clearContent();
    } else if (d.outcome === 'reject') {
      found.sheet.getRange(found.row, 12 + o).setValue('נדחה');
      /* נדחה/בוטל — 3 חודשים מרגע ההחלטה */
      var del2 = new Date(); del2.setMonth(del2.getMonth() + RETENTION_MONTHS);
      found.sheet.getRange(found.row, COL_DEL + o).setValue(del2);
    }
  }
  return { ok: true };
}

/* ═══════════ קריאה ישירה מהגיליון ═══════════
   מאפשר למערכת לקרוא את לשונית התשובות בלי להוריד קובץ CSV. */
function handleListTabs(d) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var names = ss.getSheets().map(function (sh) {
    return { name: sh.getName(), rows: Math.max(0, sh.getLastRow() - 1) };
  }).filter(function (x) { return x.name !== DATA_TAB; });
  return { ok: true, tabs: names };
}

function handleReadSheet(d) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(d.tab);
  if (!sh) return { ok: false, err: 'no-tab' };
  var last = sh.getLastRow(), lastC = sh.getLastColumn();
  if (last < 1) return { ok: true, rows: [] };
  var vals = sh.getRange(1, 1, last, lastC).getValues();
  /* המרה למחרוזות, כדי שהעיבוד יהיה זהה לקובץ CSV */
  var out = vals.map(function (row) {
    return row.map(function (v) {
      if (v instanceof Date) return Utilities.formatDate(v, Session.getScriptTimeZone(), 'M/d/yyyy HH:mm:ss');
      return String(v === null || v === undefined ? '' : v);
    });
  });
  return { ok: true, rows: out };
}

/* ═══════════ סנכרון בין מחשבים ═══════════
 * רשימת הנרשמים נשמרת בלשונית מוסתרת בגיליון, כך שכל מחשב עובד על אותו עותק.
 * גרסה עולה בכל שמירה; ניסיון לשמור מעל גרסה חדשה יותר נדחה ומדווח כהתנגשות.
 */
function dataSheet_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getSheetByName(DATA_TAB);
  if (!sh) {
    sh = ss.insertSheet(DATA_TAB);
    sh.getRange(1, 1).setValue('{"version":0}');
    try { sh.hideSheet(); } catch (e) {}
  }
  return sh;
}

function readMeta_(sh) {
  try { return JSON.parse(sh.getRange(1, 1).getValue() || '{"version":0}'); }
  catch (e) { return { version: 0 }; }
}

function handleSaveState(d) {
  if (Number(d.clientVer || 0) < MIN_CLIENT) {
    return { ok: false, err: 'old-client', msg: 'גרסת האתר בדפדפן הזה ישנה. יש לסגור את הלשונית, לפתוח מחדש וללחוץ Ctrl+Shift+R.' };
  }
  var lock = LockService.getScriptLock();
  try { lock.waitLock(15000); } catch (e) { return { ok: false, err: 'busy' }; }
  try {
    var sh = dataSheet_();
    var meta = readMeta_(sh);
    var have = Number(meta.version || 0);
    var expect = Number(d.expectedVersion || 0);
    if (!d.force && have > expect) {
      return { ok: false, err: 'conflict', meta: meta };
    }
    var json = String(d.json || '');
    var chunks = [];
    for (var i = 0; i < json.length; i += CHUNK) chunks.push([json.substr(i, CHUNK)]);

    sh.clear();
    var newMeta = { version: have + 1, savedAt: new Date().toISOString(),
                    savedBy: d.savedBy || 'לא ידוע', rows: chunks.length };
    sh.getRange(1, 1).setValue(JSON.stringify(newMeta));
    if (chunks.length) sh.getRange(2, 1, chunks.length, 1).setValues(chunks);
    return { ok: true, meta: newMeta };
  } finally {
    lock.releaseLock();
  }
}

/* ערוץ ציבורי, ללא סיסמה — חושף רק את מה שמוצג ממילא באתר: דגל בדיקת ה-AI,
   רשימת ההכשרות הגלויות (תאריכים, מיקום, קבוצות הדרכה) והטקסטים שבאתר.
   נתוני נרשמים לעולם אינם נשלחים מכאן. */
function handleGetPublicConfig(d) {
  try {
    var sh = dataSheet_();
    var last = sh.getLastRow();
    if (last < 2) return { ok: true, aiEnabled: false };
    var vals = sh.getRange(2, 1, last - 1, 1).getValues();
    var json = '';
    vals.forEach(function (r) { json += String(r[0] || ''); });
    var st = JSON.parse(json);
    var out = { ok: true, aiEnabled: !!st.aiEnabled };
    if (Array.isArray(st.trainings)) {
      out.trainings = st.trainings.filter(function (t) { return t && !t.hidden; });
    }
    if (st.texts && typeof st.texts === 'object') out.texts = st.texts;
    return out;
  } catch (e) {
    return { ok: true, aiEnabled: false };
  }
}

function handleLoadState(d) {
  var sh = dataSheet_();
  var meta = readMeta_(sh);
  if (d.metaOnly) return { ok: true, meta: meta };
  var last = sh.getLastRow();
  if (last < 2) return { ok: true, meta: meta, json: '' };
  var vals = sh.getRange(2, 1, last - 1, 1).getValues();
  var json = '';
  vals.forEach(function (r) { json += String(r[0] || ''); });
  return { ok: true, meta: meta, json: json };
}

/* ═══════════ מחיקת מסמכים אחרי 3 חודשים (ללא שינוי) ═══════════ */
function deleteExpiredDocuments() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const today = new Date();
  ss.getSheets().forEach(function (sheet) {
    if (sheet.getName() === DATA_TAB) return;
    const o = off_(sheet);
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      const dd = data[i][14 + o], link = data[i][13 + o];
      if (dd && link && String(link).indexOf('http') === 0 && new Date(dd) <= today) {
        try {
          const id = String(link).match(/folders\/([^\/\?]+)/)[1];
          DriveApp.getFolderById(id).setTrashed(true);
          sheet.getRange(i + 1, 14 + o).setValue('נמחק');
          sheet.getRange(i + 1, 15 + o).setValue('');
        } catch (err) {}
      }
    }
  });
}
📥 ייבוא נרשמים מ-Google Forms
בגיליון התשובות: File ← Download ← Comma-separated values (.csv). לאחר מכן לבחור כאן את הקובץ שהורד — אין צורך לפתוח אותו.
הנרשמים ייובאו להכשרה המסומנת כעת למעלה: —
📄 קריאה ישירה מהגיליון (מומלץ)
בלי להוריד קובץ. המערכת קוראת את הלשונית מהגיליון עצמו.
או לבחור קובץ CSV שהורד:
או להדביק את התוכן ידנית
🗂️ קבוצות ההדרכה בהכשרה הפעילה
קבוצה שסומנה כמלאה לא תוצג לנרשמים חדשים.
👩‍🏫 ניהול מדריכים
📋 ייבוא רשימה
🔒 סיסמת המערכת
יש סיסמה אחת בלבד, והיא נשמרת ונבדקת בתוך Google — לא כאן.
לשינוי: ב-Apps Script → הגדרות הפרויקט (⚙️) → Script Properties → הערך של ADMIN_PASSWORD.