Files
awo-hamburg-intranet/module/tasks/pages/admin/task_list.php
Moritz Weinmann 743476f64a Module tasks + knowledgecenter_update aus Kundenprojekt übernehmen (bereinigt)
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>
2026-06-22 09:20:02 +02:00

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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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>