feat(calendar): add overlapping layout and event attachments
This commit is contained in:
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "تم إعادة تعيين التغييرات."
|
"resetToast": "تم إعادة تعيين التغييرات."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "لوحة الملاحظات",
|
"title": "لوحة الملاحظات",
|
||||||
|
|||||||
@@ -375,7 +375,12 @@
|
|||||||
"resetToast": "Änderungen zurückgesetzt."
|
"resetToast": "Änderungen zurückgesetzt."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Bitte ein gültiges Datum im ausgewählten Format verwenden."
|
"invalidDate": "Bitte ein gültiges Datum im ausgewählten Format verwenden.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Notizen",
|
"title": "Notizen",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Οι αλλαγές επαναφέρθηκαν."
|
"resetToast": "Οι αλλαγές επαναφέρθηκαν."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Σημειώσεις",
|
"title": "Σημειώσεις",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Changes reset."
|
"resetToast": "Changes reset."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Board",
|
"title": "Board",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Cambios restablecidos."
|
"resetToast": "Cambios restablecidos."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Notas",
|
"title": "Notas",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Modifications annulées."
|
"resetToast": "Modifications annulées."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Notes",
|
"title": "Notes",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "परिवर्तन रीसेट हो गए।"
|
"resetToast": "परिवर्तन रीसेट हो गए।"
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "नोट बोर्ड",
|
"title": "नोट बोर्ड",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Modifiche ripristinate."
|
"resetToast": "Modifiche ripristinate."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Bacheca",
|
"title": "Bacheca",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "変更がリセットされました。"
|
"resetToast": "変更がリセットされました。"
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "メモボード",
|
"title": "メモボード",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Alterações restauradas."
|
"resetToast": "Alterações restauradas."
|
||||||
},
|
},
|
||||||
"iconLabel": "Ícone",
|
"iconLabel": "Ícone",
|
||||||
"invalidDate": "Use uma data válida no formato selecionado."
|
"invalidDate": "Use uma data válida no formato selecionado.",
|
||||||
|
"attachmentLabel": "Anexo",
|
||||||
|
"attachmentHint": "Anexe uma imagem, PDF ou documento local. Imagens aparecem no pop-up do evento.",
|
||||||
|
"attachmentFallback": "Anexo",
|
||||||
|
"attachmentReadError": "Nao foi possivel ler o anexo.",
|
||||||
|
"attachmentTooLarge": "O anexo pode ter no maximo 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Quadro de notas",
|
"title": "Quadro de notas",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Изменения сброшены."
|
"resetToast": "Изменения сброшены."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Заметки",
|
"title": "Заметки",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Ändringar återställda."
|
"resetToast": "Ändringar återställda."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Anteckningar",
|
"title": "Anteckningar",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Değişiklikler sıfırlandı."
|
"resetToast": "Değişiklikler sıfırlandı."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Notlar",
|
"title": "Notlar",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "Зміни скинуто."
|
"resetToast": "Зміни скинуто."
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "Нотатки",
|
"title": "Нотатки",
|
||||||
|
|||||||
@@ -358,7 +358,12 @@
|
|||||||
"resetToast": "更改已重置。"
|
"resetToast": "更改已重置。"
|
||||||
},
|
},
|
||||||
"iconLabel": "Icon",
|
"iconLabel": "Icon",
|
||||||
"invalidDate": "Use a valid date in the selected date format."
|
"invalidDate": "Use a valid date in the selected date format.",
|
||||||
|
"attachmentLabel": "Attachment",
|
||||||
|
"attachmentHint": "Attach a local image, PDF, or document. Images will be shown in the event popup.",
|
||||||
|
"attachmentFallback": "Attachment",
|
||||||
|
"attachmentReadError": "The attachment could not be read.",
|
||||||
|
"attachmentTooLarge": "Attachment may be at most 5 MB."
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"title": "便签板",
|
"title": "便签板",
|
||||||
|
|||||||
+161
-10
@@ -169,6 +169,8 @@ const EVENT_ICONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const CUSTOM_EVENT_ICONS = new Set(['tooth']);
|
const CUSTOM_EVENT_ICONS = new Set(['tooth']);
|
||||||
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
||||||
|
const ATTACHMENT_IMAGE_MIME = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
||||||
|
|
||||||
const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht
|
const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht
|
||||||
|
|
||||||
@@ -312,6 +314,35 @@ function eventIconElement(icon, className = 'event-icon') {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImageAttachment(mime) {
|
||||||
|
return ATTACHMENT_IMAGE_MIME.has(String(mime || '').toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileAsDataUrl(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(String(reader.result || ''));
|
||||||
|
reader.onerror = () => reject(new Error(t('calendar.attachmentReadError')));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentHtml(event) {
|
||||||
|
if (!event?.attachment_data) return '';
|
||||||
|
const name = esc(event.attachment_name || t('calendar.attachmentFallback'));
|
||||||
|
if (isImageAttachment(event.attachment_mime)) {
|
||||||
|
return `
|
||||||
|
<div class="event-popup__attachment event-popup__attachment--image">
|
||||||
|
<img src="${event.attachment_data}" alt="${name}">
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<a class="event-popup__attachment event-popup__attachment--file" href="${event.attachment_data}" download="${name}">
|
||||||
|
<i data-lucide="paperclip" aria-hidden="true"></i>
|
||||||
|
<span>${name}</span>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
function bindDateInputs(root) {
|
function bindDateInputs(root) {
|
||||||
root.querySelectorAll('.js-date-input').forEach((input) => {
|
root.querySelectorAll('.js-date-input').forEach((input) => {
|
||||||
input.addEventListener('blur', () => {
|
input.addEventListener('blur', () => {
|
||||||
@@ -631,6 +662,7 @@ function renderWeekView(container) {
|
|||||||
const timedEvs = days.map((d) =>
|
const timedEvs = days.map((d) =>
|
||||||
eventsOnDay(d).filter((e) => !e.all_day && e.start_datetime.includes('T'))
|
eventsOnDay(d).filter((e) => !e.all_day && e.start_datetime.includes('T'))
|
||||||
);
|
);
|
||||||
|
const layouts = timedEvs.map((events) => layoutOverlaps(events));
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="week-view">
|
<div class="week-view">
|
||||||
@@ -674,7 +706,7 @@ function renderWeekView(container) {
|
|||||||
${Array.from({ length: 24 }, (_, h) => `
|
${Array.from({ length: 24 }, (_, h) => `
|
||||||
<div class="week-view__hour-line" style="top:${h * HOUR_HEIGHT}px;"></div>
|
<div class="week-view__hour-line" style="top:${h * HOUR_HEIGHT}px;"></div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
${timedEvs[i].map((ev) => renderWeekEvent(ev)).join('')}
|
${timedEvs[i].map((ev) => renderWeekEvent(ev, layouts[i].get(ev.id))).join('')}
|
||||||
${d === state.today ? `<div class="week-view__now-line" id="now-line" style="top:${nowTop()}px;"></div>` : ''}
|
${d === state.today ? `<div class="week-view__now-line" id="now-line" style="top:${nowTop()}px;"></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@@ -712,19 +744,18 @@ function renderWeekView(container) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWeekEvent(ev) {
|
function renderWeekEvent(ev, layout = null) {
|
||||||
const start = timeToMinutes(localTime(ev.start_datetime));
|
const { start, end } = timeRangeForEvent(ev);
|
||||||
const end = ev.end_datetime
|
|
||||||
? timeToMinutes(localTime(ev.end_datetime))
|
|
||||||
: start + 60;
|
|
||||||
const duration = Math.max(end - start, 30);
|
const duration = Math.max(end - start, 30);
|
||||||
|
|
||||||
const top = (start / 60) * HOUR_HEIGHT;
|
const top = (start / 60) * HOUR_HEIGHT;
|
||||||
const height = (duration / 60) * HOUR_HEIGHT - 2;
|
const height = (duration / 60) * HOUR_HEIGHT - 2;
|
||||||
|
const left = layout ? `calc(${(layout.colIndex / layout.totalCols) * 100}% + 2px)` : '2px';
|
||||||
|
const width = layout ? `calc(${100 / layout.totalCols}% - 4px)` : 'auto';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="week-event" data-id="${ev.id}"
|
<div class="week-event" data-id="${ev.id}"
|
||||||
style="top:${top}px;height:${height}px;${ev.cal_color || ev.color ? `background-color:${esc(ev.cal_color || ev.color)};` : ''}${getContrastColor(ev.cal_color || ev.color) ? `color:${getContrastColor(ev.cal_color || ev.color)};` : ''}">
|
style="top:${top}px;height:${height}px;left:${left};width:${width};${ev.cal_color || ev.color ? `background-color:${esc(ev.cal_color || ev.color)};` : ''}${getContrastColor(ev.cal_color || ev.color) ? `color:${getContrastColor(ev.cal_color || ev.color)};` : ''}">
|
||||||
<div class="week-event__title">${eventIconHtml(ev.icon, 'event-icon event-icon--compact')}<span>${esc(ev.title)}</span></div>
|
<div class="week-event__title">${eventIconHtml(ev.icon, 'event-icon event-icon--compact')}<span>${esc(ev.title)}</span></div>
|
||||||
<div class="week-event__time">${formatTime(ev.start_datetime)}${ev.end_datetime ? '–' + formatTime(ev.end_datetime) : ''}</div>
|
<div class="week-event__time">${formatTime(ev.start_datetime)}${ev.end_datetime ? '–' + formatTime(ev.end_datetime) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -743,6 +774,66 @@ function nowTop() {
|
|||||||
return (minutes / 60) * HOUR_HEIGHT;
|
return (minutes / 60) * HOUR_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function timeRangeForEvent(ev) {
|
||||||
|
const start = timeToMinutes(localTime(ev.start_datetime));
|
||||||
|
const end = ev.end_datetime
|
||||||
|
? timeToMinutes(localTime(ev.end_datetime))
|
||||||
|
: start + 60;
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end: Math.max(end, start + 30),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutOverlaps(events) {
|
||||||
|
const groups = [];
|
||||||
|
const sorted = [...events].sort((a, b) => {
|
||||||
|
const aRange = timeRangeForEvent(a);
|
||||||
|
const bRange = timeRangeForEvent(b);
|
||||||
|
return aRange.start - bRange.start || aRange.end - bRange.end;
|
||||||
|
});
|
||||||
|
|
||||||
|
let current = [];
|
||||||
|
let currentEnd = -1;
|
||||||
|
for (const ev of sorted) {
|
||||||
|
const range = timeRangeForEvent(ev);
|
||||||
|
if (!current.length || range.start < currentEnd) {
|
||||||
|
current.push(ev);
|
||||||
|
currentEnd = current.length === 1 ? range.end : Math.max(currentEnd, range.end);
|
||||||
|
} else {
|
||||||
|
groups.push(current);
|
||||||
|
current = [ev];
|
||||||
|
currentEnd = range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.length) groups.push(current);
|
||||||
|
|
||||||
|
const layout = new Map();
|
||||||
|
for (const group of groups) {
|
||||||
|
const columns = [];
|
||||||
|
const placements = [];
|
||||||
|
for (const ev of group) {
|
||||||
|
const range = timeRangeForEvent(ev);
|
||||||
|
let colIndex = columns.findIndex((end) => end <= range.start);
|
||||||
|
if (colIndex === -1) {
|
||||||
|
colIndex = columns.length;
|
||||||
|
columns.push(range.end);
|
||||||
|
} else {
|
||||||
|
columns[colIndex] = range.end;
|
||||||
|
}
|
||||||
|
placements.push({ ev, colIndex });
|
||||||
|
}
|
||||||
|
const totalCols = Math.max(columns.length, 1);
|
||||||
|
for (const placement of placements) {
|
||||||
|
layout.set(placement.ev.id, {
|
||||||
|
colIndex: placement.colIndex,
|
||||||
|
totalCols,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Tagesansicht
|
// Tagesansicht
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -752,6 +843,7 @@ function renderDayView(container) {
|
|||||||
const dayEvs = eventsOnDay(state.cursor);
|
const dayEvs = eventsOnDay(state.cursor);
|
||||||
const allday = dayEvs.filter((e) => e.all_day || !e.start_datetime.includes('T'));
|
const allday = dayEvs.filter((e) => e.all_day || !e.start_datetime.includes('T'));
|
||||||
const timed = dayEvs.filter((e) => !e.all_day && e.start_datetime.includes('T'));
|
const timed = dayEvs.filter((e) => !e.all_day && e.start_datetime.includes('T'));
|
||||||
|
const layout = layoutOverlaps(timed);
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="day-view">
|
<div class="day-view">
|
||||||
@@ -781,7 +873,7 @@ function renderDayView(container) {
|
|||||||
${Array.from({ length: 24 }, (_, h) => `
|
${Array.from({ length: 24 }, (_, h) => `
|
||||||
<div class="week-view__hour-line" style="top:${h * HOUR_HEIGHT}px;"></div>
|
<div class="week-view__hour-line" style="top:${h * HOUR_HEIGHT}px;"></div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
${timed.map((ev) => renderWeekEvent(ev)).join('')}
|
${timed.map((ev) => renderWeekEvent(ev, layout.get(ev.id))).join('')}
|
||||||
${state.cursor === state.today ? `<div class="week-view__now-line" style="top:${nowTop()}px;"></div>` : ''}
|
${state.cursor === state.today ? `<div class="week-view__now-line" style="top:${nowTop()}px;"></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -902,6 +994,7 @@ function showEventPopup(ev, anchor) {
|
|||||||
<div>${timeStr}</div>
|
<div>${timeStr}</div>
|
||||||
${ev.location ? `<div>📍 ${esc(fmtLocation(ev.location))}</div>` : ''}
|
${ev.location ? `<div>📍 ${esc(fmtLocation(ev.location))}</div>` : ''}
|
||||||
${ev.description ? `<div>${esc(ev.description)}</div>` : ''}
|
${ev.description ? `<div>${esc(ev.description)}</div>` : ''}
|
||||||
|
${ev.attachment_data ? attachmentHtml(ev) : ''}
|
||||||
${ev.assigned_name ? `<div>👤 ${esc(ev.assigned_name)}</div>` : ''}
|
${ev.assigned_name ? `<div>👤 ${esc(ev.assigned_name)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-popup__actions">
|
<div class="event-popup__actions">
|
||||||
@@ -1163,6 +1256,47 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
|||||||
|
|
||||||
const reminderOffset = panel.querySelector('#modal-reminder-offset');
|
const reminderOffset = panel.querySelector('#modal-reminder-offset');
|
||||||
const reminderCustom = panel.querySelector('#modal-reminder-custom');
|
const reminderCustom = panel.querySelector('#modal-reminder-custom');
|
||||||
|
const attachmentInput = panel.querySelector('#modal-attachment');
|
||||||
|
const attachmentPreview = panel.querySelector('#modal-attachment-preview');
|
||||||
|
const attachmentState = {
|
||||||
|
name: event?.attachment_name || null,
|
||||||
|
mime: event?.attachment_mime || null,
|
||||||
|
size: event?.attachment_size || null,
|
||||||
|
data: event?.attachment_data || null,
|
||||||
|
};
|
||||||
|
const syncAttachmentPreview = () => {
|
||||||
|
if (!attachmentPreview) return;
|
||||||
|
attachmentPreview.innerHTML = '';
|
||||||
|
if (!attachmentState.data) {
|
||||||
|
attachmentPreview.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attachmentPreview.hidden = false;
|
||||||
|
if (isImageAttachment(attachmentState.mime)) {
|
||||||
|
attachmentPreview.innerHTML = `<img src="${attachmentState.data}" alt="${esc(attachmentState.name || '')}">`;
|
||||||
|
} else {
|
||||||
|
attachmentPreview.innerHTML = `<a href="${attachmentState.data}" download="${esc(attachmentState.name || '')}">${esc(attachmentState.name || '')}</a>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
attachmentInput?.addEventListener('change', async () => {
|
||||||
|
const file = attachmentInput.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (file.size > MAX_ATTACHMENT_BYTES) {
|
||||||
|
window.oikos?.showToast(t('calendar.attachmentTooLarge'), 'error');
|
||||||
|
attachmentInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
attachmentState.data = await readFileAsDataUrl(file);
|
||||||
|
attachmentState.name = file.name;
|
||||||
|
attachmentState.mime = file.type || 'application/octet-stream';
|
||||||
|
attachmentState.size = file.size;
|
||||||
|
syncAttachmentPreview();
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message, 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
syncAttachmentPreview();
|
||||||
reminderOffset?.addEventListener('change', () => {
|
reminderOffset?.addEventListener('change', () => {
|
||||||
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
||||||
});
|
});
|
||||||
@@ -1174,7 +1308,7 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
|||||||
await deleteEvent(event.id);
|
await deleteEvent(event.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.querySelector('#modal-save').addEventListener('click', () => saveEvent(panel, mode, event?.id, reminder));
|
panel.querySelector('#modal-save').addEventListener('click', () => saveEvent(panel, mode, event?.id, reminder, attachmentState));
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1309,6 +1443,19 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
|||||||
placeholder="${t('calendar.descriptionPlaceholder')}">${esc(isEdit && event.description ? event.description : '')}</textarea>
|
placeholder="${t('calendar.descriptionPlaceholder')}">${esc(isEdit && event.description ? event.description : '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="modal-attachment">${t('calendar.attachmentLabel')}</label>
|
||||||
|
<input class="form-input" id="modal-attachment" type="file" accept="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">
|
||||||
|
<div class="form-help">${t('calendar.attachmentHint')}</div>
|
||||||
|
<div class="event-attachment-preview" id="modal-attachment-preview" ${isEdit && event.attachment_data ? '' : 'hidden'}>
|
||||||
|
${isEdit && event.attachment_data
|
||||||
|
? (isImageAttachment(event.attachment_mime)
|
||||||
|
? `<img src="${event.attachment_data}" alt="${esc(event.attachment_name || '')}">`
|
||||||
|
: `<a href="${event.attachment_data}" download="${esc(event.attachment_name || '')}">${esc(event.attachment_name || '')}</a>`)
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
${renderRRuleFields('event', isEdit ? event.recurrence_rule : null)}
|
${renderRRuleFields('event', isEdit ? event.recurrence_rule : null)}
|
||||||
|
|
||||||
${renderCalendarReminderSection(reminder, event)}
|
${renderCalendarReminderSection(reminder, event)}
|
||||||
@@ -1324,7 +1471,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEvent(overlay, mode, eventId, existingReminder = null) {
|
async function saveEvent(overlay, mode, eventId, existingReminder = null, attachmentState = null) {
|
||||||
const saveBtn = overlay.querySelector('#modal-save');
|
const saveBtn = overlay.querySelector('#modal-save');
|
||||||
const title = overlay.querySelector('#modal-title').value.trim();
|
const title = overlay.querySelector('#modal-title').value.trim();
|
||||||
|
|
||||||
@@ -1382,6 +1529,10 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null) {
|
|||||||
all_day: allday ? 1 : 0,
|
all_day: allday ? 1 : 0,
|
||||||
location, color, icon, assigned_to: assigned_to ? parseInt(assigned_to, 10) : null,
|
location, color, icon, assigned_to: assigned_to ? parseInt(assigned_to, 10) : null,
|
||||||
recurrence_rule: rrule.recurrence_rule,
|
recurrence_rule: rrule.recurrence_rule,
|
||||||
|
attachment_name: attachmentState?.name || null,
|
||||||
|
attachment_mime: attachmentState?.mime || null,
|
||||||
|
attachment_size: attachmentState?.size || null,
|
||||||
|
attachment_data: attachmentState?.data || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
let savedEventId = eventId;
|
let savedEventId = eventId;
|
||||||
|
|||||||
@@ -386,8 +386,6 @@
|
|||||||
|
|
||||||
.week-event {
|
.week-event {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 2px;
|
|
||||||
right: 2px;
|
|
||||||
border-radius: var(--radius-xs);
|
border-radius: var(--radius-xs);
|
||||||
padding: var(--space-0h) var(--space-1);
|
padding: var(--space-0h) var(--space-1);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
@@ -419,6 +417,29 @@
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-attachment-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-attachment-preview img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 180px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-attachment-preview a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--color-accent);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Tagesansicht
|
* Tagesansicht
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
@@ -763,6 +784,25 @@
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-popup__attachment {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-popup__attachment--image img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-popup__attachment--file {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Ganztägige Ereignisse (oben in Wochen-/Tagesansicht)
|
* Ganztägige Ereignisse (oben in Wochen-/Tagesansicht)
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
|
|||||||
@@ -419,6 +419,12 @@ const MIGRATIONS_SQL = {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_family_documents_created_by ON family_documents(created_by);
|
CREATE INDEX IF NOT EXISTS idx_family_documents_created_by ON family_documents(created_by);
|
||||||
CREATE INDEX IF NOT EXISTS idx_family_document_access_user ON family_document_access(user_id);
|
CREATE INDEX IF NOT EXISTS idx_family_document_access_user ON family_document_access(user_id);
|
||||||
`,
|
`,
|
||||||
|
20: `
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_name TEXT;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_mime TEXT;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_size INTEGER;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_data TEXT;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { MIGRATIONS_SQL };
|
export { MIGRATIONS_SQL };
|
||||||
|
|||||||
@@ -853,6 +853,16 @@ const MIGRATIONS = [
|
|||||||
CREATE INDEX IF NOT EXISTS idx_family_document_access_user ON family_document_access(user_id);
|
CREATE INDEX IF NOT EXISTS idx_family_document_access_user ON family_document_access(user_id);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: 27,
|
||||||
|
description: 'Calendar event attachments',
|
||||||
|
up: `
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_name TEXT;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_mime TEXT;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_size INTEGER;
|
||||||
|
ALTER TABLE calendar_events ADD COLUMN attachment_data TEXT;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+15
-2
@@ -349,7 +349,13 @@ function buildPaths() {
|
|||||||
},
|
},
|
||||||
'/api/v1/calendar': {
|
'/api/v1/calendar': {
|
||||||
get: op({ summary: 'List calendar events', tag: 'Calendar' }),
|
get: op({ summary: 'List calendar events', tag: 'Calendar' }),
|
||||||
post: op({ summary: 'Create calendar event', tag: 'Calendar', stateChanging: true, requestBody: jsonBody(null) }),
|
post: op({
|
||||||
|
summary: 'Create calendar event',
|
||||||
|
tag: 'Calendar',
|
||||||
|
stateChanging: true,
|
||||||
|
description: 'Supports optional local file attachments via `attachment_name`, `attachment_mime`, `attachment_size`, and `attachment_data` (base64 data URL).',
|
||||||
|
requestBody: jsonBody(null),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
'/api/v1/calendar/upcoming': { get: op({ summary: 'List upcoming events', tag: 'Calendar' }) },
|
'/api/v1/calendar/upcoming': { get: op({ summary: 'List upcoming events', tag: 'Calendar' }) },
|
||||||
'/api/v1/calendar/google/auth': { get: op({ summary: 'Start Google Calendar OAuth', tag: 'Calendar', admin: true }) },
|
'/api/v1/calendar/google/auth': { get: op({ summary: 'Start Google Calendar OAuth', tag: 'Calendar', admin: true }) },
|
||||||
@@ -374,7 +380,14 @@ function buildPaths() {
|
|||||||
},
|
},
|
||||||
'/api/v1/calendar/{id}': {
|
'/api/v1/calendar/{id}': {
|
||||||
get: op({ summary: 'Get calendar event', tag: 'Calendar', params: [idParam()] }),
|
get: op({ summary: 'Get calendar event', tag: 'Calendar', params: [idParam()] }),
|
||||||
put: op({ summary: 'Update calendar event', tag: 'Calendar', params: [idParam()], stateChanging: true, requestBody: jsonBody(null) }),
|
put: op({
|
||||||
|
summary: 'Update calendar event',
|
||||||
|
tag: 'Calendar',
|
||||||
|
params: [idParam()],
|
||||||
|
stateChanging: true,
|
||||||
|
description: 'Supports optional local file attachments via `attachment_name`, `attachment_mime`, `attachment_size`, and `attachment_data` (base64 data URL).',
|
||||||
|
requestBody: jsonBody(null),
|
||||||
|
}),
|
||||||
delete: op({ summary: 'Delete calendar event', tag: 'Calendar', params: [idParam()], stateChanging: true }),
|
delete: op({ summary: 'Delete calendar event', tag: 'Calendar', params: [idParam()], stateChanging: true }),
|
||||||
},
|
},
|
||||||
'/api/v1/calendar/{id}/reset': {
|
'/api/v1/calendar/{id}/reset': {
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ const log = createLogger('Calendar');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const VALID_SOURCES = ['local', 'google', 'apple', 'ics'];
|
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 ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
||||||
const VALID_EVENT_ICONS = new Set([
|
const VALID_EVENT_ICONS = new Set([
|
||||||
'calendar', 'tooth', 'drill', 'alarm-clock', 'clock', 'bell', 'map-pin', 'home',
|
'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;
|
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
|
// RRULE-Expansion: alle Vorkommen eines wiederkehrenden Events
|
||||||
// innerhalb [from, to] generieren (inklusive beider Grenzen).
|
// 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 });
|
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(`
|
const result = db.get().prepare(`
|
||||||
INSERT INTO calendar_events
|
INSERT INTO calendar_events
|
||||||
(title, description, start_datetime, end_datetime, all_day,
|
(title, description, start_datetime, end_datetime, all_day,
|
||||||
location, color, icon, assigned_to, created_by, recurrence_rule)
|
location, color, icon, assigned_to, created_by, recurrence_rule,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
attachment_name, attachment_mime, attachment_size, attachment_data)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
vTitle.value, vDesc.value,
|
vTitle.value, vDesc.value,
|
||||||
vStart.value, vEnd.value,
|
vStart.value, vEnd.value,
|
||||||
all_day ? 1 : 0, vLoc.value,
|
all_day ? 1 : 0, vLoc.value,
|
||||||
vColor.value, vIcon, assigned_to || null,
|
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(`
|
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 });
|
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;
|
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 });
|
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 {
|
const {
|
||||||
title, description, start_datetime, end_datetime,
|
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;
|
} = req.body;
|
||||||
|
|
||||||
const userModified = event.external_source !== 'local' ? 1 : event.user_modified;
|
const userModified = event.external_source !== 'local' ? 1 : event.user_modified;
|
||||||
@@ -666,6 +707,10 @@ router.put('/:id', (req, res) => {
|
|||||||
icon = COALESCE(?, icon),
|
icon = COALESCE(?, icon),
|
||||||
assigned_to = ?,
|
assigned_to = ?,
|
||||||
recurrence_rule = ?,
|
recurrence_rule = ?,
|
||||||
|
attachment_name = ?,
|
||||||
|
attachment_mime = ?,
|
||||||
|
attachment_size = ?,
|
||||||
|
attachment_data = ?,
|
||||||
user_modified = ?
|
user_modified = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(
|
`).run(
|
||||||
@@ -679,6 +724,10 @@ router.put('/:id', (req, res) => {
|
|||||||
req.body.icon !== undefined ? vIcon : null,
|
req.body.icon !== undefined ? vIcon : null,
|
||||||
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
|
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
|
||||||
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
|
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,
|
userModified,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user