350 lines · 13.1 KB
Raw Download
1
// Helper function to get calendars
2
function getCalendars() {
3
  const myCalendar = CalendarApp.getDefaultCalendar();
4
  const workCalendarId = "email@gmail.com"; // change this email address to yours
5
  const workCalendar = CalendarApp.getCalendarById(workCalendarId);
6
  return { myCalendar, workCalendar };
7
}
8
9
function scheduleWellnessEventsSmart() {
10
  const dryRun = true; // set to false to actually create events
11
  const scheduleGetFit = true; // set to false to skip "Get Fit" events
12
  const scheduleWristStretch = true; // set to false to skip "Wrist stretch" events
13
  const scheduleLegStretch = true; // set to false to skip "Leg stretch" events
14
  
15
  const { myCalendar, workCalendar } = getCalendars();
16
17
  const numDaysAhead = 5;
18
  const now = new Date();
19
  const seriesId = "wellness-series-" + now.toISOString().split("T")[0];
20
21
  for (let offset = 0; offset < numDaysAhead; offset++) {
22
    const day = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset);
23
    const weekday = day.getDay();
24
25
    Logger.log(`\n======================`);
26
    Logger.log(`πŸ“… Checking ${day.toDateString()}`);
27
    
28
    // Schedule leg stretches for ALL days (including weekends)
29
    if (scheduleLegStretch) {
30
      scheduleLegStretchEvents(day, myCalendar, seriesId, dryRun);
31
    }
32
    
33
    // Skip weekday-only events on weekends
34
    if (weekday === 0 || weekday === 6) {
35
      Logger.log(`⏭️ Skipping weekday wellness events (weekend)`);
36
      continue;
37
    }
38
39
    scheduleDayEvents(day, myCalendar, workCalendar, seriesId, dryRun, scheduleGetFit, scheduleWristStretch);
40
  }
41
42
  Logger.log(`\n✨ Done! DryRun = ${dryRun}`);
43
}
44
45
46
function scheduleDayEvents(date, myCal, workCal, seriesId, dryRun, scheduleGetFit, scheduleWristStretch) {
47
  const startHour = 8;
48
  const endHour = 16;
49
  const dayStr = date.toDateString();
50
51
  // ---- STEP 1: Check if wellness events already exist ----
52
  const existingWellness = myCal
53
    .getEventsForDay(date)
54
    .filter(e => e.getTitle().includes("[Wellness]") && !e.getTitle().includes("Leg stretch"));
55
56
  if (existingWellness.length > 0) {
57
    Logger.log(`🚫 Skipping ${dayStr} β€” already has ${existingWellness.length} wellness events.`);
58
    existingWellness.forEach(e =>
59
      Logger.log(`   β€’ ${e.getTitle()} (${fmt(e.getStartTime())} β†’ ${fmt(e.getEndTime())})`)
60
    );
61
    return; // Skip the rest of the logic for this day
62
  }
63
64
  // ---- STEP 2: Collect all events for that day ----
65
  const allEvents = [
66
    ...myCal.getEventsForDay(date),
67
    ...(workCal ? workCal.getEventsForDay(date) : []),
68
  ];
69
70
  Logger.log(`πŸ•‘ Found ${allEvents.length} total events for ${dayStr}`);
71
72
  // Filter out all-day events (those that start/end at midnight)
73
  const busySlots = allEvents
74
    .filter(e => !isAllDay(e))
75
    .filter(e => e.getTitle() !== "Block")
76
    .map(e => ({ start: e.getStartTime(), end: e.getEndTime(), title: e.getTitle() }))
77
    .sort((a, b) => a.start - b.start);
78
79
  const ignoredAllDay = allEvents.filter(e => isAllDay(e));
80
  if (ignoredAllDay.length > 0) {
81
    Logger.log(`πŸ›‘ Ignored ${ignoredAllDay.length} all-day events:`);
82
    ignoredAllDay.forEach(e => Logger.log(`   β€’ ${e.getTitle()}`));
83
  }
84
85
  Logger.log(`πŸ“š Busy slots (${busySlots.length}):`);
86
  busySlots.forEach(b => Logger.log(`   - ${b.title} ${fmt(b.start)} β†’ ${fmt(b.end)}`));
87
88
  // Compute free gaps
89
  const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), startHour, 0);
