Remove maintenance feature
This commit is contained in:
277
action.php
277
action.php
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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'] = '';
|
||||||
|
|||||||
@@ -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) +
|
|
||||||
"§ok=" +
|
|
||||||
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;
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
74
style.css
74
style.css
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user