Magie des Kalendermanagements

Datum

Scripting

Jedes Service-Angebot verkauft Zeiten und die organisiert man am einfachsten in Kalendern, auf die man von überall her zugreifen kann. Sobald es aber komplexer wird, Dienstleistungen und ihre Zeiten in Buchungsvorgänge umzuwandeln, greifen viele wie selbstverständlich zu kommerziellen Programmen mit regelmäßigen Abonnementkosten und dies nicht zu knapp. Wenn der Laden läuft, gar kein Problem, aber was ist mit New-Comern, Studenten mit guten Ideen, start-ups, oder mein Serviceangebot unter meiner neuen shopping Seite malamadita.de. In diesem Artikel zeige ich euch, wie man mit den Möglichkeiten von open Source und Googles Apps Script es hinbekommt, Termine des nicht-öffentlichen Google-Kalenders Cal1, das ist beispielhaft ein Kalender, den du selbst bei Google eingerichtet hast, automatisch in dem öffentlichen Google-Kalender Cal-x, das ist beispielhaft der von dir in Google eingerichtete Kalender, der über iframe auf deiner Webseite erscheint, als "busy" (belegt) sichtbar werden zu lassen, so dass alle Kalender, z.B. Cal1, Cal2, Cal3... ihre belegten Zeiten in einem öffentlichen Kalender sichtbar werden und damit freie Zeiten gemanagt bzw angefragt werden können.
Open source Lösungen versprechen einen sehr robusten und datensicheren Umgang. Meist lässt es sich genauso intuitiv und automatisch einrichten, wie kommerzielle Lösungen, doch genau dieser Komfort ist oft nicht von Dauer. Die aus vielen Komponenten erstellte open source Komplettlösung wird in ihren Teilprogrammen oft von der freien Community unterschiedlich geupdated. Dadurch kann und wird es auch passieren, dass eine hochkomplexe, zusammengestellte Lösung irgendwann nicht mehr richtig funktioniert. Der Trick ist es halbautomatisiert zu arbeiten, nicht alles in allem Zugriff und Eingriffmöglichkeiten zu geben, ein paar Knöpfe regelmäßig selber zu drehen. Die Alternative wäre die unterschiedlichen librarys und scripte der eingesetzten Open Source Programme in einem einzigen Programm zusammen zu führen und sich fortan mit updates selber herumzuschlagen - keine wirkliche Lösung für einen Hobbyscripter.
Hier stelle ich einen Teil der Lösung vor, wie man im Dienstleistungssektor für verschiedene Mandanten und in unterschiedlichen Bereichen und Anforderungen eine semiautomatisierte Lösung zur Tätigkeitsdokumentation, Zeit- und Wegeerfassung, Rechnungstellung, Buchungs- und Auftragsmanagement mit open source stabil und ziemlich datensicher auf die Beine stellt. Ich nutze Thunderbird Lightning als open source Kalendersoftware auf meinem Rechner, die sich mit der Google Kalender Api synchronisiert. Ich weiß, Google und Datensicherheit - natürlich wird es in diesem Konzern, wie in jedem anderen, ein paar geben, die ihre Möglichkeiten ausnutzen. In erster Linie wollen wir aber nicht von Hinz und Kunz ausspioniert werden. Hinz und Kunz, dazu gehören auch alle üblichen Behörden.
Ein robustes Code.gs Apps Script , das du sofort einsetzen kannst: es synchronisiert nur Frei/Belegt-Zeiten und vermeidet Duplikate, indem es Quell-Event-IDs in der Beschreibung speichert. Später lässt sich das Script problemlos auf (z. B.) 12 Quellkalender skalieren. 1. Melde dich mit dem Google-Account an, der Zugriff auf beide Kalender hat (idealerweise Besitzer oder mit Schreibrechten für malamadita und Leserechte für johann franki ). 2. Öffne Google Kalender → Einstellungen → bei jedem Kalender → Einstellungen und Freigabe : Cal-x : Unter „Zugriffsberechtigungen“: "Nur Frei/Belegt (keine Details) für Öffentlichkeit anzeigen" ist okay — das betrifft das Anzeigen über die Website. Aber wichtig: der Account, mit dem du das Script ausführst, muss auf malamadita schreibend zugreifen dürfen (also Besitzer oder Editor). Cal1 : stelle sicher, dass der Script-Account Leserechte hat (in deinem Fall ist es derselbe Account, also in der Regel fine). 3. Kalender-IDs merken: Kalender → Einstellungen -> „Kalender integrieren“ → dort steht die Kalender-ID (z.B. ...@group.calendar.google.com oder eine gmail- Adresse). Du brauchst: SOURCE_CALENDAR_ID (Cal1) und TARGET_CALENDAR_ID (Cal-x). 2) Neues Google Apps Script anlegen 1. Gehe zu https://script.google.com Neues Projekt . 2. Nenne es z. B. cal-x-busy-sync . 4 / 25 javascript /** * Sync private calendars to a public "Busy-only" calendar. * - Stores original sourceEventId inside the created target event description * - Updates existing target events when times change, creates new ones when needed * - Skips all-day events (optional) — change if you want to include them * * Set SOURCE_CALENDARS = [ 'Cal1_calendar_id' ] * Set TARGET_CALENDAR_ID = 'Cal-x_calendar_id' */ const SOURCE_CALENDARS = [ 'Cal1_calendar_id_here' // replace with actual calendar ID ]; const TARGET_CALENDAR_ID = 'Cal-x_calendar_id_here' ; // replace with actual calendar ID /** * Helper: Parse a stored marker from description to find source event * Format used: "SYNC_FROM:sourceCalendarId|sourceEventId" */ function makeMarker ( sourceCalId, sourceEventId ) { return 'SYNC_FROM:' + sourceCalId + '|' + sourceEventId; } function parseMarker ( desc ) { if (!desc) return null ; const m = desc. match ( /SYNC_FROM:([^|]+)\|(.+?)(\s|$)/ ); if (!m) return null ; return { sourceCalendarId : m[ 1 ], sourceEventId : m[ 2 ] }; } /** * Main sync function. */ function syncBusyTimes ( ) { const now = new Date (); const future = new Date (); future. setMonth (future. getMonth () + 3 ); // Sync 3 Monate vorwärts (anpassbar) const targetCal = CalendarApp . getCalendarById ( TARGET_CALENDAR_ID ); if (!targetCal) { Logger . log ( 'TARGET calendar not found: ' + TARGET_CALENDAR_ID ); return ; } SOURCE_CALENDARS . forEach ( sourceCalId => { const sourceCal = CalendarApp . getCalendarById (sourceCalId); . filter ( e => e. getId () === parsed. sourceEventId ); if (matches. length === 0 ) { // source event missing — delete target to keep things clean try { te. deleteEvent (); } catch (e) { Logger . log ( 'Could not delete orphan target event: ' + e); } } } }); }); Logger . log ( 'Sync finished at ' + new Date ()); } function createBusyEvent ( targetCal, start, end, sourceCalId, sourceEventId ) { // Create with minimal title and marker in description. const ev = targetCal. createEvent ( 'Busy' , start, end, { description : makeMarker (sourceCalId, sourceEventId)}); // Set visibility (PRIVATE) — final visibility to public is controlled by calendar sharing settings. try { ev. setVisibility ( CalendarApp . Visibility . PRIVATE ); } catch (e) { /* ignore */ } } Wichtig: Ersetze Cal1_calendar_id_here und Cal-x_calendar_id_here durch die echten Kalender-IDs (siehe Schritt 1). 4) Authorisierung & Testlauf 1. Speichere das Script (Strg+S). 2. Wähle syncBusyTimes im Dropdown oben und klicke auf Run. 3. Beim ersten Lauf fordert Google die Autorisierung an (Berechtigungen für Calendar). Erlaube sie mit dem Account, der Zugriff hat. 4. Prüfe im Zielkalender Cal-x , ob neue Events mit Titel "Busy" angelegt wurden (sie haben in der Beschreibung den Marker SYNC_FROM:... ). 5. Teste durch Erstellen eines Termins in Ca1 und führe das Script erneut aus — der Termin sollte im Ziel erscheinen. Ändere die Zeit und führe wieder aus — das Ziel-Event wird aktualisiert (via Lösch+Neu). 5) Trigger einrichten (Automatisierung) 1. In Apps Script links → Triggers (Auslöser) Trigger hinzufügen . 8 / 25 syncBusyTimes Logger.log(...) SOURCE_CALENDARS 9) Optionale Verbesserungen (wenn du willst) Ignoriere bestimmte Termine (z. B. mit Label "privat-sync-exclude") — das Script könnte Titel/Description prüfen. Synchronisiere auch ganztägige Einträge (aktuell übersprungen). Schreibe ein kleines Log-Sheet in Google Sheets, das Sync-Stats speichert. Wenn du möchtest, mache ich dir jetzt 3 Dinge direkt (sofort, in dieser Antwort): 1. Ersetze die Platzhalter-IDs im Script, wenn du mir die beiden Kalender-IDs hier gibst. 2. Passe das Script an, damit ganztägige Events mit synchronisiert werden. 3. Baue eine kleine Cleanup-Routine ein, die älter als 6 Monate liegende Busy-Events entfernt. Sag mir kurz, welche der 3 du jetzt willst — oder kopiere die beiden Kalender-IDs hier (oder setze sie selbst ins Script) und du kannst sofort loslegen. Ich bin bei 4.3 Es kommt folgende Meldung: Google hasn’t verified this app The app is requesting access to sensitive info in your Google Account. Until the developer (dein Konto) verifies this app with Google, you shouldn't use it. Perfekt, du bist genau an der richtigen Stelle – und diese Meldung ist ganz normal , sobald du ein eigenes Google Apps Script zum ersten Mal ausführst. Ich erkläre dir präzise, was das bedeutet, warum das sicher ist und wie du trotzdem fortfahren kannst (ohne Risiko, solange das Script dein eigenes ist). 🧩 Warum diese Warnung erscheint Google zeigt das immer an, wenn ein Script: auf sensible Daten zugreift (z. B. Kalender, Gmail, Drive), nicht über die Google-OAuth-Verification freigegeben wurde, und nicht aus dem Google Workspace Marketplace stammt. In deinem Fall hast du selbst das Script erstellt – es läuft unter deinem eigenen Konto ( Dein_Konto ). Google kennt es also nicht als „verifizierte App“, aber du bist gleichzeitig der Entwickler und der Nutzer . Damit ist das völlig unbedenklich. 10 / 25 So gehst du jetzt vor (sicher und richtig) 1. In dem Fenster, in dem steht: “Google hasn’t verified this app” klicke auf „Advanced“ (links unten, klein geschrieben). 2. Danach erscheint ein neuer Textabschnitt mit dem Hinweis: “Go to malamadita-busy-sync (unsafe)” oder ähnlich (mit deinem Projektnamen). 👉 Klicke genau darauf. 3. Danach wirst du gefragt, welche Berechtigungen du erteilen willst. Wähle dein Konto aus. 4. Google zeigt dir, dass das Script Kalenderdaten lesen/schreiben darf – das ist gewollt. Klicke auf „Allow“ / „Zulassen“ . 5. Danach wird das Script normal ausgeführt. Beim ersten Lauf kann es ein paar Sekunden dauern, bevor du in Apps Script wieder Kontrolle hast. 🔒 Wichtiger Hinweis zur Sicherheit Da du das Script selbst in deinem eigenen Google-Konto erstellt hast: läuft es nur unter deinem Login , greift nur auf deine eigenen Kalender zu, und keine Daten verlassen dein Konto oder Google-Server. Das ist also keine externe App , sondern deine persönliche, lokale Automatisierung. Erst wenn du dieses Script irgendwann für andere Accounts freigibst (z. B. Teammitglieder mit anderen Google-Logins), wäre eine Verifizierung sinnvoll – aber für dich allein ist das unnötig. 🧭 Nach dem Bestätigen Wenn du die Autorisierung erlaubt hast: das Script läuft und legt ggf. die ersten “Busy”-Einträge im malamadita -Kalender an, du kannst im Menü oben in Apps Script → „View“ → „Executions“ oder „Logs“ die Ausführung kontrollieren. 11 / 25 Ich passe das Script so an, dass ganztägige Termine (All-day Events) aus johann franki ebenfalls als „Busy“-Blöcke im öffentlichen Kalender malamadita erscheinen. Dabei bleibt das Verhalten für normale Termine gleich. 🔧 Was sich ändert Im ursprünglichen Script gab es diese Zeile: javascript if (src. isAllDayEvent ()) return ; Das hat die Synchronisation für ganztägige Events übersprungen. Wir entfernen diese Zeile und sorgen dafür, dass sie korrekt im Zielkalender angelegt werden. Aktualisierte Version Hier ist das vollständige (bereinigte) Script mit All-Day-Support — du kannst den bisherigen Code komplett ersetzen: 12 / 25 javascript /** * Sync private calendars to a public "Busy-only" calendar. * - Supports timed and all-day events * - Avoids duplicates by storing a unique marker in description * - Updates events if start/end times change * * Replace IDs below with your real calendar IDs. */ const SOURCE_CALENDARS = [ 'johann_franki_calendar_id_here' ]; const TARGET_CALENDAR_ID = 'malamadita_calendar_id_here' ; function makeMarker ( sourceCalId, sourceEventId ) { return 'SYNC_FROM:' + sourceCalId + '|' + sourceEventId; } function parseMarker ( desc ) { if (!desc) return null ; const m = desc. match ( /SYNC_FROM:([^|]+)\|(.+?)(\s|$)/ ); if (!m) return null ; return { sourceCalendarId : m[ 1 ], sourceEventId : m[ 2 ] }; } function syncBusyTimes ( ) { const now = new Date (); const future = new Date (); future. setMonth (future. getMonth () + 3 ); // 3 Monate im Voraus synchronisieren const targetCal = CalendarApp . getCalendarById ( TARGET_CALENDAR_ID ); if (!targetCal) { Logger . log ( 'TARGET calendar not found: ' + TARGET_CALENDAR_ID ); return ; } SOURCE_CALENDARS . forEach ( sourceCalId => { const sourceCal = CalendarApp . getCalendarById (sourceCalId); if (!sourceCal) { Logger . log ( 'SOURCE calendar not found: ' + sourceCalId); return ; } const sourceEvents = sourceCal. getEvents (now, future); const targetEvents = targetCal. getEvents (now, future); sourceEvents. forEach ( src => { const srcId = src. getId (); const srcStart = src. getStartTime (); 13 / 25 const srcEnd = src. getEndTime (); const srcAllDay = src. isAllDayEvent (); // Find matching target event let matched = null ; for ( let te of targetEvents) { const parsed = parseMarker (te. getDescription ()); if (parsed && parsed. sourceCalendarId === sourceCalId && parsed. sourceEventId === srcId) { matched = te; break ; } } if (matched) { const tStart = matched. getStartTime (); const tEnd = matched. getEndTime (); const tAllDay = matched. isAllDayEvent (); if (tStart. getTime () !== srcStart. getTime () || tEnd. getTime () !== srcEnd. getTime () || tAllDay !== srcAllDay) { try { matched. deleteEvent (); } catch (e) { Logger . log ( 'Could not delete matched event: ' + e); } createBusyEvent (targetCal, srcStart, srcEnd, srcAllDay, sourceCalId, srcId); } } else { createBusyEvent (targetCal, srcStart, srcEnd, srcAllDay, sourceCalId, srcId); } }); // Cleanup orphaned target events targetEvents. forEach ( te => { const parsed = parseMarker (te. getDescription ()); if (parsed && parsed. sourceCalendarId === sourceCalId) { const matches = sourceCal. getEvents (te. getStartTime (), te. getEndTime ()) . filter ( e => e. getId () === parsed. sourceEventId ); if (matches. length === 0 ) { try { te. deleteEvent (); } catch (e) { Logger . log ( 'Could not delete orphan target event: ' + e); } } } }); }); Logger . log ( 'Sync finished at ' + new Date ()); } function createBusyEvent ( targetCal, start, end, allDay, sourceCalId, sourceEventId ) { let ev; if (allDay) { ev = targetCal. createAllDayEvent ( 'Busy' , start, { description : makeMarker (sourceCalId, sourceEventId) }); } else { ev = targetCal. createEvent ( 'Busy' , start, end, { description : 14 / 25 makeMarker (sourceCalId, sourceEventId) }); } try { ev. setVisibility ( CalendarApp . Visibility . PRIVATE ); } catch (e) { /* ignore */ } } 🧭 Was du jetzt tust 1. Ersetze im Code deine Kalender-IDs: 2. Speichern → erneut ausführen ( syncBusyTimes ) 3. Falls erneut eine Auth-Warnung kommt → wie vorhin „Advanced → Go to ... (unsafe) → Allow“. 15 / 25 🔹 javascript /** * Sync multiple private calendars to public "Busy-only" calendar (malamadita) * - Supports timed and all-day events * - Keeps only current year's events (Jan 1 - Dec 31) * - Deletes last year's Busy events after May 31 of following year * * To scale: just add up to 12 calendar IDs in SOURCE_CALENDARS */ const SOURCE_CALENDARS = [ 'johann_franki_calendar_id_here' , 'calendar2_id_here' , 'calendar3_id_here' , // ... 'calendar12_id_here' ]; const TARGET_CALENDAR_ID = 'malamadita_calendar_id_here' ; function makeMarker ( sourceCalId, sourceEventId ) { return 'SYNC_FROM:' + sourceCalId + '|' + sourceEventId; } function parseMarker ( desc ) { if (!desc) return null ; const m = desc. match ( /SYNC_FROM:([^|]+)\|(.+?)(\s|$)/ ); if (!m) return null ; return { sourceCalendarId : m[ 1 ], sourceEventId : m[ 2 ] }; } function syncBusyTimes ( ) { const today = new Date (); const currentYear = today. getFullYear (); const startOfYear = new Date (currentYear, 0 , 1 ); const endOfYear = new Date (currentYear, 11 , 31 , 23 , 59 , 59 ); const targetCal = CalendarApp . getCalendarById ( TARGET_CALENDAR_ID ); if (!targetCal) return ; SOURCE_CALENDARS . forEach ( sourceCalId => { const sourceCal = CalendarApp . getCalendarById (sourceCalId); if (!sourceCal) return ; const sourceEvents = sourceCal. getEvents (startOfYear, endOfYear); const targetEvents = targetCal. getEvents (startOfYear, endOfYear); sourceEvents. forEach ( src => { const srcId = src. getId (); const srcStart = src. getStartTime (); const srcEnd = src. getEndTime (); const srcAllDay = src. isAllDayEvent (); // check for existing target event let matched = null ; for ( let te of targetEvents) { const parsed = parseMarker (te. getDescription ()); if (parsed && parsed. sourceCalendarId === sourceCalId && parsed. sourceEventId === srcId) { matched = te; break ; } } if (matched) { const tStart = matched. getStartTime (); const tEnd = matched. getEndTime (); const tAllDay = matched. isAllDayEvent (); if (tStart. getTime () !== srcStart. getTime () || tEnd. getTime () !== srcEnd. getTime () || tAllDay !== srcAllDay) { try { matched. deleteEvent (); } catch (e) {} createBusyEvent (targetCal, srcStart, srcEnd, srcAllDay, sourceCalId, srcId); } } else { createBusyEvent (targetCal, srcStart, srcEnd, srcAllDay, sourceCalId, srcId); } }); // cleanup orphaned target events for this source targetEvents. forEach ( te => { const parsed = parseMarker (te. getDescription ()); if (parsed && parsed. sourceCalendarId === sourceCalId) { const matches = sourceCal. getEvents (te. getStartTime (), te. getEndTime ()) . filter ( e => e. getId () === parsed. sourceEventId ); if (matches. length === 0 ) { try { te. deleteEvent (); } catch (e) {} } } }); }); // cleanup previous year after May 31 const cutoffMonth = 4 ; // May const cutoffDay = 31 ; if (today. getMonth () >= cutoffMonth && today. getDate () >= cutoffDay) { const lastYear = currentYear - 1 ; const lastStart = new Date (lastYear, 0 , 1 ); const lastEnd = new Date (lastYear, 11 , 31 , 23 , 59 , 59 ); const oldEvents = targetCal. getEvents (lastStart, lastEnd); oldEvents. forEach ( ev => { try { ev. deleteEvent (); } catch (e) {} }); } } function createBusyEvent ( targetCal, start, end, allDay, sourceCalId, sourceEventId ) { if (allDay) { const ev = targetCal. createAllDayEvent ( 'Busy' , start, { description : makeMarker (sourceCalId, sourceEventId) }); try { ev. setVisibility ( CalendarApp . Visibility . PRIVATE ); } catch (e) {} } else { const ev = targetCal. createEvent ( 'Busy' , start, end, { description : makeMarker (sourceCalId, sourceEventId) }); try { ev. setVisibility ( CalendarApp . Visibility . PRIVATE ); } catch (e) {} } } SOURCE_CALENDARS 󾠮 󾠯 getEvents 󾠰 󾠱




weitere Artikel zum Thema

← Älter Neuer →