Calendar Sync V1
This commit is contained in:
397
action.php
397
action.php
@@ -4,11 +4,15 @@ use dokuwiki\Extension\ActionPlugin;
|
||||
use dokuwiki\Extension\Event;
|
||||
use dokuwiki\Extension\EventHandler;
|
||||
use dokuwiki\plugin\luxtools\CacheInvalidation;
|
||||
use dokuwiki\plugin\luxtools\CalDavClient;
|
||||
use dokuwiki\plugin\luxtools\CalendarEvent;
|
||||
use dokuwiki\plugin\luxtools\CalendarService;
|
||||
use dokuwiki\plugin\luxtools\CalendarSlot;
|
||||
use dokuwiki\plugin\luxtools\ChronoID;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDayTemplate;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalIcsEvents;
|
||||
use dokuwiki\plugin\luxtools\IcsWriter;
|
||||
use dokuwiki\plugin\luxtools\MenuItem\InvalidateCache;
|
||||
require_once(__DIR__ . '/autoload.php');
|
||||
|
||||
@@ -65,6 +69,18 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$this,
|
||||
"handleCalendarWidgetAjax",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"AJAX_CALL_UNKNOWN",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"handleMaintenanceTaskAction",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"AJAX_CALL_UNKNOWN",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"handleCalendarSyncAction",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"ACTION_ACT_PREPROCESS",
|
||||
"BEFORE",
|
||||
@@ -105,6 +121,7 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
"page-link.js",
|
||||
"linkfavicon.js",
|
||||
"calendar-widget.js",
|
||||
"event-popup.js",
|
||||
"main.js",
|
||||
];
|
||||
|
||||
@@ -147,7 +164,18 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
|
||||
$this->sendNoStoreHeaders();
|
||||
|
||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs);
|
||||
// Load slot indicators and colors for the calendar widget
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
$indicators = CalendarService::monthIndicators($slots, $year, $month);
|
||||
$slotColors = [];
|
||||
foreach ($slots as $slot) {
|
||||
$color = $slot->getColor();
|
||||
if ($color !== '') {
|
||||
$slotColors[$slot->getKey()] = $color;
|
||||
}
|
||||
}
|
||||
|
||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors);
|
||||
if ($html === '') {
|
||||
http_status(500);
|
||||
echo 'Calendar rendering failed';
|
||||
@@ -458,50 +486,348 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
/**
|
||||
* Render local calendar events section for a given date.
|
||||
*
|
||||
* Uses the slot-aware CalendarService to render events from all enabled slots.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function renderChronologicalEventsHtml(string $dateIso): string
|
||||
{
|
||||
$icsConfig = (string)$this->getConf('calendar_ics_files');
|
||||
if (trim($icsConfig) === '') return '';
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
if ($slots === []) return '';
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
|
||||
if ($events === []) return '';
|
||||
$grouped = CalendarService::eventsForDateGrouped($slots, $dateIso);
|
||||
if ($grouped === []) return '';
|
||||
|
||||
$title = (string)$this->getLang('chronological_events_title');
|
||||
if ($title === '') $title = 'Events';
|
||||
$html = '';
|
||||
|
||||
$items = '';
|
||||
foreach ($events as $entry) {
|
||||
$summary = trim((string)($entry['summary'] ?? ''));
|
||||
if ($summary === '') $summary = '(ohne Titel)';
|
||||
// Render general events
|
||||
if (isset($grouped['general'])) {
|
||||
$title = (string)$this->getLang('chronological_events_title');
|
||||
if ($title === '') $title = 'Events';
|
||||
$html .= $this->renderEventSection($grouped['general'], $title, 'general');
|
||||
}
|
||||
|
||||
$time = trim((string)($entry['time'] ?? ''));
|
||||
$startIso = trim((string)($entry['startIso'] ?? ''));
|
||||
$isAllDay = (bool)($entry['allDay'] ?? false);
|
||||
// Render maintenance tasks
|
||||
if (isset($grouped['maintenance'])) {
|
||||
$title = (string)$this->getLang('chronological_maintenance_title');
|
||||
if ($title === '') $title = 'Tasks';
|
||||
$html .= $this->renderMaintenanceSection($grouped['maintenance'], $title, $dateIso);
|
||||
}
|
||||
|
||||
if ($isAllDay || $time === '') {
|
||||
$items .= '<li>' . hsc($summary) . '</li>';
|
||||
} else {
|
||||
$timeHtml = '<span class="luxtools-event-time"';
|
||||
if ($startIso !== '') {
|
||||
$timeHtml .= ' data-luxtools-start="' . hsc($startIso) . '"';
|
||||
}
|
||||
$timeHtml .= '>' . hsc($time) . '</span>';
|
||||
$items .= '<li>' . $timeHtml . ' - ' . hsc($summary) . '</li>';
|
||||
// Render slot3/slot4 if present
|
||||
foreach (['slot3', 'slot4'] as $slotKey) {
|
||||
if (isset($grouped[$slotKey]) && isset($slots[$slotKey])) {
|
||||
$label = $slots[$slotKey]->getLabel();
|
||||
$html .= $this->renderEventSection($grouped[$slotKey], $label, $slotKey);
|
||||
}
|
||||
}
|
||||
|
||||
if ($items === '') return '';
|
||||
$html = '<ul>' . $items . '</ul>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
return '<div class="luxtools-plugin luxtools-chronological-events">'
|
||||
/**
|
||||
* Render a section of events for a given slot.
|
||||
*
|
||||
* @param CalendarEvent[] $events
|
||||
* @param string $title
|
||||
* @param string $slotKey
|
||||
* @return string
|
||||
*/
|
||||
protected function renderEventSection(array $events, string $title, string $slotKey): string
|
||||
{
|
||||
$items = '';
|
||||
foreach ($events as $event) {
|
||||
$items .= $this->renderEventListItem($event);
|
||||
}
|
||||
if ($items === '') return '';
|
||||
|
||||
return '<div class="luxtools-plugin luxtools-chronological-events luxtools-slot-' . hsc($slotKey) . '">'
|
||||
. '<h2>' . hsc($title) . '</h2>'
|
||||
. $html
|
||||
. '<ul>' . $items . '</ul>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a maintenance task section with completion buttons.
|
||||
*
|
||||
* @param CalendarEvent[] $events
|
||||
* @param string $title
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function renderMaintenanceSection(array $events, string $title, string $dateIso): string
|
||||
{
|
||||
$items = '';
|
||||
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
|
||||
|
||||
foreach ($events as $event) {
|
||||
$items .= $this->renderMaintenanceListItem($event, $ajaxUrl);
|
||||
}
|
||||
if ($items === '') return '';
|
||||
|
||||
$secToken = function_exists('getSecurityToken') ? getSecurityToken() : '';
|
||||
|
||||
return '<div class="luxtools-plugin luxtools-chronological-events luxtools-chronological-maintenance"'
|
||||
. ' data-luxtools-ajax-url="' . hsc($ajaxUrl) . '"'
|
||||
. ' data-luxtools-sectok="' . hsc($secToken) . '">'
|
||||
. '<h2>' . hsc($title) . '</h2>'
|
||||
. '<ul>' . $items . '</ul>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single event as a list item with popup data attributes.
|
||||
*
|
||||
* @param CalendarEvent $event
|
||||
* @return string
|
||||
*/
|
||||
protected function renderEventListItem(CalendarEvent $event): string
|
||||
{
|
||||
$summaryHtml = hsc($event->summary);
|
||||
|
||||
// Build event detail data attributes for popup
|
||||
$dataAttrs = ' data-luxtools-event="1"';
|
||||
$dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"';
|
||||
$dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"';
|
||||
if ($event->endIso !== '') {
|
||||
$dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"';
|
||||
}
|
||||
if ($event->location !== '') {
|
||||
$dataAttrs .= ' data-event-location="' . hsc($event->location) . '"';
|
||||
}
|
||||
if ($event->description !== '') {
|
||||
$dataAttrs .= ' data-event-description="' . hsc($event->description) . '"';
|
||||
}
|
||||
$dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"';
|
||||
$dataAttrs .= ' data-event-slot="' . hsc($event->slotKey) . '"';
|
||||
|
||||
if ($event->allDay || $event->time === '') {
|
||||
return '<li' . $dataAttrs . '><span class="luxtools-event-summary">' . $summaryHtml . '</span></li>';
|
||||
}
|
||||
|
||||
$timeHtml = '<span class="luxtools-event-time" data-luxtools-start="' . hsc($event->startIso) . '">'
|
||||
. hsc($event->time) . '</span>';
|
||||
return '<li' . $dataAttrs . '>' . $timeHtml . ' - <span class="luxtools-event-summary">' . $summaryHtml . '</span></li>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a maintenance task as a list item with completion button.
|
||||
*
|
||||
* @param CalendarEvent $event
|
||||
* @param string $ajaxUrl
|
||||
* @return string
|
||||
*/
|
||||
protected function renderMaintenanceListItem(CalendarEvent $event, string $ajaxUrl): string
|
||||
{
|
||||
$isCompleted = $event->isCompleted();
|
||||
$classes = 'luxtools-maintenance-task';
|
||||
if ($isCompleted) $classes .= ' luxtools-task-completed';
|
||||
|
||||
$summaryHtml = hsc($event->summary);
|
||||
|
||||
// Data attributes for popup and completion
|
||||
$dataAttrs = ' data-luxtools-event="1"';
|
||||
$dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"';
|
||||
$dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"';
|
||||
if ($event->endIso !== '') {
|
||||
$dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"';
|
||||
}
|
||||
if ($event->location !== '') {
|
||||
$dataAttrs .= ' data-event-location="' . hsc($event->location) . '"';
|
||||
}
|
||||
if ($event->description !== '') {
|
||||
$dataAttrs .= ' data-event-description="' . hsc($event->description) . '"';
|
||||
}
|
||||
$dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"';
|
||||
$dataAttrs .= ' data-event-slot="maintenance"';
|
||||
$dataAttrs .= ' data-task-uid="' . hsc($event->uid) . '"';
|
||||
$dataAttrs .= ' data-task-date="' . hsc($event->dateIso) . '"';
|
||||
$dataAttrs .= ' data-task-recurrence="' . hsc($event->recurrenceId) . '"';
|
||||
$dataAttrs .= ' data-task-status="' . hsc($event->status) . '"';
|
||||
|
||||
$buttonLabel = $isCompleted
|
||||
? (string)$this->getLang('maintenance_task_reopen')
|
||||
: (string)$this->getLang('maintenance_task_complete');
|
||||
if ($buttonLabel === '') $buttonLabel = $isCompleted ? 'Reopen' : 'Complete';
|
||||
$buttonAction = $isCompleted ? 'reopen' : 'complete';
|
||||
|
||||
$buttonHtml = '<button type="button" class="luxtools-task-action" data-action="' . hsc($buttonAction) . '">'
|
||||
. hsc($buttonLabel) . '</button>';
|
||||
|
||||
$timeHtml = '';
|
||||
if (!$event->allDay && $event->time !== '') {
|
||||
$timeHtml = '<span class="luxtools-event-time" data-luxtools-start="' . hsc($event->startIso) . '">'
|
||||
. hsc($event->time) . '</span> - ';
|
||||
}
|
||||
|
||||
return '<li class="' . hsc($classes) . '"' . $dataAttrs . '>'
|
||||
. $timeHtml
|
||||
. '<span class="luxtools-event-summary">' . $summaryHtml . '</span> '
|
||||
. $buttonHtml
|
||||
. '</li>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX requests for marking maintenance tasks complete/reopen.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function handleMaintenanceTaskAction(Event $event, $param)
|
||||
{
|
||||
if ($event->data !== 'luxtools_maintenance_task') return;
|
||||
|
||||
$event->preventDefault();
|
||||
$event->stopPropagation();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$this->sendNoStoreHeaders();
|
||||
|
||||
global $INPUT;
|
||||
|
||||
// Verify security token
|
||||
if (!checkSecurityToken()) {
|
||||
http_status(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Security token mismatch']);
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $INPUT->str('action'); // 'complete' or 'reopen'
|
||||
$uid = $INPUT->str('uid');
|
||||
$dateIso = $INPUT->str('date');
|
||||
$recurrence = $INPUT->str('recurrence');
|
||||
|
||||
if (!in_array($action, ['complete', 'reopen'], true)) {
|
||||
http_status(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid action']);
|
||||
return;
|
||||
}
|
||||
if ($uid === '' || !ChronoID::isIsoDate($dateIso)) {
|
||||
http_status(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Missing uid or date']);
|
||||
return;
|
||||
}
|
||||
|
||||
$slots = CalendarSlot::loadAll($this);
|
||||
$maintenanceSlot = $slots['maintenance'] ?? null;
|
||||
if ($maintenanceSlot === null || !$maintenanceSlot->isEnabled()) {
|
||||
http_status(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Maintenance calendar not configured']);
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = ($action === 'complete') ? 'COMPLETED' : 'TODO';
|
||||
|
||||
// Update local ICS file
|
||||
$localOk = false;
|
||||
$file = $maintenanceSlot->getFile();
|
||||
if ($file !== '' && is_file($file)) {
|
||||
$localOk = IcsWriter::updateEventStatus($file, $uid, $recurrence, $newStatus, $dateIso);
|
||||
}
|
||||
|
||||
if (!$localOk) {
|
||||
http_status(500);
|
||||
echo json_encode(['ok' => false, 'error' => $this->getLang('maintenance_complete_error')]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear caches so next render picks up changes
|
||||
CalendarService::clearCache();
|
||||
|
||||
// Remote CalDAV write-back if configured
|
||||
$remoteOk = true;
|
||||
$remoteError = '';
|
||||
if ($maintenanceSlot->hasRemoteSource()) {
|
||||
try {
|
||||
$caldavOk = CalDavClient::updateEventStatus(
|
||||
$maintenanceSlot->getCaldavUrl(),
|
||||
$maintenanceSlot->getUsername(),
|
||||
$maintenanceSlot->getPassword(),
|
||||
$uid,
|
||||
$recurrence,
|
||||
$newStatus,
|
||||
$dateIso
|
||||
);
|
||||
if (!$caldavOk) {
|
||||
$remoteOk = false;
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed');
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$remoteOk = false;
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed');
|
||||
}
|
||||
}
|
||||
|
||||
$msg = ($action === 'complete')
|
||||
? $this->getLang('maintenance_complete_success')
|
||||
: $this->getLang('maintenance_reopen_success');
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'message' => $msg,
|
||||
'remoteOk' => $remoteOk,
|
||||
'remoteError' => $remoteError,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX requests for manual calendar sync.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function handleCalendarSyncAction(Event $event, $param)
|
||||
{
|
||||
if ($event->data !== 'luxtools_calendar_sync') return;
|
||||
|
||||
$event->preventDefault();
|
||||
$event->stopPropagation();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$this->sendNoStoreHeaders();
|
||||
|
||||
global $INPUT;
|
||||
|
||||
if (!checkSecurityToken()) {
|
||||
http_status(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Security token mismatch']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!function_exists('auth_isadmin') || !auth_isadmin()) {
|
||||
http_status(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Admin access required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
$results = [];
|
||||
$hasErrors = false;
|
||||
|
||||
foreach ($slots as $slot) {
|
||||
if (!$slot->hasRemoteSource()) continue;
|
||||
|
||||
$ok = CalDavClient::syncSlot($slot);
|
||||
$results[$slot->getKey()] = $ok;
|
||||
if (!$ok) $hasErrors = true;
|
||||
}
|
||||
|
||||
CalendarService::clearCache();
|
||||
|
||||
$msg = $hasErrors
|
||||
? $this->getLang('calendar_sync_partial')
|
||||
: $this->getLang('calendar_sync_success');
|
||||
|
||||
echo json_encode([
|
||||
'ok' => !$hasErrors,
|
||||
'message' => $msg,
|
||||
'results' => $results,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build wiki bullet list for local calendar events.
|
||||
*
|
||||
@@ -510,23 +836,20 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
*/
|
||||
protected function buildChronologicalEventsWiki(string $dateIso): string
|
||||
{
|
||||
$icsConfig = (string)$this->getConf('calendar_ics_files');
|
||||
if (trim($icsConfig) === '') return '';
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
if ($slots === []) return '';
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
|
||||
$events = CalendarService::eventsForDate($slots, $dateIso);
|
||||
if ($events === []) return '';
|
||||
|
||||
$lines = [];
|
||||
foreach ($events as $event) {
|
||||
$summary = trim((string)($event['summary'] ?? ''));
|
||||
if ($summary === '') $summary = '(ohne Titel)';
|
||||
$summary = str_replace(["\n", "\r"], ' ', $summary);
|
||||
$summary = str_replace(["\n", "\r"], ' ', $event->summary);
|
||||
|
||||
$time = trim((string)($event['time'] ?? ''));
|
||||
if ((bool)($event['allDay'] ?? false) || $time === '') {
|
||||
if ($event->allDay || $event->time === '') {
|
||||
$lines[] = ' * ' . $summary;
|
||||
} else {
|
||||
$lines[] = ' * ' . $time . ' - ' . $summary;
|
||||
$lines[] = ' * ' . $event->time . ' - ' . $summary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user