feat(calendar): add overlapping layout and event attachments

This commit is contained in:
Rafael Foster
2026-04-29 08:53:07 -03:00
parent 5f6baeab4f
commit d9218a4941
21 changed files with 377 additions and 33 deletions
+53 -4
View File
@@ -20,6 +20,19 @@ const log = createLogger('Calendar');
const router = express.Router();
const VALID_SOURCES = ['local', 'google', 'apple', 'ics'];
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
const ATTACHMENT_MIME = new Set([
'image/png',
'image/jpeg',
'image/webp',
'image/gif',
'application/pdf',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
const ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const VALID_EVENT_ICONS = new Set([
'calendar', 'tooth', 'drill', 'alarm-clock', 'clock', 'bell', 'map-pin', 'home',
@@ -59,6 +72,20 @@ function eventIcon(value) {
return VALID_EVENT_ICONS.has(icon) ? icon : null;
}
function parseAttachment(dataUrl) {
const raw = typeof dataUrl === 'string' ? dataUrl.trim() : '';
if (!raw) return { name: null, mime: null, size: null, data: null };
const match = raw.match(/^data:([^;,]+);base64,([A-Za-z0-9+/=\s]+)$/);
if (!match) throw new Error('attachment_data: ungültiges Dateiformat.');
const mime = match[1].toLowerCase();
if (!ATTACHMENT_MIME.has(mime)) throw new Error('attachment_data: Dateityp nicht erlaubt.');
const base64 = match[2].replace(/\s/g, '');
const buffer = Buffer.from(base64, 'base64');
if (!buffer.length) throw new Error('attachment_data: Datei ist leer.');
if (buffer.length > MAX_ATTACHMENT_BYTES) throw new Error('attachment_data: Datei darf höchstens 5 MB groß sein.');
return { name: null, mime, size: buffer.length, data: base64 };
}
// --------------------------------------------------------
// RRULE-Expansion: alle Vorkommen eines wiederkehrenden Events
// innerhalb [from, to] generieren (inklusive beider Grenzen).
@@ -591,17 +618,24 @@ router.post('/', (req, res) => {
if (!user) return res.status(400).json({ error: 'assigned_to: Benutzer nicht gefunden', code: 400 });
}
const attachment = req.body.attachment_data ? parseAttachment(req.body.attachment_data) : { mime: null, size: null, data: null };
const result = db.get().prepare(`
INSERT INTO calendar_events
(title, description, start_datetime, end_datetime, all_day,
location, color, icon, assigned_to, created_by, recurrence_rule)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
location, color, icon, assigned_to, created_by, recurrence_rule,
attachment_name, attachment_mime, attachment_size, attachment_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
vTitle.value, vDesc.value,
vStart.value, vEnd.value,
all_day ? 1 : 0, vLoc.value,
vColor.value, vIcon, assigned_to || null,
userId, vRrule.value
userId, vRrule.value,
req.body.attachment_name || null,
attachment.mime,
attachment.size,
attachment.data
);
const event = db.get().prepare(`
@@ -646,10 +680,17 @@ router.put('/:id', (req, res) => {
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const vIcon = req.body.icon !== undefined ? eventIcon(req.body.icon) : event.icon;
if (!vIcon) return res.status(400).json({ error: 'icon: invalid calendar event icon.', code: 400 });
const attachment = req.body.attachment_data !== undefined
? (req.body.attachment_data ? parseAttachment(req.body.attachment_data) : { mime: null, size: null, data: null })
: {
mime: event.attachment_mime,
size: event.attachment_size,
data: event.attachment_data,
};
const {
title, description, start_datetime, end_datetime,
all_day, location, color: colorVal, assigned_to, recurrence_rule,
all_day, location, color: colorVal, assigned_to, recurrence_rule, attachment_name,
} = req.body;
const userModified = event.external_source !== 'local' ? 1 : event.user_modified;
@@ -666,6 +707,10 @@ router.put('/:id', (req, res) => {
icon = COALESCE(?, icon),
assigned_to = ?,
recurrence_rule = ?,
attachment_name = ?,
attachment_mime = ?,
attachment_size = ?,
attachment_data = ?,
user_modified = ?
WHERE id = ?
`).run(
@@ -679,6 +724,10 @@ router.put('/:id', (req, res) => {
req.body.icon !== undefined ? vIcon : null,
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
attachment_name !== undefined ? (attachment_name || null) : event.attachment_name,
attachment.mime,
attachment.size,
attachment.data,
userModified,
id
);