mirror of
https://gh.wpcy.net/https://github.com/LJPc-solutions/freescout-calendar-module.git
synced 2026-07-16 08:37:27 +08:00
1060 lines
47 KiB
PHP
1060 lines
47 KiB
PHP
<script {!! \Helper::cspNonceAttr() !!} src="{{ Module::getPublicPath(LJPC_CALENDARS_MODULE).'/js/moment.min.js' }}"></script>
|
|
<script {!! \Helper::cspNonceAttr() !!} src="{{ Module::getPublicPath(LJPC_CALENDARS_MODULE).'/js/toastui-calendar.min.js' }}"></script>
|
|
<script {!! \Helper::cspNonceAttr() !!} >
|
|
// Ensure laroute routes are available for calendar module
|
|
if (typeof laroute !== 'undefined' && typeof laroute.routes !== 'undefined') {
|
|
// Define calendar routes if they're not already defined
|
|
var calendarRoutes = {
|
|
'ljpccalendarmodule.api.events': '/calendar/api/events',
|
|
'ljpccalendarmodule.api.event.get': '/calendar/api/event/{eventId}',
|
|
'ljpccalendarmodule.api.event.create': '/calendar/api/events',
|
|
'ljpccalendarmodule.api.event.update': '/calendar/api/events',
|
|
'ljpccalendarmodule.api.event.delete': '/calendar/api/events',
|
|
'ljpccalendarmodule.api.calendar.get': '/calendar/api/calendars/{id}',
|
|
'ljpccalendarmodule.api.event.create_from_attachment': '/calendar/api/events/attachment',
|
|
'ljpccalendarmodule.api.event.create_from_conversation': '/calendar/api/events/{conversation}',
|
|
'ljpccalendarmodule.index': '/calendar'
|
|
};
|
|
|
|
// Manually add routes if needed
|
|
for (var routeName in calendarRoutes) {
|
|
if (!laroute.route(routeName, [], false)) {
|
|
// Route doesn't exist, we'll need to handle this differently
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<script {!! \Helper::cspNonceAttr() !!} >
|
|
document.addEventListener("DOMContentLoaded", (event) => {
|
|
// Set the locale
|
|
moment.locale('{{Helper::getRealAppLocale()}}');
|
|
|
|
// Constants
|
|
const MOBILE_BREAKPOINT = 768;
|
|
const VIEW_DAY = 'day';
|
|
const VIEW_WEEK = 'week';
|
|
const VIEW_MONTH = 'month';
|
|
|
|
// Parse hash parameters for permalinks
|
|
let hashParams;
|
|
try {
|
|
// Get the hash portion (remove the # character)
|
|
const hashString = window.location.hash.substring(1);
|
|
|
|
// Check for direct event ID format (just the ID without event= prefix)
|
|
if (hashString && hashString.indexOf('=') === -1 && hashString.length > 0) {
|
|
// If the hash doesn't contain '=', treat it as the event ID directly
|
|
const params = new URLSearchParams();
|
|
params.set('event', hashString);
|
|
hashParams = params;
|
|
} else {
|
|
// Normal URLSearchParams parsing for event=ID format
|
|
hashParams = new URLSearchParams(hashString);
|
|
}
|
|
} catch (err) {
|
|
hashParams = new URLSearchParams();
|
|
}
|
|
|
|
const calendars = {!! json_encode($calendars) !!};
|
|
const csrfToken = '{{ csrf_token() }}';
|
|
let templates = {};
|
|
@if ( Helper::isTimeFormat24())
|
|
templates = {
|
|
timegridDisplayPrimaryTime({time}) {
|
|
var hour = time.getHours();
|
|
var formattedHour = hour < 10 ? '0' + hour : hour;
|
|
|
|
return `${formattedHour}:00`;
|
|
}
|
|
}
|
|
@endif
|
|
const calendarOptions = {
|
|
defaultView: 'week',
|
|
useDetailPopup: false,
|
|
usageStatistics: false,
|
|
scheduleView: ["time"],
|
|
week: {
|
|
startDayOfWeek: 1,
|
|
taskView: false,
|
|
eventView: ['allday', 'time'],
|
|
dayNames: window.ljpccalendarmoduledaytranslations,
|
|
},
|
|
month: {
|
|
startDayOfWeek: 1,
|
|
dayNames: window.ljpccalendarmoduledaytranslations,
|
|
},
|
|
calendars: calendars,
|
|
template: templates
|
|
};
|
|
|
|
// DOM elements
|
|
const dom = {
|
|
general: {
|
|
calendar: document.getElementById('calendar'),
|
|
},
|
|
calendarPicker: {
|
|
container: document.getElementById('calendar-picker'),
|
|
},
|
|
topBar: {
|
|
previousButton: document.getElementById('previous-button'),
|
|
nextButton: document.getElementById('next-button'),
|
|
todayButton: document.getElementById('today-button'),
|
|
currentDate: document.getElementById('current-date'),
|
|
},
|
|
newEventModal: {
|
|
modal: document.getElementById('event-modal'),
|
|
modalClose: document.querySelector('.event-modal-close'),
|
|
title: document.getElementById('event-title'),
|
|
start: document.getElementById('event-start'),
|
|
end: document.getElementById('event-end'),
|
|
location: document.getElementById('event-location'),
|
|
body: document.getElementById('event-body'),
|
|
calendar: document.getElementById('event-calendar'),
|
|
saveButton: document.getElementById('save-button'),
|
|
},
|
|
eventDetailModal: {
|
|
modal: document.getElementById('event-details-modal'),
|
|
modalClose: document.querySelector('#event-details-modal .event-modal-close'),
|
|
title: document.getElementById('event-details-title'),
|
|
start: document.getElementById('event-details-start'),
|
|
end: document.getElementById('event-details-end'),
|
|
location: document.getElementById('event-details-location'),
|
|
body: document.getElementById('event-details-body'),
|
|
calendar: document.getElementById('event-details-calendar'),
|
|
createdBy: document.getElementById('event-details-created-by'),
|
|
hiddenCalendar: document.getElementById('hidden-event-details-calendar'),
|
|
hiddenUid: document.getElementById('hidden-event-details-uid'),
|
|
hiddenCustomFields: document.getElementById('hidden-event-details-custom-fields'),
|
|
updateButton: document.getElementById('update-button'),
|
|
deleteButton: document.getElementById('delete-button'),
|
|
createCopyButton: document.getElementById('create-copy-button'),
|
|
copyPermalinkButton: document.getElementById('copy-permalink-button'),
|
|
originalStart: null,
|
|
originalEnd: null,
|
|
},
|
|
viewSelector: {
|
|
dayButton: document.getElementById('day-view-button'),
|
|
weekButton: document.getElementById('week-view-button'),
|
|
monthButton: document.getElementById('month-view-button'),
|
|
}
|
|
};
|
|
|
|
// Helper functions
|
|
const hasPermissions = (calendarId, permission) => {
|
|
calendarId = parseInt(calendarId, 10);
|
|
const calendar = calendars.find(calendar => calendar.id === calendarId);
|
|
if (typeof calendar === 'undefined')
|
|
return false;
|
|
return calendar.permissions.hasOwnProperty(permission) && calendar.permissions[permission];
|
|
}
|
|
const outerHeight = (element) => {
|
|
const height = element.offsetHeight,
|
|
style = window.getComputedStyle(element)
|
|
|
|
return ['top', 'bottom']
|
|
.map(side => parseInt(style[`margin-${side}`]))
|
|
.reduce((total, side) => total + side, height)
|
|
}
|
|
|
|
// Calendar
|
|
const calendar = new tui.Calendar(dom.general.calendar, calendarOptions);
|
|
|
|
// Calendar picker
|
|
const calendarButtons = document.createElement('div');
|
|
calendarButtons.classList.add('calendar-buttons');
|
|
|
|
const calendarInstances = [];
|
|
|
|
calendars.forEach(calendarObj => {
|
|
const button = document.createElement('button');
|
|
button.classList.add('calendar-button');
|
|
button.innerHTML = `<span class="calendar-button-dot" style="color: ${calendarObj.backgroundColor};"></span>${calendarObj.name}`;
|
|
|
|
const calendarInstance = {
|
|
id: calendarObj.id,
|
|
name: calendarObj.name,
|
|
backgroundColor: calendarObj.backgroundColor,
|
|
isVisible: true,
|
|
render: () => {
|
|
const dot = button.querySelector('.calendar-button-dot');
|
|
dot.classList.toggle('transparent', !calendarInstance.isVisible);
|
|
calendar.setCalendarVisibility(calendarInstance.id, calendarInstance.isVisible);
|
|
}
|
|
};
|
|
|
|
button.addEventListener('click', () => {
|
|
calendarInstance.isVisible = !calendarInstance.isVisible;
|
|
calendarInstance.render();
|
|
});
|
|
|
|
calendarButtons.appendChild(button);
|
|
calendarInstances.push(calendarInstance);
|
|
});
|
|
|
|
dom.calendarPicker.container.appendChild(calendarButtons);
|
|
|
|
const isMobileView = () => window.innerWidth < MOBILE_BREAKPOINT;
|
|
|
|
const setCalendarView = (view) => {
|
|
calendar.changeView(view);
|
|
updateViewButtons(view);
|
|
updateTime();
|
|
};
|
|
|
|
const updateViewButtons = (currentView) => {
|
|
dom.viewSelector.dayButton.classList.toggle('active', currentView === VIEW_DAY);
|
|
dom.viewSelector.weekButton.classList.toggle('active', currentView === VIEW_WEEK);
|
|
dom.viewSelector.monthButton.classList.toggle('active', currentView === VIEW_MONTH);
|
|
|
|
// Hide week and month buttons on mobile
|
|
const isMobile = isMobileView();
|
|
dom.viewSelector.weekButton.style.display = isMobile ? 'none' : 'inline-block';
|
|
dom.viewSelector.monthButton.style.display = isMobile ? 'none' : 'inline-block';
|
|
};
|
|
|
|
// Event listeners for view buttons
|
|
dom.viewSelector.dayButton.addEventListener('click', () => setCalendarView(VIEW_DAY));
|
|
dom.viewSelector.weekButton.addEventListener('click', () => setCalendarView(VIEW_WEEK));
|
|
dom.viewSelector.monthButton.addEventListener('click', () => setCalendarView(VIEW_MONTH));
|
|
|
|
|
|
// Function to open event by ID from permalink
|
|
const openEventById = async (eventId) => {
|
|
if (!eventId) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Use the optimized direct lookup API first for best performance
|
|
let event = null;
|
|
|
|
try {
|
|
event = await api.getEventById(eventId);
|
|
} catch (directLookupError) {
|
|
// If direct lookup fails, fall back to searching in current view
|
|
console.warn('Direct event lookup failed, falling back to view search:', directLookupError);
|
|
|
|
const start = moment(calendar.getDateRangeStart().toDate()).set({"hour": 0, "minute": 0}).toISOString();
|
|
const end = moment(calendar.getDateRangeEnd().toDate()).set({"hour": 23, "minute": 59}).toISOString();
|
|
const eventsInView = await api.getEvents(start, end);
|
|
|
|
// Try exact match first
|
|
event = eventsInView.find(e => e.id === eventId);
|
|
|
|
// Then try with string conversion
|
|
if (!event) {
|
|
event = eventsInView.find(e => String(e.id) === String(eventId));
|
|
}
|
|
|
|
// Then try with number conversion (if possible)
|
|
if (!event && !isNaN(parseInt(eventId))) {
|
|
const numericId = parseInt(eventId);
|
|
event = eventsInView.find(e => parseInt(e.id) === numericId);
|
|
}
|
|
}
|
|
|
|
if (event && event.id && event.calendarId && event.start && event.end) {
|
|
try {
|
|
// Navigate calendar to event date
|
|
const eventDate = new Date(event.start);
|
|
calendar.setDate(eventDate);
|
|
|
|
// Create view model for event detail modal
|
|
const eventViewModel = {
|
|
id: event.id,
|
|
calendarId: event.calendarId,
|
|
title: event.title || '',
|
|
start: {toDate: () => new Date(event.start)},
|
|
end: {toDate: () => new Date(event.end)},
|
|
location: event.location || '',
|
|
body: event.body || '',
|
|
raw: event.raw || {customFields: {}}
|
|
};
|
|
|
|
// Make sure calendar for this event is visible
|
|
let calendarFound = false;
|
|
for (const calendarInstance of calendarInstances) {
|
|
if (calendarInstance.id === parseInt(event.calendarId)) {
|
|
calendarInstance.isVisible = true;
|
|
calendarInstance.render();
|
|
calendarFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Need to fully render the calendar before opening the modal
|
|
setTimeout(() => {
|
|
try {
|
|
openEventDetailsModal(eventViewModel);
|
|
getEvents(event.id);
|
|
updateTime();
|
|
} catch (err) {
|
|
showFloatingAlert('error', '{{__('Error displaying event details')}}');
|
|
}
|
|
}, 300);
|
|
|
|
return true;
|
|
} catch (err) {
|
|
showFloatingAlert('error', '{{__('Error processing event data')}}');
|
|
return false;
|
|
}
|
|
} else {
|
|
// Check if the calendar module is installed
|
|
const calendarExists = calendars && calendars.length > 0;
|
|
if (!calendarExists) {
|
|
showFloatingAlert('error', '{{__('Calendar module appears to not be fully installed or initialized')}}');
|
|
} else {
|
|
showFloatingAlert('error', '{{__('Event not found or you do not have access to it')}}');
|
|
}
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to load event')}}');
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Rerender functions
|
|
const rerender = () => {
|
|
const navbarHeight = outerHeight(document.querySelector('.navbar'));
|
|
const footerHeight = outerHeight(document.querySelector('.footer'));
|
|
const maxHeight = window.innerHeight - navbarHeight - footerHeight;
|
|
|
|
const calendarNavbarHeight = outerHeight(document.querySelector('.calendar-wrapper .navbar'));
|
|
|
|
document.querySelector('.ljpc_calendar_module').style.minHeight = `${maxHeight}px`;
|
|
dom.general.calendar.style.minHeight = `${maxHeight - calendarNavbarHeight}px`;
|
|
dom.general.calendar.style.height = `${maxHeight - calendarNavbarHeight}px`;
|
|
|
|
const isMobile = isMobileView();
|
|
const currentView = calendar.getViewName();
|
|
const wantedView = isMobile ? VIEW_DAY : (currentView === VIEW_DAY ? VIEW_DAY : VIEW_WEEK);
|
|
|
|
if (currentView !== wantedView) {
|
|
setCalendarView(wantedView);
|
|
|
|
// Set all calendars visible
|
|
for (const calendarInstance of calendarInstances) {
|
|
calendarInstance.isVisible = true;
|
|
calendarInstance.render();
|
|
}
|
|
}
|
|
|
|
updateViewButtons(wantedView);
|
|
calendar.render();
|
|
updateTime();
|
|
}
|
|
|
|
const updateTime = () => {
|
|
const start = calendar.getDateRangeStart().toDate();
|
|
const end = calendar.getDateRangeEnd().toDate();
|
|
const currentView = calendar.getViewName();
|
|
|
|
let dateFormat;
|
|
if (currentView === VIEW_DAY) {
|
|
dateFormat = moment(start).format(window.ljpccalendarmoduletranslations.dateFormat);
|
|
} else if (currentView === VIEW_WEEK) {
|
|
dateFormat = `${moment(start).format(window.ljpccalendarmoduletranslations.dateFormat)} - ${moment(end).format(window.ljpccalendarmoduletranslations.dateFormat)}`;
|
|
} else {
|
|
const middle = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);
|
|
dateFormat = moment(middle).format('MMMM YYYY');
|
|
}
|
|
|
|
dom.topBar.currentDate.innerHTML = dateFormat;
|
|
|
|
getEvents();
|
|
}
|
|
|
|
const api = {
|
|
getEvents: async (start, end) => {
|
|
const url = '{{ route('ljpccalendarmodule.api.events') }}' + `?start=${start}&end=${end}`;
|
|
return (await fetch(url)).json();
|
|
},
|
|
getEventById: async (eventId) => {
|
|
// Use the dedicated endpoint for better performance
|
|
const safeEventId = encodeURIComponent(eventId);
|
|
const url = '/calendar/api/event/' + safeEventId;
|
|
|
|
try {
|
|
// Add timeout to prevent hanging on large calendars
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
|
|
|
|
const response = await fetch(url, {
|
|
signal: controller.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
return null; // Event not found
|
|
}
|
|
throw new Error(`Failed to fetch event`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
console.error('Event fetch timed out - calendar may be too large');
|
|
}
|
|
console.error('Error fetching event:', error);
|
|
return null;
|
|
}
|
|
},
|
|
createEvent: async (event) => {
|
|
const url = '{{ route('ljpccalendarmodule.api.event.create') }}';
|
|
return (await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
},
|
|
body: JSON.stringify(event),
|
|
})).json();
|
|
},
|
|
updateEvent: async (event) => {
|
|
const url = '{{ route('ljpccalendarmodule.api.event.update') }}' + `?id=${event.uid}&_method=PUT`;
|
|
return (await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
},
|
|
body: JSON.stringify(event),
|
|
})).json();
|
|
},
|
|
deleteEvent: async (eventId, calendarId) => {
|
|
const url = '{{ route('ljpccalendarmodule.api.event.delete') }}' + `?id=${eventId}&calendarId=${calendarId}&_method=DELETE`;
|
|
return (await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
},
|
|
})).json();
|
|
},
|
|
}
|
|
|
|
const getEvents = (scrollToEventId) => {
|
|
const start = moment(calendar.getDateRangeStart().toDate()).set({"hour": 0, "minute": 0}).toISOString();
|
|
const end = moment(calendar.getDateRangeEnd().toDate()).set({"hour": 23, "minute": 59}).toISOString();
|
|
|
|
return api.getEvents(start, end).then(data => {
|
|
calendar.clear();
|
|
calendar.createEvents(data);
|
|
|
|
for (const calendarInstance of calendarInstances) {
|
|
calendar.setCalendarVisibility(calendarInstance.id, calendarInstance.isVisible);
|
|
}
|
|
|
|
// Check if there's an event ID in the URL hash and open that event
|
|
const eventId = hashParams.get('event');
|
|
// replace url to remove the event id
|
|
if (eventId && eventId.length > 0) {
|
|
const url = new URL(window.location.href);
|
|
url.hash = ''; // Remove the hash
|
|
window.history.replaceState({}, document.title, url.toString());
|
|
hashParams = new URLSearchParams(); // Clear the hashParams
|
|
}
|
|
|
|
if (eventId && eventId.length > 0) {
|
|
|
|
try {
|
|
// Try to find the event in current view
|
|
let event = null;
|
|
|
|
// First look for exact match
|
|
event = data.find(e => e.id === eventId);
|
|
|
|
// If not found, try a more flexible approach (numeric comparison if the ID is a number)
|
|
if (!event && !isNaN(parseInt(eventId))) {
|
|
const numericId = parseInt(eventId);
|
|
event = data.find(e => parseInt(e.id) === numericId);
|
|
}
|
|
|
|
if (event && event.id && event.calendarId) {
|
|
// Event is in current view, open it
|
|
const eventViewModel = {
|
|
id: event.id,
|
|
calendarId: event.calendarId,
|
|
title: event.title || '',
|
|
start: {toDate: () => new Date(event.start)},
|
|
end: {toDate: () => new Date(event.end)},
|
|
location: event.location || '',
|
|
body: event.body || '',
|
|
raw: event.raw || {customFields: {}}
|
|
};
|
|
|
|
// Make sure calendar for this event is visible
|
|
let calendarFound = false;
|
|
for (const calendarInstance of calendarInstances) {
|
|
if (calendarInstance.id === parseInt(event.calendarId)) {
|
|
calendarInstance.isVisible = true;
|
|
calendarInstance.render();
|
|
calendarFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
scrollToEventId = event.id;
|
|
|
|
// Open event details modal
|
|
setTimeout(() => {
|
|
try {
|
|
openEventDetailsModal(eventViewModel);
|
|
} catch (err) {
|
|
showFloatingAlert('error', '{{__('Error displaying event details')}}');
|
|
}
|
|
}, 300); // Short delay to ensure UI is ready
|
|
} else {
|
|
// Event not in current view, use direct method to fetch and open it
|
|
openEventById(eventId);
|
|
}
|
|
} catch (err) {
|
|
showFloatingAlert('error', '{{__('Error processing permalink')}}');
|
|
}
|
|
}
|
|
|
|
if (scrollToEventId) {
|
|
setTimeout(function () {
|
|
const dom = document.querySelector('.toastui-calendar-time');
|
|
const eventTop = document.querySelector('.toastui-calendar-event-time[data-event-id="' + scrollToEventId + '"]');
|
|
const scrollTop = eventTop.offsetTop - dom.offsetTop - 100; // Adjust scroll position
|
|
|
|
dom.scrollTo({top: scrollTop});
|
|
}, 100);
|
|
}
|
|
|
|
return data;
|
|
}).catch(error => {
|
|
showFloatingAlert('error', '{{__('Failed to load calendar events')}}');
|
|
return [];
|
|
});
|
|
}
|
|
|
|
dom.topBar.previousButton.addEventListener('click', () => {
|
|
calendar.prev();
|
|
updateTime()
|
|
});
|
|
dom.topBar.nextButton.addEventListener('click', () => {
|
|
calendar.next();
|
|
updateTime()
|
|
});
|
|
dom.topBar.todayButton.addEventListener('click', () => {
|
|
calendar.today();
|
|
updateTime()
|
|
});
|
|
|
|
const fetchCustomFields = async (calendarId) => {
|
|
try {
|
|
const baseUrl = '{{ route('ljpccalendarmodule.api.calendar.get', '') }}';
|
|
// Remove any trailing placeholders and construct the proper URL
|
|
const url = baseUrl.replace(/\{.*?\}$/, '') + '/' + calendarId;
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
return [];
|
|
}
|
|
const calendar = await response.json();
|
|
if (!calendar.custom_fields || calendar.custom_fields === null) {
|
|
return [];
|
|
}
|
|
const fields = calendar.custom_fields.fields || [];
|
|
return fields;
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const renderCustomFields = (fields, container, canEdit, values = {}) => {
|
|
container.innerHTML = '';
|
|
fields.forEach(field => {
|
|
const fieldElement = document.createElement('div');
|
|
fieldElement.className = 'form-group';
|
|
|
|
const label = document.createElement('label');
|
|
label.textContent = field.name + (field.required ? ' *' : '');
|
|
label.htmlFor = `custom_field_${field.id}`;
|
|
fieldElement.appendChild(label);
|
|
|
|
let input;
|
|
switch (field.type) {
|
|
case 'text':
|
|
case 'number':
|
|
case 'email':
|
|
case 'date':
|
|
input = document.createElement('input');
|
|
input.type = field.type;
|
|
input.className = 'form-control';
|
|
input.name = `custom_field_${field.id}`;
|
|
input.required = field.required;
|
|
input.value = values[`custom_field_${field.id}`] || '';
|
|
break;
|
|
case 'dropdown':
|
|
case 'multiselect':
|
|
input = document.createElement('select');
|
|
input.className = 'form-control';
|
|
input.style = 'height: auto;';
|
|
input.name = `custom_field_${field.id}`;
|
|
input.required = field.required;
|
|
if (field.type === 'multiselect') {
|
|
input.multiple = true;
|
|
}
|
|
field.options.forEach(option => {
|
|
const optionElement = document.createElement('option');
|
|
optionElement.value = option;
|
|
optionElement.textContent = option;
|
|
if (values[`custom_field_${field.id}`] === option || (Array.isArray(values[`custom_field_${field.id}`]) && values[`custom_field_${field.id}`].includes(option))) {
|
|
optionElement.selected = true;
|
|
}
|
|
input.appendChild(optionElement);
|
|
});
|
|
break;
|
|
case 'boolean':
|
|
input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.name = `custom_field_${field.id}`;
|
|
input.checked = values[`custom_field_${field.id}`] === true;
|
|
input.style = 'margin-left:5px;'
|
|
break;
|
|
case 'source':
|
|
if (!values.hasOwnProperty('conversation_url') || !values.hasOwnProperty('conversation_id')) {
|
|
return;
|
|
}
|
|
input = document.createElement('a');
|
|
input.href = values.conversation_url;
|
|
input.textContent = '#' + values.conversation_id;
|
|
input.target = '_blank';
|
|
input.style = "display:block;";
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
input.id = `custom_field_${field.id}`
|
|
input.disabled = !canEdit;
|
|
|
|
fieldElement.appendChild(input);
|
|
container.appendChild(fieldElement);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* New event modal
|
|
*/
|
|
const openNewEventModal = (event) => {
|
|
calendar.clearGridSelections();
|
|
|
|
dom.newEventModal.modal.style.display = 'block';
|
|
|
|
dom.newEventModal.start.value = moment(event.start).format('YYYY-MM-DD HH:mm');
|
|
dom.newEventModal.end.value = moment(event.end).format('YYYY-MM-DD HH:mm');
|
|
|
|
// Populate calendar options
|
|
dom.newEventModal.calendar.innerHTML = '<option value="" hidden>{{__('Select a calendar')}}</option>';
|
|
calendars.forEach(calendar => {
|
|
if (!hasPermissions(calendar.id, 'createItems')) {
|
|
return;
|
|
}
|
|
if (calendar.type === 'normal' || calendar.type === 'caldav') {
|
|
const option = document.createElement('option');
|
|
option.value = calendar.id;
|
|
option.text = calendar.name;
|
|
dom.newEventModal.calendar.add(option);
|
|
}
|
|
});
|
|
|
|
// Add event listener for calendar selection
|
|
dom.newEventModal.calendar.addEventListener('change', async function () {
|
|
const selectedCalendarId = this.value;
|
|
const customFields = await fetchCustomFields(selectedCalendarId);
|
|
const canEdit = hasPermissions(selectedCalendarId, 'createItems');
|
|
renderCustomFields(customFields, document.getElementById('event-custom-fields'), canEdit);
|
|
});
|
|
}
|
|
const closeNewEventModal = () => {
|
|
dom.newEventModal.modal.style.display = 'none';
|
|
dom.newEventModal.title.value = '';
|
|
dom.newEventModal.start.value = '';
|
|
dom.newEventModal.end.value = '';
|
|
dom.newEventModal.location.value = '';
|
|
dom.newEventModal.body.value = '';
|
|
dom.newEventModal.calendar.value = '';
|
|
|
|
// Clear custom fields
|
|
document.getElementById('event-custom-fields').innerHTML = '';
|
|
}
|
|
dom.newEventModal.modalClose.addEventListener('click', closeNewEventModal);
|
|
window.addEventListener('click', (event) => {
|
|
if (event.target === dom.newEventModal.modal) {
|
|
closeNewEventModal();
|
|
}
|
|
});
|
|
const eventForm = document.querySelector('#event-modal form');
|
|
eventForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
//disable save button
|
|
dom.newEventModal.saveButton.disabled = true;
|
|
|
|
const customFieldsData = {};
|
|
const fields = jQuery('#event-custom-fields input, #event-custom-fields select');
|
|
for (const fieldId of fields) {
|
|
const field = jQuery(fieldId);
|
|
customFieldsData[field.attr('name')] = field.attr('type') === 'checkbox' ? field.prop('checked') : field.val();
|
|
}
|
|
|
|
try {
|
|
api.createEvent({
|
|
calendarId: dom.newEventModal.calendar.value,
|
|
title: dom.newEventModal.title.value,
|
|
start: moment(dom.newEventModal.start.value).toISOString(),
|
|
end: moment(dom.newEventModal.end.value).toISOString(),
|
|
location: dom.newEventModal.location.value,
|
|
body: dom.newEventModal.body.value,
|
|
customFields: customFieldsData
|
|
}).then(response => {
|
|
//enable save button
|
|
dom.newEventModal.saveButton.disabled = false;
|
|
|
|
if (response.ok) {
|
|
getEvents();
|
|
closeNewEventModal();
|
|
showFloatingAlert('success', '{{__('Event created successfully')}}');
|
|
} else {
|
|
showFloatingAlert('error', '{{__('Failed to create event')}}');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to create event')}}');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Event details modal
|
|
*/
|
|
const openEventDetailsModal = async (event) => {
|
|
dom.eventDetailModal.modal.style.display = 'block';
|
|
|
|
// Store original times
|
|
dom.eventDetailModal.originalStart = event.start.toDate();
|
|
dom.eventDetailModal.originalEnd = event.end.toDate();
|
|
|
|
dom.eventDetailModal.title.value = event.title;
|
|
dom.eventDetailModal.start.value = moment(event.start.toDate()).format('YYYY-MM-DDTHH:mm');
|
|
dom.eventDetailModal.end.value = moment(event.end.toDate()).format('YYYY-MM-DDTHH:mm');
|
|
dom.eventDetailModal.location.value = event.location;
|
|
dom.eventDetailModal.body.value = event.body;
|
|
dom.eventDetailModal.hiddenUid.value = event.id;
|
|
if (event.raw.hasOwnProperty('customFields')) {
|
|
dom.eventDetailModal.hiddenCustomFields.value = JSON.stringify(event.raw.customFields);
|
|
} else {
|
|
dom.eventDetailModal.hiddenCustomFields.value = '{}';
|
|
}
|
|
|
|
// Clear previous options
|
|
dom.eventDetailModal.calendar.innerHTML = '';
|
|
|
|
// Populate calendar options
|
|
for (const calendar of calendars) {
|
|
if (calendar.id === event.calendarId) {
|
|
dom.eventDetailModal.calendar.innerHTML = calendar.name;
|
|
dom.eventDetailModal.hiddenCalendar.value = calendar.id;
|
|
|
|
if (event.raw.customFields && event.raw.customFields.hasOwnProperty('author_name')) {
|
|
dom.eventDetailModal.createdBy.innerHTML = event.raw.customFields.author_name;
|
|
} else {
|
|
jQuery('.event-details-created-by-form-group').hide();
|
|
}
|
|
|
|
const canEdit = hasPermissions(calendar.id, 'editItems') && (calendar.type === 'normal' || calendar.type === 'caldav');
|
|
const canCreate = hasPermissions(calendar.id, 'createItems') && (calendar.type === 'normal' || calendar.type === 'caldav');
|
|
|
|
dom.eventDetailModal.title.readOnly = !canEdit;
|
|
dom.eventDetailModal.start.readOnly = !canEdit;
|
|
dom.eventDetailModal.end.readOnly = !canEdit;
|
|
dom.eventDetailModal.location.readOnly = !canEdit;
|
|
dom.eventDetailModal.body.readOnly = !canEdit;
|
|
dom.eventDetailModal.updateButton.disabled = !canEdit;
|
|
dom.eventDetailModal.deleteButton.disabled = !canEdit;
|
|
dom.eventDetailModal.createCopyButton.disabled = !canCreate;
|
|
|
|
// Fetch and render custom fields
|
|
try {
|
|
const customFields = await fetchCustomFields(calendar.id);
|
|
const customFieldsContainer = document.getElementById('event-details-custom-fields');
|
|
const customFieldValues = (event.raw && event.raw.customFields) ? event.raw.customFields : {};
|
|
renderCustomFields(customFields, customFieldsContainer, canEdit, customFieldValues);
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to load custom fields')}}');
|
|
}
|
|
|
|
break; // Exit the loop once we've found the matching calendar
|
|
}
|
|
}
|
|
};
|
|
|
|
dom.eventDetailModal.deleteButton.addEventListener('click', async () => {
|
|
const eventId = dom.eventDetailModal.hiddenUid.value;
|
|
const calendarId = dom.eventDetailModal.hiddenCalendar.value;
|
|
|
|
if (!hasPermissions(calendarId, 'editItems')) {
|
|
showFloatingAlert('error', '{{__('You do not have permission to delete this event')}}');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('{{__('Are you sure you want to delete this event?')}}')) {
|
|
return;
|
|
}
|
|
|
|
// Disable delete and update buttons
|
|
dom.eventDetailModal.deleteButton.disabled = true;
|
|
dom.eventDetailModal.updateButton.disabled = true;
|
|
|
|
try {
|
|
const response = await api.deleteEvent(eventId, calendarId);
|
|
if (response.ok) {
|
|
getEvents();
|
|
closeEventDetailsModal();
|
|
showFloatingAlert('success', '{{__('Event deleted successfully')}}');
|
|
} else {
|
|
showFloatingAlert('error', '{{__('Failed to delete event')}}');
|
|
}
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to delete event')}}');
|
|
} finally {
|
|
// Enable delete and update buttons
|
|
dom.eventDetailModal.deleteButton.disabled = false;
|
|
dom.eventDetailModal.updateButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
const closeEventDetailsModal = () => {
|
|
dom.eventDetailModal.modal.style.display = 'none';
|
|
dom.eventDetailModal.title.value = '';
|
|
dom.eventDetailModal.start.value = '';
|
|
dom.eventDetailModal.end.value = '';
|
|
dom.eventDetailModal.location.value = '';
|
|
dom.eventDetailModal.body.value = '';
|
|
dom.eventDetailModal.calendar.innerHTML = '';
|
|
dom.eventDetailModal.originalStart = null;
|
|
dom.eventDetailModal.originalEnd = null;
|
|
|
|
|
|
// Clear custom fields
|
|
document.getElementById('event-details-custom-fields').innerHTML = '';
|
|
|
|
jQuery('.event-details-created-by-form-group').show();
|
|
}
|
|
|
|
// Copy permalink button handler
|
|
dom.eventDetailModal.copyPermalinkButton.addEventListener('click', () => {
|
|
const eventId = dom.eventDetailModal.hiddenUid.value;
|
|
if (!eventId) {
|
|
showFloatingAlert('error', '{{__('Could not generate permalink')}}');
|
|
return;
|
|
}
|
|
|
|
// Create the permalink with the event ID - use simple #eventID format for better compatibility
|
|
const url = new URL(window.location.href);
|
|
url.hash = eventId; // Simple hash format that will be detected by our hash parser
|
|
|
|
const permalinkUrl = url.toString();
|
|
|
|
// Copy to clipboard
|
|
navigator.clipboard.writeText(url.toString())
|
|
.then(() => {
|
|
showFloatingAlert('success', '{{__('Permalink copied to clipboard')}}');
|
|
})
|
|
.catch(() => {
|
|
// Fallback for browsers that don't support clipboard API
|
|
const tempInput = document.createElement('input');
|
|
tempInput.value = url.toString();
|
|
document.body.appendChild(tempInput);
|
|
tempInput.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(tempInput);
|
|
showFloatingAlert('success', '{{__('Permalink copied to clipboard')}}');
|
|
});
|
|
});
|
|
|
|
dom.eventDetailModal.createCopyButton.addEventListener('click', async () => {
|
|
const calendarId = dom.eventDetailModal.hiddenCalendar.value;
|
|
|
|
if (!hasPermissions(calendarId, 'createItems')) {
|
|
showFloatingAlert('error', '{{__('You do not have permission to create events in this calendar')}}');
|
|
return;
|
|
}
|
|
|
|
// Disable buttons while processing
|
|
dom.eventDetailModal.createCopyButton.disabled = true;
|
|
dom.eventDetailModal.updateButton.disabled = true;
|
|
dom.eventDetailModal.deleteButton.disabled = true;
|
|
|
|
const customFieldsData = {};
|
|
const fields = jQuery('#event-details-custom-fields input, #event-details-custom-fields select');
|
|
for (const fieldId of fields) {
|
|
const field = jQuery(fieldId);
|
|
customFieldsData[field.attr('name')] = field.attr('type') === 'checkbox' ? field.prop('checked') : field.val();
|
|
}
|
|
|
|
// Remove conversation_id and author_id from custom fields if they exist
|
|
delete customFieldsData['conversation_id'];
|
|
delete customFieldsData['author_id'];
|
|
|
|
const currentStart = new Date(dom.eventDetailModal.start.value);
|
|
const currentEnd = new Date(dom.eventDetailModal.end.value);
|
|
|
|
// Determine if we need to modify the title
|
|
let title = dom.eventDetailModal.title.value;
|
|
if (dom.eventDetailModal.originalStart.getTime() === currentStart.getTime() &&
|
|
dom.eventDetailModal.originalEnd.getTime() === currentEnd.getTime()) {
|
|
title = '{{__('Copy of')}} ' + title;
|
|
}
|
|
|
|
try {
|
|
const response = await api.createEvent({
|
|
calendarId: calendarId,
|
|
title: title,
|
|
start: moment(dom.eventDetailModal.start.value).toISOString(),
|
|
end: moment(dom.eventDetailModal.end.value).toISOString(),
|
|
location: dom.eventDetailModal.location.value,
|
|
body: dom.eventDetailModal.body.value,
|
|
customFields: customFieldsData
|
|
});
|
|
|
|
if (response.ok) {
|
|
getEvents();
|
|
closeEventDetailsModal();
|
|
showFloatingAlert('success', '{{__('Event copy created successfully')}}');
|
|
} else {
|
|
showFloatingAlert('error', '{{__('Failed to create event copy')}}');
|
|
}
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to create event copy')}}');
|
|
} finally {
|
|
// Re-enable buttons
|
|
dom.eventDetailModal.createCopyButton.disabled = false;
|
|
dom.eventDetailModal.updateButton.disabled = false;
|
|
dom.eventDetailModal.deleteButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
dom.eventDetailModal.modalClose.addEventListener('click', closeEventDetailsModal);
|
|
|
|
window.addEventListener('click', (event) => {
|
|
if (event.target === dom.eventDetailModal.modal) {
|
|
closeEventDetailsModal();
|
|
}
|
|
});
|
|
|
|
document.querySelector('#event-details-modal form').addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
//disable update and delete button
|
|
dom.eventDetailModal.updateButton.disabled = true;
|
|
dom.eventDetailModal.deleteButton.disabled = true;
|
|
|
|
const customFieldsData = {};
|
|
const fields = jQuery('#event-details-custom-fields input, #event-details-custom-fields select');
|
|
for (const fieldId of fields) {
|
|
const field = jQuery(fieldId);
|
|
customFieldsData[field.attr('name')] = field.attr('type') === 'checkbox' ? field.prop('checked') : field.val();
|
|
}
|
|
|
|
const originalCustomFields = JSON.parse(dom.eventDetailModal.hiddenCustomFields.value);
|
|
let newCustomFields = originalCustomFields;
|
|
|
|
for (const key in customFieldsData) {
|
|
if (customFieldsData[key] !== originalCustomFields[key] || !originalCustomFields.hasOwnProperty(key)) {
|
|
newCustomFields[key] = customFieldsData[key];
|
|
}
|
|
}
|
|
|
|
const updatedEvent = {
|
|
uid: dom.eventDetailModal.hiddenUid.value,
|
|
calendarId: dom.eventDetailModal.hiddenCalendar.value,
|
|
title: dom.eventDetailModal.title.value,
|
|
start: moment(dom.eventDetailModal.start.value).toISOString(),
|
|
end: moment(dom.eventDetailModal.end.value).toISOString(),
|
|
location: dom.eventDetailModal.location.value,
|
|
body: dom.eventDetailModal.body.value,
|
|
customFields: newCustomFields
|
|
};
|
|
|
|
try {
|
|
const response = await api.updateEvent(updatedEvent);
|
|
if (response.ok) {
|
|
getEvents();
|
|
closeEventDetailsModal();
|
|
showFloatingAlert('success', '{{__('Event updated successfully')}}');
|
|
} else {
|
|
showFloatingAlert('error', '{{__('Failed to update event')}}');
|
|
}
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to update event')}}');
|
|
} finally {
|
|
// Enable update and delete buttons
|
|
dom.eventDetailModal.updateButton.disabled = false;
|
|
dom.eventDetailModal.deleteButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
const dragEvent = (eventInfo) => {
|
|
calendar.clearGridSelections();
|
|
|
|
const changes = eventInfo.changes;
|
|
const event = eventInfo.event;
|
|
|
|
|
|
//updatedEvent = merge of changes + uid
|
|
const updatedEvent = {
|
|
uid: event.id,
|
|
calendarId: event.calendarId,
|
|
title: event.title,
|
|
location: event.location,
|
|
body: event.body,
|
|
start: moment(event.start.toDate()).toISOString(),
|
|
end: moment(event.end.toDate()).toISOString(),
|
|
};
|
|
|
|
if (changes.start) {
|
|
updatedEvent.start = moment(changes.start.toDate()).toISOString();
|
|
}
|
|
|
|
if (changes.end) {
|
|
updatedEvent.end = moment(changes.end.toDate()).toISOString();
|
|
}
|
|
|
|
try {
|
|
api.updateEvent(updatedEvent);
|
|
} catch (error) {
|
|
showFloatingAlert('error', '{{__('Failed to update event')}}');
|
|
}
|
|
|
|
calendar.updateEvent(event.id, event.calendarId, changes);
|
|
}
|
|
|
|
/**
|
|
* Calendar events
|
|
*/
|
|
calendar.on('selectDateTime', openNewEventModal);
|
|
calendar.on('beforeCreateEvent', (event) => {
|
|
calendar.clearGridSelections();
|
|
});
|
|
calendar.on('clickEvent', (event) => {
|
|
calendar.clearGridSelections();
|
|
|
|
openEventDetailsModal(event.event);
|
|
});
|
|
calendar.on('beforeUpdateEvent', dragEvent);
|
|
|
|
window.addEventListener('resize', () => {
|
|
rerender()
|
|
});
|
|
rerender();
|
|
|
|
});
|
|
</script>
|
|
|