Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt: Knowledgecenter: - Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten - Files-Gallery-Lizenzschlüssel entfernt (config.php) - Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur, nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss), Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge - 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …) Tasks: - Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments) - Task-Definitionen und Verlinkungen geleert - Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
413 lines
19 KiB
PHP
413 lines
19 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
if (!isset($tasks) && !isset($error)) {
|
|
require_once dirname(dirname(__DIR__)) . '/actions/admin/task_list_action.php';
|
|
$viewData = task_cert_admin_task_list_action();
|
|
if (is_array($viewData)) {
|
|
extract($viewData, EXTR_SKIP);
|
|
}
|
|
}
|
|
|
|
$tasks = $tasks ?? [];
|
|
$myTasks = $my_tasks ?? [];
|
|
$error = $error ?? null;
|
|
$searchQuery = (string) ($search_query ?? '');
|
|
$categoryIdFilter = (int) ($category_id_filter ?? 0);
|
|
$categoryFilter = is_array($category_filter ?? null) ? $category_filter : null;
|
|
$categoryFilterName = trim((string) ($categoryFilter['name'] ?? ''));
|
|
$selectedContactId = (int) ($selected_contact_id ?? 0);
|
|
$selectedContactName = (string) ($selected_contact_name ?? '');
|
|
$contactAssignments = is_array($contact_assignments ?? null) ? $contact_assignments : [];
|
|
|
|
$statusLabels = TaskCertConstants::assignmentStatusLabels();
|
|
|
|
if (!function_exists('task_cert_render_styles')) {
|
|
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
|
}
|
|
task_cert_render_styles(['admin']);
|
|
$taskListUrl = task_cert_view_url('admin.task_list');
|
|
$taskFormUrl = task_cert_view_url('admin.task_form');
|
|
$taskApprovalQueueUrl = task_cert_view_url('admin.approval_queue');
|
|
$taskEscalationUrl = task_cert_view_url('admin.escalation_dashboard');
|
|
$taskCategoriesUrl = task_cert_view_url('admin.task_categories');
|
|
$taskListWithCategoryUrl = $categoryIdFilter > 0
|
|
? task_cert_view_url('admin.task_list', ['category_id' => $categoryIdFilter])
|
|
: $taskListUrl;
|
|
?>
|
|
<div class="task-cert task-cert-admin-list">
|
|
<div class="task-cert-admin-toolbar">
|
|
<div class="task-cert-admin-toolbar-head">
|
|
<h2 class="task-cert-admin-toolbar-title">Admin</h2>
|
|
<div class="task-cert-admin-toolbar-actions">
|
|
<?php if (!$error): ?>
|
|
<a href="<?php echo TaskCertView::e($taskFormUrl); ?>" class="btn task-cert-btn-success">Neue Aufgabe</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="task-cert-admin-toolbar-nav">
|
|
<div class="task-cert-admin-nav" aria-label="Admin-Navigation">
|
|
<a href="<?php echo TaskCertView::e($taskListUrl); ?>" class="task-cert-admin-nav-link is-active">Aufgabenliste</a>
|
|
<a href="<?php echo TaskCertView::e($taskApprovalQueueUrl); ?>" class="task-cert-admin-nav-link">Freigaben</a>
|
|
<a href="<?php echo TaskCertView::e($taskEscalationUrl); ?>" class="task-cert-admin-nav-link">Eskalationen</a>
|
|
<a href="<?php echo TaskCertView::e($taskCategoriesUrl); ?>" class="task-cert-admin-nav-link">Kategorien</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="task-cert-error"><?php echo TaskCertView::e($error); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($categoryIdFilter > 0): ?>
|
|
<div class="task-cert-info">
|
|
Gefiltert nach Kategorie:
|
|
<strong><?php echo TaskCertView::e($categoryFilterName !== '' ? $categoryFilterName : ('Kategorie #' . $categoryIdFilter)); ?></strong>.
|
|
<a href="<?php echo TaskCertView::e($taskListUrl); ?>">Filter entfernen</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
$taskDetailUrl = task_cert_view_url('admin.task_detail');
|
|
$formatDateTime = static function ($raw): string {
|
|
$raw = trim((string) $raw);
|
|
if ($raw === '') { return '-'; }
|
|
try { return (new DateTimeImmutable($raw))->format('d.m.Y H:i'); }
|
|
catch (Throwable $e) { return $raw; }
|
|
};
|
|
$unitLabels = ['day' => 'Tägl.', 'week' => 'Wöch.', 'month' => 'Mon.', 'year' => 'Jährl.'];
|
|
$renderRecurrenceBadge = static function (array $row) use ($unitLabels): string {
|
|
$freq = (string) ($row['frequency_type'] ?? '');
|
|
$basis = (string) ($row['recurrence_basis'] ?? '');
|
|
$unit = (string) ($row['recurrence_unit'] ?? '');
|
|
if ($freq !== 'recurring') {
|
|
return '<span class="task-cert-recurrence-badge is-onetime">Einmalig</span>';
|
|
}
|
|
$unitLabel = $unitLabels[$unit] ?? 'Wdh.';
|
|
if ($basis === 'completion') {
|
|
return '<span class="task-cert-recurrence-badge is-completion">' . TaskCertView::e($unitLabel) . ' (Erled.)</span>';
|
|
}
|
|
return '<span class="task-cert-recurrence-badge is-calendar">' . TaskCertView::e($unitLabel) . ' (Kal.)</span>';
|
|
};
|
|
?>
|
|
|
|
<div class="task-cert-admin-list-grid">
|
|
<?php if ($myTasks !== []): ?>
|
|
<div class="task-cert-admin-list-section">
|
|
<h3 class="task-cert-admin-list-section-title">Meine Aufgaben</h3>
|
|
<input type="search" class="task-cert-admin-table-filter" data-table-filter placeholder="Suche in Meine Aufgaben">
|
|
<div class="task-cert-table-wrap">
|
|
<table class="task-cert-table" data-filterable-table>
|
|
<thead>
|
|
<tr>
|
|
<th>Titel</th>
|
|
<th>Kategorie</th>
|
|
<th>Rhythmus</th>
|
|
<th>Aktiv</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($myTasks as $row): ?>
|
|
<?php
|
|
$taskCategoryName = trim((string) ($row['task_category_name'] ?? ''));
|
|
if ($taskCategoryName === '') { $taskCategoryName = 'Allgemein'; }
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?php echo TaskCertView::e($taskFormUrl . '?id=' . (int) $row['id']); ?>">
|
|
<?php echo TaskCertView::e($row['title']); ?>
|
|
</a>
|
|
</td>
|
|
<td><?php echo TaskCertView::e($taskCategoryName); ?></td>
|
|
<td><?php echo $renderRecurrenceBadge($row); ?></td>
|
|
<td><?php echo ((int) $row['active'] === 1) ? 'Ja' : 'Nein'; ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="task-cert-admin-list-section">
|
|
<h3 class="task-cert-admin-list-section-title">Alle Aufgaben</h3>
|
|
<input type="search" class="task-cert-admin-table-filter" data-table-filter placeholder="Suche in Alle Aufgaben">
|
|
<div class="task-cert-table-wrap">
|
|
<table class="task-cert-table" data-filterable-table>
|
|
<thead>
|
|
<tr>
|
|
<th>Titel</th>
|
|
<th>Kategorie</th>
|
|
<th>Rhythmus</th>
|
|
<th>Aktiv</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($tasks as $row): ?>
|
|
<?php
|
|
$taskCategoryName = trim((string) ($row['task_category_name'] ?? ''));
|
|
if ($taskCategoryName === '') { $taskCategoryName = 'Allgemein'; }
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?php echo TaskCertView::e($taskFormUrl . '?id=' . (int) $row['id']); ?>">
|
|
<?php echo TaskCertView::e($row['title']); ?>
|
|
</a>
|
|
</td>
|
|
<td><?php echo TaskCertView::e($taskCategoryName); ?></td>
|
|
<td><?php echo $renderRecurrenceBadge($row); ?></td>
|
|
<td><?php echo ((int) $row['active'] === 1) ? 'Ja' : 'Nein'; ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if ($tasks === []): ?>
|
|
<tr>
|
|
<td colspan="4">Noch keine Aufgaben vorhanden.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="task-cert-admin-contact-search-section">
|
|
<h3 class="task-cert-admin-list-section-title">Mitarbeiter-Aufgaben</h3>
|
|
<form method="get" action="<?php echo TaskCertView::e($taskListUrl); ?>" class="task-cert-admin-contact-search-form">
|
|
<?php if ($categoryIdFilter > 0): ?>
|
|
<input type="hidden" name="category_id" value="<?php echo $categoryIdFilter; ?>">
|
|
<?php endif; ?>
|
|
<div class="task-cert-contact-autocomplete" id="tc-contact-autocomplete">
|
|
<input type="hidden" name="contact_id" id="tc-contact-id" value="<?php echo $selectedContactId > 0 ? $selectedContactId : ''; ?>">
|
|
<input type="text"
|
|
id="tc-contact-search"
|
|
class="task-cert-contact-autocomplete-input"
|
|
autocomplete="off"
|
|
placeholder="Mitarbeiter suchen..."
|
|
value="<?php echo $selectedContactId > 0 ? TaskCertView::e($selectedContactName) : ''; ?>">
|
|
<ul class="task-cert-contact-autocomplete-dropdown" id="tc-contact-dropdown"></ul>
|
|
</div>
|
|
<?php if ($selectedContactId > 0): ?>
|
|
<a href="<?php echo TaskCertView::e($taskListWithCategoryUrl); ?>" class="btn">Zurücksetzen</a>
|
|
<?php endif; ?>
|
|
</form>
|
|
|
|
<?php if ($selectedContactId > 0): ?>
|
|
<input type="search" class="task-cert-admin-table-filter" data-table-filter placeholder="Suche in Aufgaben von <?php echo TaskCertView::e($selectedContactName); ?>">
|
|
<div class="task-cert-table-wrap">
|
|
<table class="task-cert-table" data-filterable-table>
|
|
<thead>
|
|
<tr>
|
|
<th>Aufgabe</th>
|
|
<th>Kategorie</th>
|
|
<th>Status</th>
|
|
<th>Zugewiesen am</th>
|
|
<th>Fällig am</th>
|
|
<th>Letzte Einreichung</th>
|
|
<th>Zyklus</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($contactAssignments as $row): ?>
|
|
<?php
|
|
$aStatus = (string) ($row['status'] ?? '');
|
|
$aCategoryName = trim((string) ($row['task_category_name'] ?? ''));
|
|
if ($aCategoryName === '') { $aCategoryName = 'Allgemein'; }
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?php echo TaskCertView::e($taskFormUrl . '?id=' . (int) $row['task_id']); ?>">
|
|
<?php echo TaskCertView::e((string) ($row['task_title'] ?? '-')); ?>
|
|
</a>
|
|
</td>
|
|
<td><?php echo TaskCertView::e($aCategoryName); ?></td>
|
|
<td><span class="task-cert-status-badge status-<?php echo TaskCertView::e($aStatus); ?>"><?php echo TaskCertView::e($statusLabels[$aStatus] ?? $aStatus); ?></span></td>
|
|
<td><?php echo TaskCertView::e($formatDateTime($row['created_at'] ?? '')); ?></td>
|
|
<td><?php echo TaskCertView::e($formatDateTime($row['due_at'] ?? '')); ?></td>
|
|
<td><?php echo TaskCertView::e($formatDateTime($row['last_submitted_at'] ?? '')); ?></td>
|
|
<td><?php echo TaskCertView::e((string) ($row['cycle_key'] ?? '')); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if ($contactAssignments === []): ?>
|
|
<tr>
|
|
<td colspan="7">Keine Aufgaben für diesen Mitarbeiter gefunden.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="task-cert-hint">Wählen Sie einen Mitarbeiter aus um dessen Aufgaben zu sehen.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
/* ── Table filter ── */
|
|
var filters = document.querySelectorAll('[data-table-filter]');
|
|
for (var i = 0; i < filters.length; i++) {
|
|
(function (input) {
|
|
var section = input.closest('.task-cert-admin-list-section, .task-cert-admin-contact-search-section');
|
|
if (!section) return;
|
|
var table = section.querySelector('[data-filterable-table]');
|
|
if (!table) return;
|
|
input.addEventListener('input', function () {
|
|
var query = (input.value || '').toLowerCase();
|
|
var rows = table.querySelectorAll('tbody tr');
|
|
for (var j = 0; j < rows.length; j++) {
|
|
var text = (rows[j].textContent || '').toLowerCase();
|
|
rows[j].style.display = query === '' || text.indexOf(query) !== -1 ? '' : 'none';
|
|
}
|
|
});
|
|
})(filters[i]);
|
|
}
|
|
|
|
/* ── Contact autocomplete ── */
|
|
var searchInput = document.getElementById('tc-contact-search');
|
|
var hiddenInput = document.getElementById('tc-contact-id');
|
|
var dropdown = document.getElementById('tc-contact-dropdown');
|
|
var wrapper = document.getElementById('tc-contact-autocomplete');
|
|
if (!searchInput || !hiddenInput || !dropdown) return;
|
|
|
|
var searchEndpoint = <?php echo json_encode(task_cert_base_url() . '/endpoints/admin/contact_search.php'); ?>;
|
|
var debounceTimer = null;
|
|
var selectedIndex = -1;
|
|
|
|
function renderDropdown(results) {
|
|
if (!results || results.length === 0) {
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
selectedIndex = -1;
|
|
return;
|
|
}
|
|
dropdown.innerHTML = results.map(function (c, idx) {
|
|
return '<li class="task-cert-contact-autocomplete-item" data-id="' + c.id + '" data-label="' + escapeAttr(c.label) + '">'
|
|
+ escapeHtml(c.label) + '</li>';
|
|
}).join('');
|
|
dropdown.classList.add('is-open');
|
|
selectedIndex = -1;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function escapeAttr(str) {
|
|
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function highlightItem(idx) {
|
|
var items = dropdown.querySelectorAll('.task-cert-contact-autocomplete-item');
|
|
for (var j = 0; j < items.length; j++) {
|
|
items[j].classList.toggle('is-highlighted', j === idx);
|
|
}
|
|
}
|
|
|
|
function selectContact(id, label) {
|
|
hiddenInput.value = id;
|
|
searchInput.value = label;
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
selectedIndex = -1;
|
|
hiddenInput.closest('form').submit();
|
|
}
|
|
|
|
searchInput.addEventListener('input', function () {
|
|
clearTimeout(debounceTimer);
|
|
var term = searchInput.value.trim();
|
|
|
|
// Clear hidden value when user edits the field
|
|
hiddenInput.value = '';
|
|
|
|
if (term.length < 2) {
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
return;
|
|
}
|
|
|
|
debounceTimer = setTimeout(function () {
|
|
fetch(searchEndpoint + '?term=' + encodeURIComponent(term))
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
renderDropdown(data.results || []);
|
|
})
|
|
.catch(function () {
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
});
|
|
}, 250);
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', function (e) {
|
|
var items = dropdown.querySelectorAll('.task-cert-contact-autocomplete-item');
|
|
if (items.length === 0) return;
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
|
|
highlightItem(selectedIndex);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
|
highlightItem(selectedIndex);
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (selectedIndex >= 0 && items[selectedIndex]) {
|
|
selectContact(items[selectedIndex].dataset.id, items[selectedIndex].dataset.label);
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
selectedIndex = -1;
|
|
}
|
|
});
|
|
|
|
dropdown.addEventListener('click', function (e) {
|
|
var li = e.target.closest('.task-cert-contact-autocomplete-item');
|
|
if (!li) return;
|
|
selectContact(li.dataset.id, li.dataset.label);
|
|
});
|
|
|
|
document.addEventListener('click', function (e) {
|
|
if (!e.target.closest('#tc-contact-autocomplete')) {
|
|
dropdown.innerHTML = '';
|
|
dropdown.classList.remove('is-open');
|
|
selectedIndex = -1;
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<?php
|
|
$heartbeats = CronHeartbeat::readAll();
|
|
$cronJobs = [
|
|
'materialize_assignments' => 'Zuweisungen materialisieren',
|
|
'recalculate_escalations' => 'Eskalationen berechnen',
|
|
'sync_assignments' => 'Zuweisungen synchronisieren',
|
|
];
|
|
?>
|
|
<div class="task-cert-admin-cron-status">
|
|
<?php foreach ($cronJobs as $jobKey => $jobLabel): ?>
|
|
<?php
|
|
$hb = $heartbeats[$jobKey] ?? null;
|
|
$lastRun = '';
|
|
if (is_array($hb) && isset($hb['timestamp'])) {
|
|
try {
|
|
$lastRun = (new DateTimeImmutable($hb['timestamp']))->format('d.m.Y H:i');
|
|
} catch (Throwable $e) {
|
|
$lastRun = (string) $hb['timestamp'];
|
|
}
|
|
}
|
|
?>
|
|
<span>
|
|
<?php echo TaskCertView::e($jobLabel); ?>:
|
|
<?php echo $lastRun !== '' ? TaskCertView::e($lastRun) : '<em>Noch nie gelaufen</em>'; ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
</div>
|