Remove maintenance feature

This commit is contained in:
2026-04-03 14:53:05 +02:00
parent 946c269d42
commit d33c7a748b
11 changed files with 28 additions and 889 deletions

View File

@@ -82,12 +82,6 @@ class action_plugin_luxtools extends ActionPlugin
$this, $this,
"handleCalendarWidgetAjax", "handleCalendarWidgetAjax",
); );
$controller->register_hook(
"AJAX_CALL_UNKNOWN",
"BEFORE",
$this,
"handleMaintenanceTaskAction",
);
$controller->register_hook( $controller->register_hook(
"AJAX_CALL_UNKNOWN", "AJAX_CALL_UNKNOWN",
"BEFORE", "BEFORE",
@@ -753,21 +747,8 @@ class action_plugin_luxtools extends ActionPlugin
); );
} }
// Render maintenance tasks // Render slot2/slot3/slot4 if present
if (isset($grouped["maintenance"])) { foreach (["slot2", "slot3", "slot4"] as $slotKey) {
$title = (string) $this->getLang("chronological_maintenance_title");
if ($title === "") {
$title = "Tasks";
}
$html .= $this->renderMaintenanceSection(
$grouped["maintenance"],
$title,
$dateIso,
);
}
// Render slot3/slot4 if present
foreach (["slot3", "slot4"] as $slotKey) {
if (isset($grouped[$slotKey]) && isset($slots[$slotKey])) { if (isset($grouped[$slotKey]) && isset($slots[$slotKey])) {
$label = $slots[$slotKey]->getLabel(); $label = $slots[$slotKey]->getLabel();
$html .= $this->renderEventSection( $html .= $this->renderEventSection(
@@ -814,51 +795,6 @@ class action_plugin_luxtools extends ActionPlugin
"</div>"; "</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. * Render a single event as a list item with popup data attributes.
* *
@@ -921,215 +857,6 @@ class action_plugin_luxtools extends ActionPlugin
"</span></li>"; "</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 {
$caldavResult = CalDavClient::updateEventStatus(
$maintenanceSlot->getCaldavUrl(),
$maintenanceSlot->getUsername(),
$maintenanceSlot->getPassword(),
$uid,
$recurrence,
$newStatus,
$dateIso,
);
if ($caldavResult !== "") {
$remoteOk = false;
$remoteError =
$this->getLang("maintenance_remote_write_failed") .
": " .
$caldavResult;
}
} catch (Throwable $e) {
$remoteOk = false;
$remoteError =
$this->getLang("maintenance_remote_write_failed") .
": " .
$e->getMessage();
}
}
$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. * Handle AJAX requests for manual calendar sync.
* *

View File

@@ -9,7 +9,7 @@ if (!defined('DOKU_INC')) die();
class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
{ {
/** @var string[] Calendar slot keys */ /** @var string[] Calendar slot keys */
protected $calendarSlotKeys = ['general', 'maintenance', 'slot3', 'slot4']; protected $calendarSlotKeys = ['general', 'slot2', 'slot3', 'slot4'];
/** @var string[] */ /** @var string[] */
protected $configKeys = [ protected $configKeys = [
@@ -38,12 +38,12 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
'calendar_general_password', 'calendar_general_password',
'calendar_general_color', 'calendar_general_color',
'calendar_general_display', 'calendar_general_display',
'calendar_maintenance_file', 'calendar_slot2_file',
'calendar_maintenance_caldav_url', 'calendar_slot2_caldav_url',
'calendar_maintenance_username', 'calendar_slot2_username',
'calendar_maintenance_password', 'calendar_slot2_password',
'calendar_maintenance_color', 'calendar_slot2_color',
'calendar_maintenance_display', 'calendar_slot2_display',
'calendar_slot3_file', 'calendar_slot3_file',
'calendar_slot3_caldav_url', 'calendar_slot3_caldav_url',
'calendar_slot3_username', 'calendar_slot3_username',
@@ -278,7 +278,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
// Calendar slot settings // Calendar slot settings
$slotLabels = [ $slotLabels = [
'general' => 'General', 'general' => 'General',
'maintenance' => 'Maintenance', 'slot2' => 'Slot 2',
'slot3' => 'Slot 3', 'slot3' => 'Slot 3',
'slot4' => 'Slot 4', 'slot4' => 'Slot 4',
]; ];

View File

@@ -37,7 +37,7 @@ $conf['open_service_url'] = 'http://127.0.0.1:8765';
// Base filesystem path for chronological photo integration. // Base filesystem path for chronological photo integration.
$conf['image_base_path'] = ''; $conf['image_base_path'] = '';
// Calendar slot configuration (4 slots: general, maintenance, slot3, slot4) // Calendar slot configuration (4 slots: general, slot2, slot3, slot4)
// Each slot has: file, caldav_url, username, password, color, display // Each slot has: file, caldav_url, username, password, color, display
$conf['calendar_general_file'] = ''; $conf['calendar_general_file'] = '';
$conf['calendar_general_caldav_url'] = ''; $conf['calendar_general_caldav_url'] = '';
@@ -46,12 +46,12 @@ $conf['calendar_general_password'] = '';
$conf['calendar_general_color'] = '#4a90d9'; $conf['calendar_general_color'] = '#4a90d9';
$conf['calendar_general_display'] = 'none'; $conf['calendar_general_display'] = 'none';
$conf['calendar_maintenance_file'] = ''; $conf['calendar_slot2_file'] = '';
$conf['calendar_maintenance_caldav_url'] = ''; $conf['calendar_slot2_caldav_url'] = '';
$conf['calendar_maintenance_username'] = ''; $conf['calendar_slot2_username'] = '';
$conf['calendar_maintenance_password'] = ''; $conf['calendar_slot2_password'] = '';
$conf['calendar_maintenance_color'] = '#e67e22'; $conf['calendar_slot2_color'] = '#e67e22';
$conf['calendar_maintenance_display'] = 'none'; $conf['calendar_slot2_display'] = 'none';
$conf['calendar_slot3_file'] = ''; $conf['calendar_slot3_file'] = '';
$conf['calendar_slot3_caldav_url'] = ''; $conf['calendar_slot3_caldav_url'] = '';

View File

@@ -1,14 +1,12 @@
/* global window, document, jQuery */ /* global window, document, jQuery */
/** /**
* Event Popup, Day Popup, Event CRUD, and Maintenance Task Handling * Event Popup, Day Popup, and Event CRUD
* *
* - Clicking an event item with data-luxtools-event="1" opens a detail popup. * - Clicking an event item with data-luxtools-event="1" opens a detail popup.
* - Clicking empty space in a calendar day cell opens a day popup listing all events. * - Clicking empty space in a calendar day cell opens a day popup listing all events.
* - Day popup includes a "Create Event" action for authenticated users. * - Day popup includes a "Create Event" action for authenticated users.
* - Event popup includes "Edit" and "Delete" actions for authenticated users. * - Event popup includes "Edit" and "Delete" actions for authenticated users.
* - Clicking a maintenance task action button sends an AJAX request to
* complete/reopen the task.
*/ */
(function () { (function () {
"use strict"; "use strict";
@@ -889,99 +887,6 @@
return { confirmDelete: confirmDelete, executeDelete: executeDelete }; return { confirmDelete: confirmDelete, executeDelete: executeDelete };
})(); })();
// ============================================================
// Maintenance Task Actions
// ============================================================
var MaintenanceTasks = (function () {
function handleAction(button) {
var action = button.getAttribute("data-action");
if (!action) return;
var item = button.closest("[data-task-uid]");
if (!item) item = button.closest("[data-uid]");
if (!item) return;
var uid =
item.getAttribute("data-task-uid") ||
item.getAttribute("data-uid") ||
"";
var date =
item.getAttribute("data-task-date") ||
item.getAttribute("data-date") ||
"";
var recurrence =
item.getAttribute("data-task-recurrence") ||
item.getAttribute("data-recurrence") ||
"";
if (!uid || !date) return;
var ajaxUrl = getAjaxUrl();
var sectok = getSecurityToken(item);
button.disabled = true;
button.textContent = "...";
var params =
"call=luxtools_maintenance_task" +
"&action=" +
encodeURIComponent(action) +
"&uid=" +
encodeURIComponent(uid) +
"&date=" +
encodeURIComponent(date) +
"&recurrence=" +
encodeURIComponent(recurrence) +
"&sectok=" +
encodeURIComponent(sectok);
var xhr = new XMLHttpRequest();
xhr.open("POST", ajaxUrl, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onload = function () {
var result;
try {
result = JSON.parse(xhr.responseText);
} catch (e) {
result = { ok: false, error: "Invalid response" };
}
if (result.ok) {
if (action === "complete") {
item.classList.add("luxtools-task-completed");
button.textContent = "Reopen";
button.setAttribute("data-action", "reopen");
} else {
item.classList.remove("luxtools-task-completed");
item.style.opacity = "1";
button.textContent = "Complete";
button.setAttribute("data-action", "complete");
}
button.disabled = false;
if (result.remoteOk === false && result.remoteError) {
showNotification(result.remoteError, "warning");
}
} else {
showNotification(result.error || "Action failed", "error");
button.textContent = action === "complete" ? "Complete" : "Reopen";
button.disabled = false;
}
};
xhr.onerror = function () {
showNotification("Network error", "error");
button.textContent = action === "complete" ? "Complete" : "Reopen";
button.disabled = false;
};
xhr.send(params);
}
return { handleAction: handleAction };
})();
// ============================================================ // ============================================================
// Event Delegation // Event Delegation
// ============================================================ // ============================================================
@@ -990,26 +895,6 @@
function (e) { function (e) {
var target = e.target; var target = e.target;
// Maintenance task action buttons (day pages)
if (
target.classList &&
target.classList.contains("luxtools-task-action")
) {
e.preventDefault();
MaintenanceTasks.handleAction(target);
return;
}
// Maintenance task complete buttons (syntax plugin list)
if (
target.classList &&
target.classList.contains("luxtools-task-complete-btn")
) {
e.preventDefault();
MaintenanceTasks.handleAction(target);
return;
}
// Event form save // Event form save
if ( if (
target.classList && target.classList &&
@@ -1166,5 +1051,4 @@
Luxtools.EventPopup = EventPopup; Luxtools.EventPopup = EventPopup;
Luxtools.DayPopup = DayPopup; Luxtools.DayPopup = DayPopup;
Luxtools.MaintenanceTasks = MaintenanceTasks;
})(); })();

View File

@@ -95,14 +95,6 @@ $lang["pagelink_unlinked"] = "Seite nicht verknüpft";
$lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft"; $lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft";
$lang["chronological_photos_title"] = "Fotos"; $lang["chronological_photos_title"] = "Fotos";
$lang["chronological_events_title"] = "Termine"; $lang["chronological_events_title"] = "Termine";
$lang["chronological_maintenance_title"] = "Aufgaben";
$lang["maintenance_task_complete"] = "Erledigen";
$lang["maintenance_task_reopen"] = "Wieder öffnen";
$lang["maintenance_no_tasks"] = "Keine offenen Aufgaben.";
$lang["maintenance_complete_success"] = "Aufgabe als erledigt markiert.";
$lang["maintenance_complete_error"] = "Aktualisierung der Aufgabe fehlgeschlagen.";
$lang["maintenance_reopen_success"] = "Aufgabe wieder geöffnet.";
$lang["maintenance_remote_write_failed"] = "Lokale Aktualisierung erfolgreich, aber CalDAV-Update fehlgeschlagen. Wird bei nächster Synchronisierung erneut versucht.";
$lang["calendar_sync_button"] = "Kalender synchronisieren"; $lang["calendar_sync_button"] = "Kalender synchronisieren";
$lang["calendar_sync_success"] = "Kalender-Synchronisierung abgeschlossen."; $lang["calendar_sync_success"] = "Kalender-Synchronisierung abgeschlossen.";
$lang["calendar_sync_error"] = "Kalender-Synchronisierung fehlgeschlagen."; $lang["calendar_sync_error"] = "Kalender-Synchronisierung fehlgeschlagen.";

View File

@@ -96,14 +96,6 @@ $lang["pagelink_multi_warning"] = "Multiple folders linked";
$lang["calendar_err_badmonth"] = "Invalid calendar month. Use YYYY-MM."; $lang["calendar_err_badmonth"] = "Invalid calendar month. Use YYYY-MM.";
$lang["chronological_photos_title"] = "Photos"; $lang["chronological_photos_title"] = "Photos";
$lang["chronological_events_title"] = "Events"; $lang["chronological_events_title"] = "Events";
$lang["chronological_maintenance_title"] = "Tasks";
$lang["maintenance_task_complete"] = "Complete";
$lang["maintenance_task_reopen"] = "Reopen";
$lang["maintenance_no_tasks"] = "No open tasks.";
$lang["maintenance_complete_success"] = "Task marked as completed.";
$lang["maintenance_complete_error"] = "Failed to update task.";
$lang["maintenance_reopen_success"] = "Task reopened.";
$lang["maintenance_remote_write_failed"] = "Local update succeeded, but remote CalDAV update failed. Will retry on next sync.";
$lang["calendar_sync_button"] = "Sync Calendars"; $lang["calendar_sync_button"] = "Sync Calendars";
$lang["calendar_sync_success"] = "Calendar sync completed."; $lang["calendar_sync_success"] = "Calendar sync completed.";
$lang["calendar_sync_error"] = "Calendar sync failed."; $lang["calendar_sync_error"] = "Calendar sync failed.";

View File

@@ -10,7 +10,7 @@ namespace dokuwiki\plugin\luxtools;
*/ */
class CalendarEvent class CalendarEvent
{ {
/** @var string Calendar slot key (e.g. 'general', 'maintenance') */ /** @var string Calendar slot key (e.g. 'general', 'slot2') */
public $slotKey; public $slotKey;
/** @var string Unique source event UID */ /** @var string Unique source event UID */
@@ -52,35 +52,4 @@ class CalendarEvent
/** @var string The date (YYYY-MM-DD) this event applies to */ /** @var string The date (YYYY-MM-DD) this event applies to */
public $dateIso; public $dateIso;
/**
* Build a stable completion key for maintenance task tracking.
*
* @return string
*/
public function completionKey(): string
{
return implode('|', [$this->slotKey, $this->uid, $this->dateIso]);
}
/**
* Whether this event/task is marked as completed.
*
* @return bool
*/
public function isCompleted(): bool
{
$s = strtoupper($this->status);
return $s === 'COMPLETED';
}
/**
* Whether this event/task is open (for maintenance filtering).
*
* @return bool
*/
public function isOpen(): bool
{
return !$this->isCompleted();
}
} }

View File

@@ -14,17 +14,13 @@ use Throwable;
/** /**
* Slot-aware calendar service. * Slot-aware calendar service.
* *
* Provides normalized event data grouped by slot for rendering, * Provides normalized event data grouped by slot for rendering and widget indicators.
* widget indicators, task list queries, and completion tracking.
*/ */
class CalendarService class CalendarService
{ {
/** @var array<string,CalendarEvent[]> In-request cache keyed by "slotKey|dateIso" */ /** @var array<string,CalendarEvent[]> In-request cache keyed by "slotKey|dateIso" */
protected static $dayCache = []; protected static $dayCache = [];
/** @var array<string,CalendarEvent[]> In-request cache keyed by "slotKey|all" for open tasks */
protected static $taskCache = [];
/** @var array<string,VCalendar|null> In-request cache keyed by file path */ /** @var array<string,VCalendar|null> In-request cache keyed by file path */
protected static $vcalCache = []; protected static $vcalCache = [];
@@ -111,81 +107,6 @@ class CalendarService
return self::slotEventsForDate($slot, $dateIso) !== []; return self::slotEventsForDate($slot, $dateIso) !== [];
} }
/**
* Get all open maintenance tasks due up to (and including) today.
*
* @param CalendarSlot $maintenanceSlot
* @param string $todayIso YYYY-MM-DD
* @param int $pastDays Maximum number of overdue days to include
* @return CalendarEvent[] Sorted: overdue first, then today, then by title
*/
public static function openMaintenanceTasks(CalendarSlot $maintenanceSlot, string $todayIso, int $pastDays = 30): array
{
if (!$maintenanceSlot->isEnabled()) return [];
if (!ChronoID::isIsoDate($todayIso)) return [];
$pastDays = max(0, $pastDays);
$file = $maintenanceSlot->getFile();
if ($file === '' || !is_file($file) || !is_readable($file)) return [];
$cacheKey = $maintenanceSlot->getKey() . '|tasks|' . $todayIso . '|' . $pastDays;
if (isset(self::$taskCache[$cacheKey])) {
return self::$taskCache[$cacheKey];
}
$tasks = self::parseAllTasksFromFile($file, $maintenanceSlot->getKey(), $todayIso);
$oldestOpenDateIso = self::oldestOpenTaskDate($todayIso, $pastDays);
// Filter: only non-completed, due today or earlier, and not older than the configured overdue window.
$open = [];
foreach ($tasks as $task) {
if ($task->isCompleted()) continue;
if ($task->dateIso > $todayIso) continue;
if ($task->dateIso < $oldestOpenDateIso) continue;
$open[] = $task;
}
// Sort: overdue first, then today, then by time, then by title
usort($open, static function (CalendarEvent $a, CalendarEvent $b) use ($todayIso): int {
$aOverdue = $a->dateIso < $todayIso;
$bOverdue = $b->dateIso < $todayIso;
if ($aOverdue !== $bOverdue) {
return $aOverdue ? -1 : 1;
}
$dateCmp = strcmp($a->dateIso, $b->dateIso);
if ($dateCmp !== 0) return $dateCmp;
$timeCmp = strcmp($a->time, $b->time);
if ($timeCmp !== 0) return $timeCmp;
return strcmp($a->summary, $b->summary);
});
self::$taskCache[$cacheKey] = $open;
return $open;
}
/**
* @param string $todayIso
* @param int $pastDays
* @return string
*/
protected static function oldestOpenTaskDate(string $todayIso, int $pastDays): string
{
try {
$today = new DateTimeImmutable($todayIso . ' 00:00:00', new DateTimeZone('UTC'));
} catch (Throwable $e) {
return $todayIso;
}
if ($pastDays === 0) {
return $today->format('Y-m-d');
}
return $today->sub(new DateInterval('P' . $pastDays . 'D'))->format('Y-m-d');
}
/** /**
* Get slot-level day indicator data for a whole month. * Get slot-level day indicator data for a whole month.
* *
@@ -329,49 +250,6 @@ class CalendarService
} }
} }
/**
* Parse all tasks (VEVENT with STATUS) from a maintenance file,
* expanding recurrences up to the given date.
*
* @param string $file
* @param string $slotKey
* @param string $todayIso
* @return CalendarEvent[]
*/
protected static function parseAllTasksFromFile(string $file, string $slotKey, string $todayIso): array
{
$calendar = self::readCalendar($file);
if ($calendar === null) return [];
try {
$component = $calendar;
// Expand from a reasonable lookback to tomorrow
$utc = new DateTimeZone('UTC');
$rangeStart = new DateTimeImmutable('2020-01-01 00:00:00', $utc);
$rangeEnd = new DateTimeImmutable($todayIso . ' 00:00:00', $utc);
$rangeEnd = $rangeEnd->add(new DateInterval('P1D'));
$expanded = $component->expand($rangeStart, $rangeEnd);
if (!($expanded instanceof VCalendar)) return [];
$tasks = [];
// Collect VEVENTs
foreach ($expanded->select('VEVENT') as $vevent) {
if (!($vevent instanceof VEvent)) continue;
$event = self::normalizeVEvent($vevent, $slotKey);
if ($event !== null) {
$tasks[] = $event;
}
}
return $tasks;
} catch (Throwable $e) {
return [];
}
}
/** /**
* Collect normalized events from an expanded VCalendar for a specific date. * Collect normalized events from an expanded VCalendar for a specific date.
* *
@@ -440,42 +318,6 @@ class CalendarService
return $event; return $event;
} }
/**
* Normalize a VEVENT into a CalendarEvent (without day filtering).
*
* @param VEvent $vevent
* @param string $slotKey
* @return CalendarEvent|null
*/
protected static function normalizeVEvent(VEvent $vevent, string $slotKey): ?CalendarEvent
{
if (!isset($vevent->DTSTART)) return null;
$isAllDay = strtoupper((string)($vevent->DTSTART['VALUE'] ?? '')) === 'DATE';
$start = self::toImmutable($vevent->DTSTART->getDateTime());
if ($start === null) return null;
$end = self::resolveEnd($vevent, $start, $isAllDay);
$event = new CalendarEvent();
$event->slotKey = $slotKey;
$event->uid = trim((string)($vevent->UID ?? ''));
$event->recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? trim((string)$vevent->{'RECURRENCE-ID'}) : '';
$event->summary = trim((string)($vevent->SUMMARY ?? ''));
if ($event->summary === '') $event->summary = '(ohne Titel)';
$event->startIso = $start->format(DateTimeInterface::ATOM);
$event->endIso = $end->format(DateTimeInterface::ATOM);
$event->allDay = $isAllDay;
$event->time = $isAllDay ? '' : $start->format('H:i');
$event->location = trim((string)($vevent->LOCATION ?? ''));
$event->description = trim((string)($vevent->DESCRIPTION ?? ''));
$event->status = strtoupper(trim((string)($vevent->STATUS ?? '')));
$event->componentType = 'VEVENT';
$event->dateIso = $start->format('Y-m-d');
return $event;
}
/** /**
* Resolve the end date/time for a VEVENT. * Resolve the end date/time for a VEVENT.
* *
@@ -573,7 +415,6 @@ class CalendarService
public static function clearCache(): void public static function clearCache(): void
{ {
self::$dayCache = []; self::$dayCache = [];
self::$taskCache = [];
self::$vcalCache = []; self::$vcalCache = [];
} }
} }

View File

@@ -12,7 +12,7 @@ namespace dokuwiki\plugin\luxtools;
class CalendarSlot class CalendarSlot
{ {
/** @var string[] Ordered list of all supported slot keys */ /** @var string[] Ordered list of all supported slot keys */
public const SLOT_KEYS = ['general', 'maintenance', 'slot3', 'slot4']; public const SLOT_KEYS = ['general', 'slot2', 'slot3', 'slot4'];
/** @var string[] Allowed widget indicator display positions */ /** @var string[] Allowed widget indicator display positions */
public const INDICATOR_DISPLAYS = ['none', 'top-left', 'top-right', 'bottom-left', 'bottom-right']; public const INDICATOR_DISPLAYS = ['none', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
@@ -20,7 +20,7 @@ class CalendarSlot
/** @var array<string,string> Human-readable labels for slot keys */ /** @var array<string,string> Human-readable labels for slot keys */
public const SLOT_LABELS = [ public const SLOT_LABELS = [
'general' => 'General', 'general' => 'General',
'maintenance' => 'Maintenance', 'slot2' => 'Slot 2',
'slot3' => 'Slot 3', 'slot3' => 'Slot 3',
'slot4' => 'Slot 4', 'slot4' => 'Slot 4',
]; ];

View File

@@ -724,7 +724,7 @@ div.luxtools-calendar
/* ============================================================ /* ============================================================
* Calendar Widget Indicators * Calendar Widget Indicators
* Colored corner markers showing which slots have events on a day. * Colored corner markers showing which slots have events on a day.
* Positions: general=top-left, maintenance=top-right, * Positions: general=top-left, slot2=top-right,
* slot3=bottom-right, slot4=bottom-left (clockwise) * slot3=bottom-right, slot4=bottom-left (clockwise)
* ============================================================ */ * ============================================================ */
div.luxtools-calendar td.luxtools-calendar-day { div.luxtools-calendar td.luxtools-calendar-day {
@@ -926,78 +926,6 @@ div.luxtools-chronological-events li[data-luxtools-event] .luxtools-event-time {
margin-right: 0.25em; margin-right: 0.25em;
} }
/* ============================================================
* Maintenance Tasks
* ============================================================ */
div.luxtools-chronological-maintenance li {
border-left-color: #e67e22;
}
li.luxtools-maintenance-task.luxtools-task-completed {
opacity: 0.5;
text-decoration: line-through;
}
button.luxtools-task-action,
button.luxtools-task-complete-btn {
margin-left: 0.5em;
padding: 0.15em 0.5em;
font-size: 0.85em;
border: 1px solid @ini_border;
border-radius: 0.2em;
background-color: @ini_background_alt;
cursor: pointer;
}
button.luxtools-task-action:hover,
button.luxtools-task-complete-btn:hover {
background-color: @ini_highlight;
}
button.luxtools-task-action:disabled,
button.luxtools-task-complete-btn:disabled {
opacity: 0.5;
cursor: wait;
}
/* ============================================================
* Maintenance Task List (syntax plugin)
* ============================================================ */
div.luxtools-maintenance-tasks {
margin: 1em 0;
}
ul.luxtools-maintenance-task-list {
list-style: none;
padding-left: 0;
}
ul.luxtools-maintenance-task-list li {
padding: 0.35em 0.5em;
margin: 0.25em 0;
border-left: 3px solid #e67e22;
}
li.luxtools-task-overdue .luxtools-task-date {
color: #c0392b;
font-weight: bold;
}
.luxtools-task-date {
font-family: monospace;
margin-right: 0.5em;
}
.luxtools-task-time {
font-weight: bold;
margin-right: 0.25em;
}
.luxtools-maintenance-task-item.luxtools-task-completed {
opacity: 0.5;
text-decoration: line-through;
}
/* ============================================================ /* ============================================================
* Event Popup (content-specific styles structural dialog * Event Popup (content-specific styles structural dialog
* styles live in dialog.css) * styles live in dialog.css)

View File

@@ -1,194 +0,0 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\CalendarService;
use dokuwiki\plugin\luxtools\CalendarSlot;
require_once(__DIR__ . '/../autoload.php');
/**
* luxtools Plugin: Maintenance task list syntax.
*
* Renders a list of all non-completed maintenance tasks due today or earlier.
*
* Syntax:
* {{maintenance_tasks>}}
* {{maintenance_tasks>&past=14}}
*/
class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
{
private const DEFAULT_PAST_DAYS = 30;
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
return 225;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern(
'\{\{maintenance_tasks>.*?\}\}',
$mode,
'plugin_luxtools_maintenance'
);
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
$match = substr($match, strlen('{{maintenance_tasks>'), -2);
$params = $this->parseFlags($match);
return [
'ok' => true,
'past' => $this->normalizePastDays($params['past'] ?? null),
];
}
/** @inheritdoc */
public function render($format, Doku_Renderer $renderer, $data)
{
if ($data === false || !is_array($data)) return false;
if ($format !== 'xhtml') return false;
if (!($renderer instanceof Doku_Renderer_xhtml)) return false;
$renderer->nocache();
$slots = CalendarSlot::loadAll($this);
$maintenanceSlot = $slots['maintenance'] ?? null;
if ($maintenanceSlot === null || !$maintenanceSlot->isEnabled()) {
$renderer->doc .= '<div class="luxtools-plugin luxtools-maintenance-tasks">'
. '<p class="luxtools-empty">'
. hsc($this->getLang('maintenance_no_tasks'))
. '</p></div>';
return true;
}
$todayIso = date('Y-m-d');
$pastDays = $this->normalizePastDays($data['past'] ?? null);
$tasks = CalendarService::openMaintenanceTasks($maintenanceSlot, $todayIso, $pastDays);
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
$secToken = function_exists('getSecurityToken') ? (string)getSecurityToken() : '';
$title = (string)$this->getLang('chronological_maintenance_title');
if ($title === '') $title = 'Tasks';
$renderer->doc .= '<div class="luxtools-plugin luxtools-maintenance-tasks"'
. ' data-luxtools-ajax-url="' . hsc($ajaxUrl) . '"'
. ' data-luxtools-sectok="' . hsc($secToken) . '">';
$renderer->doc .= '<h3>' . hsc($title) . '</h3>';
if ($tasks === []) {
$noTasks = (string)$this->getLang('maintenance_no_tasks');
if ($noTasks === '') $noTasks = 'No open tasks.';
$renderer->doc .= '<p class="luxtools-empty">' . hsc($noTasks) . '</p>';
} else {
$renderer->doc .= '<ul class="luxtools-maintenance-task-list">';
foreach ($tasks as $task) {
$overdue = ($task->dateIso < $todayIso);
$classes = 'luxtools-maintenance-task-item';
if ($overdue) {
$classes .= ' luxtools-task-overdue';
}
$renderer->doc .= '<li class="' . $classes . '"';
$renderer->doc .= ' data-uid="' . hsc($task->uid) . '"';
$renderer->doc .= ' data-date="' . hsc($task->dateIso) . '"';
$renderer->doc .= ' data-recurrence="' . hsc($task->recurrenceId) . '"';
$renderer->doc .= '>';
// Date badge
$renderer->doc .= '<span class="luxtools-task-date">' . hsc($task->dateIso) . '</span> ';
// Time if not all-day
if ($task->time !== '') {
$renderer->doc .= '<span class="luxtools-task-time">' . hsc($task->time) . '</span> ';
}
// Summary
$renderer->doc .= '<span class="luxtools-task-summary">' . hsc($task->summary) . '</span>';
// Complete button
$completeLabel = (string)$this->getLang('maintenance_task_complete');
if ($completeLabel === '') $completeLabel = 'Complete';
$renderer->doc .= ' <button class="luxtools-task-complete-btn" type="button"'
. ' data-action="complete"'
. '>' . hsc($completeLabel) . '</button>';
$renderer->doc .= '</li>';
}
$renderer->doc .= '</ul>';
}
$renderer->doc .= '</div>';
return true;
}
/**
* @param string $rawFlags
* @return array<string,string>
*/
protected function parseFlags(string $rawFlags): array
{
$rawFlags = trim($rawFlags);
if ($rawFlags === '') {
return [];
}
if ($rawFlags[0] === '&') {
$rawFlags = substr($rawFlags, 1);
}
$params = [];
foreach (explode('&', $rawFlags) as $flag) {
if (trim($flag) === '') continue;
[$name, $value] = array_pad(explode('=', $flag, 2), 2, '');
$name = strtolower(trim($name));
$value = trim($value);
if ($name === '') continue;
$params[$name] = $value;
}
return $params;
}
/**
* @param mixed $value
* @return int
*/
protected function normalizePastDays($value): int
{
if ($value === null || $value === '') {
return self::DEFAULT_PAST_DAYS;
}
if (is_int($value)) {
return max(0, $value);
}
$value = trim((string)$value);
if ($value === '' || !preg_match('/^-?\d+$/', $value)) {
return self::DEFAULT_PAST_DAYS;
}
return max(0, (int)$value);
}
}