Untitled
unknown
plain_text
10 months ago
10 kB
12
Indexable
/* * Time out email should follow the email thread * Yellow color should be the same one currently in use * Detention emails should be calculated based on appointment time plus one hour for stg and two if else */ /********************** * CONFIG & CONSTANTS * **********************/ const TZ = "America/New_York"; // Hard-coded recipients (Phase 1: Matson Jamesburg) const EMAIL_TO = "[email protected]"; // e.g., "[email protected]" const EMAIL_CC = "[email protected]"; // e.g., "[email protected]" or "" // Column indices (1-based) const COL = { STATUS: 1, // A — dispatcher notes ("arr 0700" then "arr 0700-0900") CUSTOMER: 4, // D — must equal "Matson Jamesburg" LOAD: 7, // G — required TOWN: 12, // L — required ARR_DD: 28, // AB — arrival de-dupe time (plain "HHMM") DEP_DD: 29, // AC — departure de-dupe time (plain "HHMM") THREAD_ID:30 //AD - store Gmail thread id }; const REQUIRED_CUSTOMER = "MATSON"; // Visuals const PARTIAL_HL_END_COL = COL.LOAD; // highlight A→G on arrival const HIGHLIGHT_COLOR = "#fff59d"; // light yellow /**************************** * INSTALLABLE ON-EDIT HOOK * ****************************/ function onEdit(e) { try { if (!e || !e.range || e.range.getNumRows() !== 1 || e.range.getNumColumns() !== 1) return; const sheet = e.range.getSheet(); if (!todayMatchesTabName(sheet.getName(), TZ)) return; const col = e.range.getColumn(); const row = e.range.getRow(); if (col !== COL.STATUS) return; // only react to Column A edits // Read row values we need const loadNum = sheet.getRange(row, COL.LOAD).getValue(); const town = sheet.getRange(row, COL.TOWN).getValue(); const customer= sheet.getRange(row, COL.CUSTOMER).getValue(); const status = sheet.getRange(row, COL.STATUS).getValue(); const arrDD = sheet.getRange(row, COL.ARR_DD).getValue(); const depDD = sheet.getRange(row, COL.DEP_DD).getValue(); let threadId = sheet.getRange(row, COL.THREAD_ID).getValue(); // Guardrails if ((customer || "").toString().trim() !== REQUIRED_CUSTOMER) return; if (!stringPresent(loadNum)) return; if (!stringPresent(town)) return; const times = parseTimes(status); if (!times.arrival && !times.departure) return; const subject = buildSubject(loadNum, "Jamesburg NJ"); // Branching logic with de-dupe const needArrival = times.arrival && !hhmmEquals(times.arrival, arrDD); const needDeparture = times.departure && !hhmmEquals(times.departure, depDD); // Both arrival & departure present in the cell if (times.arrival && times.departure) { if (needArrival && needDeparture) { const body = "Time in " + times.arrival + "\n" + "Time out " + times.departure; threadId = sendNewAndGetThreadId(EMAIL_TO, EMAIL_CC, subject, body) //sendEmail(EMAIL_TO, EMAIL_CC, subject, body); // Write de-dupe stamps setCell(sheet, row, COL.ARR_DD, times.arrival); setCell(sheet, row, COL.DEP_DD, times.departure); if (threadId) setCell(sheet, row, COL.THREAD_ID, threadId); // Full-row highlight highlightFull(sheet, row); } else if (!needArrival && needDeparture) { const body = "Time out " + times.departure; // Replying to existing thread; if missing, fallback to new + store id if (threadId) { replyToThread(threadId, body, EMAIL_CC); } else { threadId = sendNewAndGetThreadId(EMAIL_TO, EMAIL_CC, subject, body) if (threadId) setCell(sheet, row, COL.THREAD_ID, threadId) } setCell(sheet, row, COL.DEP_DD, times.departure); highlightFull(sheet, row); } else if (needArrival && !needDeparture) { const body = "Time in " + times.arrival; // New messge for arrival; capture thread id threadId = sendNewAndGetThreadId(EMAIL_TO, EMAIL_CC, subject, body); setCell(sheet, row, COL.ARR_DD, times.arrival); if (threadId) setCell(sheet, row, COL.THREAD_ID, threadId); highlightPartial(sheet, row); } else { // Neither needed (both same as de-dupe) → do nothing return; } return; } // Arrival-only present if (times.arrival && !times.departure) { if (!needArrival) return; const body = "Time in " + times.arrival; // New message for arrival; capture thread id threadId = sendNewAndGetThreadId(EMAIL_TO, EMAIL_CC, subject, body); setCell(sheet, row, COL.ARR_DD, times.arrival); if (threadId) setCell(sheet, row, COL.THREAD_ID, threadId) highlightPartial(sheet, row); return; } // (We don't support "dep-only" input per your spec; departures come via "-HHMM" append) } catch (err) { console.error("onEdit error:", err); } } /***************** * HELPER FUNCS * *****************/ function todayMatchesTabName(sheetName, tz) { const now = new Date(); const fullDay = Utilities.formatDate(now, tz, "EEEE"); // e.g., "Friday" const shortDay = Utilities.formatDate(now, tz, "EEE"); // e.g., "Fri" const md = Utilities.formatDate(now, tz, "M/d"); // e.g., "8/29" // Accepted variants: const candidates = [ `${fullDay} ${md}`, `${shortDay} ${md}`, `${fullDay} ${md.replace("/", "_")}`, `${shortDay} ${md.replace("/", "_")}` ].map(s => s.toLowerCase().trim()); const name = (sheetName || "").toString().toLowerCase().trim(); return candidates.includes(name); } // Parse "arr 0700" or "arr 07:00-09:00" (tolerate spaces). // Returns: { arrival: "HHMM" | null, departure: "HHMM" | null } function parseTimes(statusText) { if (!statusText) return { arrival: null, departure: null }; let s = statusText.toString().trim().toLowerCase(); // Must start with "arr" if (!/^arr\b/.test(s)) return { arrival: null, departure: null }; // Remove any extra spaces around dash; normalize "arr" block // Examples handled: "arr 07:00-09:00", "arr 7:00 - 9:00", "arr 0700-0900" s = s.replace(/\s*-\s*/g, "-").replace(/\s+/g, " "); // Extract after 'arr' const afterArr = s.replace(/^arr\s*/, ""); // e.g., "07:00-09:00" or "0700" let arrival = null, departure = null; if (afterArr.includes("-")) { const [aRaw, dRaw] = afterArr.split("-"); arrival = normalizeToHHMM(aRaw); departure = normalizeToHHMM(dRaw); } else { arrival = normalizeToHHMM(afterArr); } if (!isValidHHMM(arrival)) arrival = null; if (!isValidHHMM(departure)) departure = null; return { arrival, departure }; } // Normalize inputs like "07:00" → "0700", "7:00" → "0700", "700" → "0700" function normalizeToHHMM(raw) { if (!raw && raw !== 0) return null; let txt = raw.toString().trim(); // If contains colon, split hours:minutes if (txt.includes(":")) { const parts = txt.split(":"); if (parts.length < 2) return null; let h = parts[0].replace(/\D/g, ""); let m = parts[1].replace(/\D/g, ""); if (h.length === 0 || m.length === 0) return null; h = h.padStart(2, "0"); m = m.padStart(2, "0"); return `${h}${m}`; } // Digits-only path (could be "700", "0700", "0900") txt = txt.replace(/\D/g, ""); if (txt.length === 3) { // e.g., "700" → "0700" txt = "0" + txt; } else if (txt.length !== 4) { return null; } return txt; } function isValidHHMM(s) { if (!s || typeof s !== "string" || s.length !== 4) return false; if (!/^\d{4}$/.test(s)) return false; const h = parseInt(s.slice(0, 2), 10); const m = parseInt(s.slice(2, 4), 10); return h >= 0 && h <= 23 && m >= 0 && m <= 59; } function hhmmEquals(a, b) { const aa = (a || "").toString().trim(); const bb = (b || "").toString().trim(); return aa === bb && aa !== ""; } function buildSubject(loadNum, town) { return `${loadNum} / ${town} - Updates`; } function sendNewAndGetThreadId(to, cc, subject, body) { // send message first if (cc && cc.toString().trim() != "") { GmailApp.sendEmail(to, subject, body, { cc: cc}); } else { GmailApp.sendEmail(to, subject, body); } // Find the newly created thread // Narrow the search window to reduce ambiguity const q = [ `from:me`, `to:"${to}"`, `subject:"${escapeForQuery(subject)}"`, `newer_than:3d` ].join(' '); const threads = GmailApp.search(q, 0, 5); if (!threads || threads.length === 0) return ""; // Heuristic: pick the most recent thread with exact subject match for (let i = 0; i < threads.length; i++) { const th = threads[i]; if (th.getFirstMessageSubject() === subject) { return th.getId(); } } // Fallback to the newest thread return threads[0].getId(); } /** * Replies into an existing thread by id, keeping the conversation intact. * Adds CC if provided (GmailThread.reply honors cc option). */ function replyToThread(threadId, body, cc) { try { const thread = GmailApp.getThreadById(threadId); if (!thread) throw new Error("Thread not found for id: " + threadId); if (cc && cc.toString().trim() !== "") { thread.reply(body, { cc: cc }); } else { thread.reply(body); } } catch (err) { console.error("replyToThread error:", err); throw err; } } // Escape quotes for Gmail search query function escapeForQuery(s) { return (s || "").toString().replace(/"/g, '\\"'); } function setCell(sheet, row, col, value) { sheet.getRange(row, col).setValue(value); } function highlightPartial(sheet, row) { sheet.getRange(row, 1, 1, PARTIAL_HL_END_COL).setBackground(HIGHLIGHT_COLOR); } function highlightFull(sheet, row) { const lastCol = sheet.getLastColumn(); sheet.getRange(row, 1, 1, lastCol).setBackground(HIGHLIGHT_COLOR); } function stringPresent(v) { return v !== null && v !== undefined && v.toString().trim() !== ""; }
Editor is loading...
Leave a Comment