90
  const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate(), endHour, 0);
91
  const freeGaps = [];
92
  let cursor = dayStart;
93
94
  for (const b of busySlots) {
95
    if (b.start > cursor) freeGaps.push({ start: new Date(cursor), end: new Date(b.start) });
96
    if (b.end > cursor) cursor = b.end;
97
  }
98
  if (cursor < dayEnd) freeGaps.push({ start: new Date(cursor), end: dayEnd });
99
100
  Logger.log(`🟩 Free gaps (${freeGaps.length}) for ${dayStr}:`);
101
  freeGaps.forEach(g =>
102
    Logger.log(`   gap: ${fmt(g.start)} β†’ ${fmt(g.end)} (${mins(g)} min)`)
103
  );
104
105
  // Helper: log or create events
106
  function createPairedEvents(title, start, durationMins) {
107
    const end = new Date(start.getTime() + durationMins * 60000);
108
    const fullTitle = `[Wellness] ${title}`;
109
    const desc = `Auto-created wellness event (${seriesId})`;
110
    if (dryRun) {
111
      Logger.log(`πŸ’‘ Would create "${fullTitle}" ${fmt(start)} β†’ ${fmt(end)}`);
112
    } else {
113
      const event = myCal.createEvent(fullTitle, start, end, {
114
        description: desc,
115
        reminders: { useDefault: false, overrides: [] },
116
      });
117
118
      if (workCal) {
119
        workCal.createEvent("Block", start, end, {
120
          description: `Auto-blocked for personal wellness (${seriesId})`,
121
        reminders: { useDefault: false, overrides: [] },
122
        });
123
      }
124
      Logger.log(`βœ… Created "${fullTitle}" ${fmt(start)} β†’ ${fmt(end)}`);
125
    }
126
  }
127
128
  // --- Schedule Get Fit (15 min) ---
129
  let fitGap = null;
130
  if (scheduleGetFit) {
131
    fitGap = freeGaps.find(g => g.end - g.start >= 15 * 60000);
132
    if (fitGap) {
133
      Logger.log(`πŸ‹οΈ Using gap ${fmt(fitGap.start)} β†’ ${fmt(fitGap.end)} for "Get Fit"`);
134
      createPairedEvents("Get Fit", fitGap.start, 15);
135
    } else {
136
      Logger.log(`⚠️ No room for "Get Fit" on ${dayStr}`);
137
    }
138
  } else {
139
    Logger.log(`⭐️ Skipping "Get Fit" (disabled)`);
140
  }
141
142
  // --- Rebuild gaps after "Get Fit" ---
143
  const newBusy = [
144
    ...busySlots,
145
    ...(fitGap ? [{ start: fitGap.start, end: new Date(fitGap.start.getTime() + 15 * 60000) }] : []),
146
  ].sort((a, b) => a.start - b.start);
147
148
  const updatedGaps = [];
149
  cursor = dayStart;
150
  for (const b of newBusy) {
151
    if (b.start > cursor) updatedGaps.push({ start: new Date(cursor), end: new Date(b.start) });
152
    if (b.end > cursor) cursor = b.end;
153
  }
154
  if (cursor < dayEnd) updatedGaps.push({ start: new Date(cursor), end: dayEnd });
155
156
  Logger.log(`πŸ”„ Updated free gaps after "Get Fit" (${updatedGaps.length}):`);
157
  updatedGaps.forEach(g =>
158
    Logger.log(`   gap: ${fmt(g.start)} β†’ ${fmt(g.end)} (${mins(g)} min)`)
159
  );
160
161
  // --- Schedule Wrist stretches (10 min) ---
