Magie des Kalendermanagements
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
arteurope
←
Älter
Neuer
→