feat(calendar): implement local calendar invitations with email notifications
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m19s

- Added functionality to create, accept, decline, and delete local calendar invitations.
- Integrated email notifications for calendar event invitations and deletions.
- Updated database schema to support local invitations and outbound event tracking.
- Enhanced the calendar UI to display pending invitations and allow users to manage them.
- Localized new strings for invitations in English and German.
This commit is contained in:
2026-03-02 14:37:54 +01:00
parent d989e1291d
commit c2c10f9a4b
10 changed files with 606 additions and 18 deletions

View File

@@ -558,6 +558,65 @@ export async function initDatabase() {
`);
}
// Track outbound calendar event federation sends for deletion propagation
if (isPostgres) {
await db.exec(`
CREATE TABLE IF NOT EXISTS calendar_event_outbound (
id SERIAL PRIMARY KEY,
event_uid TEXT NOT NULL,
remote_domain TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(event_uid, remote_domain)
);
CREATE INDEX IF NOT EXISTS idx_cal_out_event_uid ON calendar_event_outbound(event_uid);
`);
} else {
await db.exec(`
CREATE TABLE IF NOT EXISTS calendar_event_outbound (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_uid TEXT NOT NULL,
remote_domain TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(event_uid, remote_domain)
);
CREATE INDEX IF NOT EXISTS idx_cal_out_event_uid ON calendar_event_outbound(event_uid);
`);
}
// Local calendar event invitations (share-with-acceptance flow)
if (isPostgres) {
await db.exec(`
CREATE TABLE IF NOT EXISTS calendar_local_invitations (
id SERIAL PRIMARY KEY,
event_id INTEGER NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
from_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(event_id, to_user_id)
);
CREATE INDEX IF NOT EXISTS idx_cal_local_inv_to_user ON calendar_local_invitations(to_user_id);
CREATE INDEX IF NOT EXISTS idx_cal_local_inv_event ON calendar_local_invitations(event_id);
`);
} else {
await db.exec(`
CREATE TABLE IF NOT EXISTS calendar_local_invitations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id INTEGER NOT NULL,
from_user_id INTEGER NOT NULL,
to_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(event_id, to_user_id),
FOREIGN KEY (event_id) REFERENCES calendar_events(id) ON DELETE CASCADE,
FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_cal_local_inv_to_user ON calendar_local_invitations(to_user_id);
CREATE INDEX IF NOT EXISTS idx_cal_local_inv_event ON calendar_local_invitations(event_id);
`);
}
// ── Default admin (only on very first start) ────────────────────────────
const adminAlreadySeeded = await db.get("SELECT value FROM settings WHERE key = 'admin_seeded'");
if (!adminAlreadySeeded) {