162
  if (scheduleWristStretch) {
163
    let placed = 0;
164
    const desiredCount = 3;
165
    const usedTimes = []; // Track used time slots to avoid duplicates
166
167
    const ascendingGaps = [...updatedGaps].sort((a, b) => a.start - b.start);
168
    const descendingGaps = [...updatedGaps].sort((a, b) => b.start - a.start);
169
170
    // Helper to check if a time slot overlaps with already used times
171
    function isTimeUsed(start) {
172
      return usedTimes.some(used => Math.abs(used - start) < 10 * 60000);
173
    }
174
175
    // 1️⃣ Place one in the middle of the day (as close to noon as possible)
176
    const noon = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0);
177
    let closestGap = null;
178
    let closestStart = null;
179
    let closestDistance = Infinity;
180
181
    for (const gap of updatedGaps) {
182
      if (gap.end - gap.start >= 10 * 60000) { // Must fit 10 minutes
183
        // Try to place the stretch so it's centered around noon
184
        const idealStart = new Date(noon.getTime() - 5 * 60000); // 5 min before noon
185
        let candidateStart;
186
        
187
        // If ideal time fits in this gap, use it
188
        if (idealStart >= gap.start && new Date(idealStart.getTime() + 10 * 60000) <= gap.end) {
189
          candidateStart = idealStart;
190
        } else if (noon >= gap.start && noon < gap.end) {
191
          // Noon is in this gap, use gap start or noon (whichever fits)
192
          candidateStart = gap.start;
193
        } else {
194
          // Use the midpoint of the gap
195
          candidateStart = new Date((gap.start.getTime() + gap.end.getTime()) / 2 - 5 * 60000);
196
          if (candidateStart < gap.start) candidateStart = gap.start;
197
        }
198
        
199
        const distance = Math.abs(candidateStart - noon);
200
        
201
        if (distance < closestDistance) {
202
          closestDistance = distance;
203
          closestGap = gap;
204
          closestStart = candidateStart;
205
        }
206
      }
207
    }
208
209
    if (closestGap && closestStart) {
210
      createPairedEvents("Wrist stretch", closestStart, 10);
211
      usedTimes.push(closestStart);
212
      placed++;
213
      Logger.log(`πŸ•› Scheduled midday wrist stretch at ${fmt(closestStart)}`);
214
    }
215
216
    // 2️⃣ Place the latest one (as close to 4 pm as possible)
217
    for (const gap of descendingGaps) {
218
      if (placed >= desiredCount) break;
219
      const latestPossibleStart = new Date(gap.end.getTime() - 10 * 60000);
220
      if (latestPossibleStart >= gap.start && !isTimeUsed(latestPossibleStart)) {
221
        createPairedEvents("Wrist stretch", latestPossibleStart, 10);
222
        usedTimes.push(latestPossibleStart);
223
        placed++;
224
        Logger.log(`πŸ•“ Scheduled late-day wrist stretch at ${fmt(latestPossibleStart)}`);
225
        break;
226
      }
227
    }
228
229
    // 3️⃣ Fill remaining earlier in the day
230
    for (const gap of ascendingGaps) {
231
      if (placed >= desiredCount) break;
232
      let start = gap.start;
233
      while (placed < desiredCount && start.getTime() + 10 * 60000 <= gap.end.getTime()) {
234
        if (!isTimeUsed(start)) {
235
          createPairedEvents("Wrist stretch", start, 10);
236
          usedTimes.push(start);
237
          placed++;
238
          Logger.log(`πŸ’ͺ Scheduled earlier wrist stretch at ${fmt(start)}`);
239
        }
240
        start = new Date(start.getTime() + 90 * 60000);
241
      }
242
    }
243
244
    if (placed < desiredCount)
245
      Logger.log(`⚠️ Only scheduled ${placed}/${desiredCount} Wrist stretches on ${dayStr}`);
246
    else
247
      Logger.log(`βœ… Scheduled all ${desiredCount} Wrist stretches for ${dayStr}`);
248
  } else {
249
    Logger.log(`⭐️ Skipping "Wrist stretch" events (disabled)`);
250
  }
251
}
252
253
// NEW FUNCTION: Schedule leg stretch events at 6am and 8pm
254
function scheduleLegStretchEvents(date, myCal, seriesId, dryRun) {
255
  const dayStr = date.toDateString();
256
  
257
  // Check if leg stretch events already exist for this day
258
  const existingLegStretches = myCal
259
    .getEventsForDay(date)
260
    .filter(e => e.getTitle().includes("Leg stretch and exercise"));
261
  
262
  if (existingLegStretches.length >= 2) {
263
    Logger.log(`🦡 Leg stretch events already exist for ${dayStr} (${existingLegStretches.length} found)`);
264
    return;
265
  }
266
  
267
  const times = [
268
    { hour: 6, minute: 0, label: "morning" },
269
    { hour: 20, minute: 0, label: "evening" }
270
  ];
271
  
272
  times.forEach(time => {
273
    const start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.hour, time.minute);
274
    const end = new Date(start.getTime() + 15 * 60000); // 15 minutes
275
    const fullTitle = "[Wellness] Leg stretch and exercise";
276
    const desc = `Auto-created wellness event (${seriesId})`;
277
    
278
    // Check if this specific time already has a leg stretch event
279
    const existingAtThisTime = existingLegStretches.some(e => 
280
      e.getStartTime().getHours() === time.hour && 
281
      e.getStartTime().getMinutes() === time.minute
282
    );
283
    
284
    if (existingAtThisTime) {
285
      Logger.log(`🦡 Leg stretch already exists at ${fmt(start)} on ${dayStr}`);
286
      return;
287
    }
288
    
289
    if (dryRun) {
290
      Logger.log(`πŸ’‘ Would create "${fullTitle}" at ${fmt(start)} β†’ ${fmt(end)} (${time.label})`);
291
    } else {
292
      myCal.createEvent(fullTitle, start, end, {
293
        description: desc,
294
        reminders: { useDefault: false, overrides: [] },
295
      });
296
      Logger.log(`βœ… Created "${fullTitle}" at ${fmt(start)} β†’ ${fmt(end)} (${time.label})`);
297
    }
298
  });
299
}
300
301
//
302
// Helpers
303
//
304
function fmt(d) {
305
  return Utilities.formatDate(d, Session.getScriptTimeZone(), "h:mm a");
306
}
307
function mins(g) {
308
  return Math.round((g.end - g.start) / 60000);
309
}
310
function isAllDay(event) {
311
  const start = event.getStartTime();
312
  const end = event.getEndTime();
313
  const dur = end - start;
314
  // Treat events that start/end at midnight or last >= 20h as all-day
315
  return (
316
    dur >= 20 * 60 * 60 * 1000 ||
317
    (start.getHours() === 0 && start.getMinutes() === 0 && end.getHours() === 0 && end.getMinutes() === 0)
318
  );
319
}
320
321
function removeAllWellnessEvents() {
322
  const { myCalendar, workCalendar } = getCalendars();
323
  
324
  const startDate = new Date();
325
  // remove past year
326
  // startDate.setFullYear(startDate.getFullYear() - 1);
327
  // remove from today forward
328
  startDate.setHours(0, 0, 0, 0);
329
330
  const endDate = new Date();
331
  endDate.setDate(endDate.getDate() + 30);
332
333
  const myEvents = myCalendar.getEvents(startDate, endDate);
334
  const workEvents = workCalendar ? workCalendar.getEvents(startDate, endDate) : [];
335
336
  myEvents.forEach(event => {
337
    if (event.getTitle().includes("[Wellness]") || event.getDescription().includes("Auto-created wellness event")) {
338
      Logger.log(`Deleting event "${event.getTitle()}" from ${myCalendar.getName()}`);
339
      event.deleteEvent();
340
    }
341
  });
342
343
  workEvents.forEach(event => {
344
    // Only delete "Block" events that have the wellness description
345
    if (event.getTitle() === "Block" && event.getDescription().includes("Auto-blocked for personal wellness")) {
346
      Logger.log(`Deleting event "${event.getTitle()}" from ${workCalendar.getName()}`);
347
      event.deleteEvent();
348
    }
349
  });
350
}