Files
awo-hamburg-intranet/module/tasks/pages/admin/task_detail.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

553 lines
26 KiB
PHP

<?php
declare(strict_types=1);
if (!isset($task) && !isset($error)) {
require_once dirname(dirname(__DIR__)) . '/actions/admin/task_detail_action.php';
$viewData = task_cert_admin_task_detail_action();
if (is_array($viewData)) {
extract($viewData, EXTR_SKIP);
}
}
$error = $error ?? null;
$task = $task ?? null;
$dashboard = is_array($dashboard ?? null) ? $dashboard : [];
if (!function_exists('task_cert_render_styles')) {
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
}
task_cert_render_styles(['admin'], ['vendor/gridjs/mermaid.min.css']);
task_cert_render_scripts([
'vendor/gridjs/gridjs.umd.js',
'js/task_cert_tabs.js',
'js/task_cert_admin_overview_grid.js',
'js/task_cert_admin_actions.js',
'js/task_cert_admin_mark_completed.js',
]);
$taskId = (int) ($task['id'] ?? 0);
$taskTitle = (string) ($task['title'] ?? 'Übersicht');
$taskListUrl = task_cert_view_url('admin.task_list');
$taskEditUrl = $taskId > 0
? task_cert_view_url('admin.task_form', ['id' => $taskId])
: task_cert_view_url('admin.task_form');
$taskDetailUrl = task_cert_view_url('admin.task_detail');
$approvalDecisionUrl = task_cert_base_url() . '/endpoints/admin/approval_decision.php';
$resetAssignmentUrl = task_cert_base_url() . '/endpoints/admin/reset_assignment_progress.php';
$resetCycleUrl = task_cert_base_url() . '/endpoints/admin/reset_cycle_progress.php';
$markCompletedMethodsUrl = task_cert_base_url() . '/endpoints/admin/mark_completed_methods.php';
$markCompletedUrl = task_cert_base_url() . '/endpoints/admin/mark_completed.php';
$csrfToken = (string) ($csrf_token ?? '');
$filters = is_array($dashboard['filters'] ?? null) ? $dashboard['filters'] : [];
$cycleOptions = is_array($dashboard['cycle_options'] ?? null) ? $dashboard['cycle_options'] : [];
$selectedCycleKey = (string) ($dashboard['selected_cycle_key'] ?? '');
$cycleSelectionWarning = trim((string) ($dashboard['cycle_selection_warning'] ?? ''));
$activeTabKey = (string) ($dashboard['active_tab_key'] ?? 'overview');
$showCycleSelector = (bool) ($dashboard['show_cycle_selector'] ?? true);
$exportMode = (string) ($dashboard['export_mode'] ?? 'cycle');
$canExportAnswers = (bool) ($dashboard['can_export_answers'] ?? false);
$tabIndexMap = [
'overview' => '0',
'approvals' => '1',
'answers' => '2',
// Backward-Kompatibilität: alte Tab-Keys zeigen auf Übersicht
'todo' => '0',
'completed' => '0',
'overdue' => '0',
];
// Status-Vorfilter aus altem Tab-Key ableiten
$initialStatusFilter = '';
if ($activeTabKey === 'todo') {
$initialStatusFilter = 'open';
} elseif ($activeTabKey === 'completed') {
$initialStatusFilter = 'completed';
} elseif ($activeTabKey === 'overdue') {
$initialStatusFilter = 'overdue';
}
$activeTabIndex = $tabIndexMap[$activeTabKey] ?? '0';
$kpis = is_array($dashboard['kpis'] ?? null) ? $dashboard['kpis'] : [];
$methodStats = is_array($dashboard['method_stats'] ?? null) ? $dashboard['method_stats'] : [];
$todoRows = is_array($dashboard['todo_rows'] ?? null) ? $dashboard['todo_rows'] : [];
$completedRows = is_array($dashboard['completed_rows'] ?? null) ? $dashboard['completed_rows'] : [];
$approvalRows = is_array($dashboard['approval_rows'] ?? null) ? $dashboard['approval_rows'] : [];
$overdueRows = is_array($dashboard['overdue_rows'] ?? null) ? $dashboard['overdue_rows'] : [];
$submissionRows = is_array($dashboard['submission_rows'] ?? null) ? $dashboard['submission_rows'] : [];
// Zusammengeführte Zeilen für die Übersicht-Grid-Tabelle
$mergedRows = [];
$seenAssignmentIds = [];
$todoAssignmentIds = [];
foreach ($todoRows as $row) {
$assignmentId = (int) ($row['id'] ?? 0);
if ($assignmentId > 0 && isset($seenAssignmentIds[$assignmentId])) {
continue;
}
$seenAssignmentIds[$assignmentId] = true;
$todoAssignmentIds[$assignmentId] = true;
$graceUntilRaw = trim((string) ($row['grace_until'] ?? ''));
$graceBadgeHtml = '';
if ($graceUntilRaw !== '') {
try {
$graceUntilDt = new DateTimeImmutable($graceUntilRaw);
if ((new DateTimeImmutable()) < $graceUntilDt) {
$graceBadgeHtml = '<span class="task-cert-grace-badge">Karenz bis ' . TaskCertView::e($graceUntilDt->format('d.m.Y')) . '</span>';
}
} catch (Throwable $e) {
// ignore
}
}
$todoActionHtml = '';
if ($assignmentId > 0) {
$todoActionHtml = '<button type="button" class="btn task-cert-btn-success task-cert-btn-sm task-cert-btn-mark-completed" data-assignment-id="' . $assignmentId . '">Erledigt markieren</button>';
}
$mergedRows[] = [
'contact_name' => (string) ($row['contact_name'] ?? '-'),
'contact_email' => (string) ($row['contact_email'] ?? ''),
'_row_group' => 'todo',
'_row_group_label' => 'Offen',
'status' => (string) ($row['status'] ?? ''),
'status_label' => '',
'assigned_at_sort' => (string) ($row['created_at'] ?? ''),
'due_at_sort' => (string) ($row['due_at'] ?? ''),
'completed_at_sort' => null,
'overdue_at_sort' => (string) ($row['overdue_at'] ?? ''),
'escalation_level' => (int) ($row['escalation_level'] ?? 0),
'grace_badge_html' => $graceBadgeHtml,
'last_activity_sort' => (string) ($row['last_submitted_at'] ?? ''),
'certificate_label' => null,
'certificate_code' => null,
'valid_until_sort' => null,
'cycle_key' => (string) ($row['cycle_key'] ?? ''),
'_row_html_action' => $todoActionHtml,
];
}
foreach ($completedRows as $row) {
$completedAssignmentId = (int) ($row['id'] ?? 0);
if ($completedAssignmentId > 0 && isset($seenAssignmentIds[$completedAssignmentId])) {
continue;
}
if ($completedAssignmentId > 0) {
$seenAssignmentIds[$completedAssignmentId] = true;
}
$certStatus = (string) ($row['certificate_status'] ?? '');
$actionHtml = '';
if ($completedAssignmentId > 0) {
$actionHtml = '<button type="button" class="btn task-cert-btn-danger task-cert-btn-sm task-cert-btn-reset-assignment" data-assignment-id="' . $completedAssignmentId . '">Zurücksetzen</button>';
}
$mergedRows[] = [
'contact_name' => (string) ($row['contact_name'] ?? '-'),
'contact_email' => (string) ($row['contact_email'] ?? ''),
'_row_group' => 'completed',
'_row_group_label' => 'Erledigt',
'status' => 'completed',
'status_label' => '',
'assigned_at_sort' => null,
'due_at_sort' => null,
'completed_at_sort' => (string) ($row['completed_at'] ?? ''),
'overdue_at_sort' => null,
'escalation_level' => null,
'grace_badge_html' => '',
'last_activity_sort' => null,
'certificate_label' => '',
'certificate_code' => (string) ($row['certificate_code'] ?? ''),
'valid_until_sort' => (string) ($row['certificate_valid_until'] ?? ''),
'cycle_key' => (string) ($row['cycle_key'] ?? ''),
'_row_html_action' => $actionHtml,
];
// Zertifikat-Label setzen
$lastIdx = count($mergedRows) - 1;
if ((int) ($row['certificate_id'] ?? 0) > 0) {
$mergedRows[$lastIdx]['certificate_label'] = $certStatus;
}
}
foreach ($overdueRows as $row) {
$assignmentId = (int) ($row['id'] ?? 0);
if ($assignmentId > 0 && isset($seenAssignmentIds[$assignmentId])) {
continue; // Bereits in Todo- oder Completed-Zeilen enthalten
}
if ($assignmentId > 0) {
$seenAssignmentIds[$assignmentId] = true;
}
$overdueActionHtml = '';
if ($assignmentId > 0) {
$overdueActionHtml = '<button type="button" class="btn task-cert-btn-success task-cert-btn-sm task-cert-btn-mark-completed" data-assignment-id="' . $assignmentId . '">Erledigt markieren</button>';
}
$mergedRows[] = [
'contact_name' => (string) ($row['contact_name'] ?? '-'),
'contact_email' => (string) ($row['contact_email'] ?? ''),
'_row_group' => 'overdue',
'_row_group_label' => 'Überfällig',
'status' => (string) ($row['status'] ?? ''),
'status_label' => '',
'assigned_at_sort' => null,
'due_at_sort' => (string) ($row['due_at'] ?? ''),
'completed_at_sort' => null,
'overdue_at_sort' => (string) ($row['overdue_at'] ?? ''),
'escalation_level' => (int) ($row['escalation_level'] ?? 0),
'grace_badge_html' => '',
'last_activity_sort' => (string) ($row['last_submitted_at'] ?? ''),
'certificate_label' => null,
'certificate_code' => null,
'valid_until_sort' => null,
'cycle_key' => '',
'_row_html_action' => $overdueActionHtml,
];
}
$statusLabelMap = TaskCertConstants::assignmentStatusLabels();
$methodLabelMap = TaskCertConstants::methodLabels();
$submissionStatusLabelMap = TaskCertConstants::submissionStatusLabels();
$certificateStatusLabelMap = TaskCertConstants::certificateStatusLabels();
// Status- und Zertifikat-Labels in die mergedRows einfügen
foreach ($mergedRows as &$mergedRow) {
$s = $mergedRow['status'] ?? '';
if ($mergedRow['status_label'] === '' && $s !== '') {
$mergedRow['status_label'] = $statusLabelMap[$s] ?? $s;
}
$cl = $mergedRow['certificate_label'] ?? null;
if ($cl !== null && $cl !== '' && isset($certificateStatusLabelMap[$cl])) {
$mergedRow['certificate_label'] = $certificateStatusLabelMap[$cl];
}
}
unset($mergedRow);
$mergedRowsJson = json_encode($mergedRows, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
// Antworten-Zeilen für Grid.js aufbereiten
$downloadFileUrl = task_cert_base_url() . '/endpoints/admin/download_submission_file.php';
$answersGridRows = [];
foreach ($submissionRows as $subRow) {
$subStatus = (string) ($subRow['status'] ?? '');
$methodType = (string) ($subRow['method_type'] ?? '');
$answerItems = is_array($subRow['answer_items'] ?? null) ? $subRow['answer_items'] : [];
$detailHtml = '';
if ($answerItems !== []) {
$detailHtml = '<div class="task-cert-monitoring-answer-items">';
foreach ($answerItems as $item) {
$detailHtml .= '<div><strong>' . TaskCertView::e((string) ($item['label'] ?? '')) . ':</strong> ';
$itemFiles = is_array($item['files'] ?? null) ? $item['files'] : [];
if ($itemFiles !== []) {
$links = [];
foreach ($itemFiles as $file) {
$url = $downloadFileUrl . '?' . http_build_query([
'submission_id' => (int) ($file['submission_id'] ?? 0),
'field_key' => (string) ($file['field_key'] ?? ''),
'index' => (int) ($file['index'] ?? 0),
'csrf_token' => $csrfToken,
]);
$links[] = '<a href="' . TaskCertView::e($url) . '" target="_blank" rel="noopener">'
. TaskCertView::e((string) ($file['name'] ?? 'Datei')) . '</a>';
}
$detailHtml .= implode(', ', $links);
} else {
$detailHtml .= TaskCertView::e((string) ($item['value'] ?? ''));
}
$detailHtml .= '</div>';
}
$detailHtml .= '</div>';
}
$score = null;
if ($subRow['score'] !== null && $subRow['max_score'] !== null) {
$score = (int) $subRow['score'] . '/' . (int) $subRow['max_score'];
}
// answer_items als Key-Value-Map für CSV-Export
$answerMap = [];
foreach ($answerItems as $item) {
$label = (string) ($item['label'] ?? '');
if ($label !== '') {
$answerMap[$label] = (string) ($item['value'] ?? '');
}
}
$answersGridRows[] = [
'contact_name' => (string) ($subRow['contact_name'] ?? '-'),
'contact_email' => (string) ($subRow['contact_email'] ?? ''),
'method_label' => (string) ($subRow['method_label'] ?? ($methodLabelMap[$methodType] ?? $methodType)),
'status_label' => $submissionStatusLabelMap[$subStatus] ?? $subStatus,
'submitted_at_sort' => (string) ($subRow['submitted_at'] ?? ''),
'score' => $score,
'score_num' => $subRow['score'] !== null ? (int) $subRow['score'] : null,
'detail_html' => $detailHtml,
'answer_fields' => $answerMap,
];
}
$answersGridRowsJson = json_encode($answersGridRows, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
$formatDateTime = static function ($value): string {
$value = trim((string) $value);
if ($value === '') {
return '-';
}
try {
return (new DateTimeImmutable($value))->format('d.m.Y H:i');
} catch (Throwable $throwable) {
return $value;
}
};
$hasCycle = (bool) ($dashboard['has_cycle'] ?? ($selectedCycleKey !== ''));
$noCycleReason = trim((string) ($dashboard['no_cycle_reason'] ?? ''));
if ($noCycleReason === '') {
$noCycleReason = 'Aktuell ist kein Zyklus verfügbar.';
}
$exportAnswersUrl = '';
if ($taskId > 0 && $canExportAnswers) {
$exportQuery = [
'id' => $taskId,
'mode' => $exportMode,
'csrf_token' => $csrfToken,
];
if ($exportMode === 'cycle' && $selectedCycleKey !== '') {
$exportQuery['cycle_key'] = $selectedCycleKey;
}
$exportAnswersUrl = task_cert_base_url() . '/endpoints/admin/export_answers_csv.php?' . http_build_query($exportQuery);
}
?>
<div
class="task-cert task-cert-admin-detail task-cert-admin-monitoring"
data-task-cert-tabs
data-tabs-active="<?php echo TaskCertView::e($activeTabIndex); ?>"
data-csrf-token="<?php echo TaskCertView::e($csrfToken); ?>"
data-reset-assignment-url="<?php echo TaskCertView::e($resetAssignmentUrl); ?>"
data-reset-cycle-url="<?php echo TaskCertView::e($resetCycleUrl); ?>"
data-approval-url="<?php echo TaskCertView::e($approvalDecisionUrl); ?>"
data-task-id="<?php echo (int) $taskId; ?>"
>
<div id="task-cert-toast-container" class="task-cert-toast-container" aria-live="polite"></div>
<div class="task-cert-form-header">
<div class="task-cert-title-wrap">
<a
href="<?php echo TaskCertView::e($taskListUrl); ?>"
class="task-cert-back-link"
aria-label="Zur Aufgabenliste"
title="Zur Aufgabenliste"
>&larr;</a>
<h2><?php echo TaskCertView::e($taskTitle); ?></h2>
<?php
$frequencyType = (string) ($task['frequency_type'] ?? '');
$recurrenceBasis = (string) ($task['recurrence_basis'] ?? '');
$recurrenceUnit = (string) ($task['recurrence_unit'] ?? '');
$isCompletionBased = $frequencyType === 'recurring' && $recurrenceBasis === 'completion';
$isCalendarBased = $frequencyType === 'recurring' && $recurrenceBasis !== 'completion';
$isOneTime = $frequencyType !== 'recurring';
$unitLabels = ['day' => 'Täglich', 'week' => 'Wöchentlich', 'month' => 'Monatlich', 'year' => 'Jährlich'];
?>
<?php if ($isCompletionBased): ?>
<span class="task-cert-recurrence-badge is-completion"><?php echo TaskCertView::e($unitLabels[$recurrenceUnit] ?? 'Wiederkehrend'); ?> (erledigungsbasiert)</span>
<?php elseif ($isCalendarBased): ?>
<span class="task-cert-recurrence-badge is-calendar"><?php echo TaskCertView::e($unitLabels[$recurrenceUnit] ?? 'Wiederkehrend'); ?> (kalenderbasiert)</span>
<?php else: ?>
<span class="task-cert-recurrence-badge is-onetime">Einmalig</span>
<?php endif; ?>
</div>
<div class="task-cert-toolbar-right">
<?php if ($showCycleSelector && $cycleOptions !== []): ?>
<form action="<?php echo TaskCertView::e($taskDetailUrl); ?>" method="get" class="task-cert-cycle-switch-inline" data-cycle-switch-form>
<input type="hidden" name="id" value="<?php echo (int) $taskId; ?>">
<input type="hidden" name="tab" value="<?php echo TaskCertView::e($activeTabKey); ?>" data-tabs-active-input>
<select name="cycle_key" onchange="this.form.submit()">
<?php foreach ($cycleOptions as $option): ?>
<?php $cycleKey = (string) ($option['cycle_key'] ?? ''); ?>
<?php if ($cycleKey === '') { continue; } ?>
<option value="<?php echo TaskCertView::e($cycleKey); ?>" <?php echo $cycleKey === $selectedCycleKey ? 'selected' : ''; ?>>
<?php echo TaskCertView::e((string) ($option['label'] ?? $cycleKey)); ?>
</option>
<?php endforeach; ?>
</select>
</form>
<?php endif; ?>
<a href="<?php echo TaskCertView::e($taskListUrl); ?>" class="btn">Zur Liste</a>
<?php if ($taskId > 0): ?>
<a href="<?php echo TaskCertView::e($taskEditUrl); ?>" class="btn">Bearbeiten</a>
<?php endif; ?>
</div>
</div>
<?php if ($error): ?>
<div class="task-cert-error"><?php echo TaskCertView::e($error); ?></div>
<?php elseif ($task): ?>
<?php if ($cycleSelectionWarning !== ''): ?>
<div class="task-cert-warning"><?php echo TaskCertView::e($cycleSelectionWarning); ?></div>
<?php endif; ?>
<?php if ($hasCycle): ?>
<div class="task-cert-wizard-tabs" role="tablist" aria-label="Reiter-Navigation">
<button type="button" class="task-cert-wizard-tab" data-tabs-tab="0" data-tab-key="overview" role="tab">Übersicht</button>
<button type="button" class="task-cert-wizard-tab" data-tabs-tab="1" data-tab-key="approvals" role="tab">Freigaben</button>
<button type="button" class="task-cert-wizard-tab" data-tabs-tab="2" data-tab-key="answers" role="tab">Antworten</button>
</div>
<!-- Panel 0: Übersicht (KPIs + zusammengeführte Tabelle) -->
<section class="task-cert-wizard-step" data-tabs-panel="0" hidden>
<?php
$totalAssigned = (int) ($kpis['total_assigned'] ?? 0);
$completedCount = (int) ($kpis['completed_count'] ?? 0);
$completionRate = $totalAssigned > 0 ? round(($completedCount / $totalAssigned) * 100, 1) : 0;
$overdueCount = (int) ($kpis['overdue_count'] ?? 0);
$approvalCount = (int) ($kpis['awaiting_approval_count'] ?? 0);
$escalatedCount = (int) ($kpis['escalated_count'] ?? 0);
?>
<div class="task-cert-kpi-tiles">
<div class="task-cert-kpi-tile">
<span class="task-cert-kpi-value"><?php echo $completedCount; ?> / <?php echo $totalAssigned; ?></span>
<span class="task-cert-kpi-label">Abgeschlossen (<?php echo TaskCertView::e((string) $completionRate); ?>%)</span>
</div>
<div class="task-cert-kpi-tile">
<span class="task-cert-kpi-value"><?php echo $overdueCount; ?></span>
<span class="task-cert-kpi-label">Überfällig</span>
</div>
<div class="task-cert-kpi-tile">
<span class="task-cert-kpi-value"><?php echo $approvalCount; ?></span>
<span class="task-cert-kpi-label">Freigaben offen</span>
</div>
<div class="task-cert-kpi-tile">
<span class="task-cert-kpi-value"><?php echo $escalatedCount; ?></span>
<span class="task-cert-kpi-label">Eskaliert</span>
</div>
</div>
<div class="task-cert-overview-grid-toolbar">
<input type="text" id="task-cert-overview-search" placeholder="Suchen..." autocomplete="off">
<select id="task-cert-overview-status-filter">
<option value="">Alle Status</option>
<?php foreach ($statusLabelMap as $statusKey => $statusLabel): ?>
<option value="<?php echo TaskCertView::e($statusKey); ?>"><?php echo TaskCertView::e($statusLabel); ?></option>
<?php endforeach; ?>
</select>
<button type="button" id="task-cert-overview-export" class="btn">CSV Export</button>
<?php if ($completedRows !== []): ?>
<button
type="button"
class="btn task-cert-btn-danger task-cert-btn-reset-cycle"
data-task-id="<?php echo (int) $taskId; ?>"
>Alle im Zyklus zurücksetzen</button>
<?php endif; ?>
</div>
<div
id="task-cert-overview-grid"
class="task-cert-table-wrap"
data-initial-status-filter="<?php echo TaskCertView::e($initialStatusFilter); ?>"
></div>
<script type="application/json" id="task-cert-overview-grid-data"><?php echo $mergedRowsJson; ?></script>
</section>
<!-- Panel 1: Freigaben (unverändert) -->
<section class="task-cert-wizard-step" data-tabs-panel="1" hidden>
<div class="task-cert-block">
<h3>Freigaben</h3>
<div class="task-cert-table-wrap">
<table class="task-cert-table">
<thead>
<tr>
<th>Mitarbeiter</th>
<th>Methode</th>
<th>Einreichungsstatus</th>
<th>Eingereicht am</th>
<th>Zuweisungsstatus</th>
<th>Fällig am</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($approvalRows as $row): ?>
<?php
$submissionStatus = (string) ($row['status'] ?? '');
$assignmentStatus = (string) ($row['assignment_status'] ?? '');
?>
<tr>
<td>
<?php echo TaskCertView::e((string) ($row['contact_name'] ?? '-')); ?><br>
<small><?php echo TaskCertView::e((string) ($row['contact_email'] ?? '')); ?></small>
</td>
<?php $methodType = (string) ($row['method_type'] ?? ''); ?>
<td><?php echo TaskCertView::e($methodLabelMap[$methodType] ?? $methodType); ?></td>
<td><?php echo TaskCertView::e($submissionStatusLabelMap[$submissionStatus] ?? $submissionStatus); ?></td>
<td><?php echo TaskCertView::e($formatDateTime($row['submitted_at'] ?? '')); ?></td>
<td><span class="task-cert-status-badge status-<?php echo TaskCertView::e($assignmentStatus); ?>"><?php echo TaskCertView::e($statusLabelMap[$assignmentStatus] ?? $assignmentStatus); ?></span></td>
<td><?php echo TaskCertView::e($formatDateTime($row['due_at'] ?? '')); ?></td>
<td>
<?php if ((int) ($row['id'] ?? 0) > 0): ?>
<div
class="task-cert-inline-approval-form"
data-submission-id="<?php echo (int) ($row['id'] ?? 0); ?>"
>
<textarea class="task-cert-approval-note" rows="2" placeholder="Notiz (optional)"></textarea>
<div class="task-cert-inline-approval-actions">
<button type="button" class="btn task-cert-btn-success task-cert-btn-approval" data-decision="approve">Freigeben</button>
<button type="button" class="btn task-cert-btn-danger task-cert-btn-approval" data-decision="reject">Ablehnen</button>
</div>
</div>
<?php else: ?>
-
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ($approvalRows === []): ?>
<tr>
<td colspan="7">Keine Freigaben im aktuellen Filter.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</section>
<!-- Panel 2: Antworten -->
<section class="task-cert-wizard-step" data-tabs-panel="2" hidden>
<div class="task-cert-overview-grid-toolbar">
<input type="text" id="task-cert-answers-search" placeholder="Suchen..." autocomplete="off">
<button type="button" id="task-cert-answers-export" class="btn">CSV Export</button>
</div>
<div id="task-cert-answers-grid" class="task-cert-table-wrap"></div>
<script type="application/json" id="task-cert-answers-grid-data"><?php echo $answersGridRowsJson; ?></script>
</section>
<?php else: ?>
<div class="task-cert-warning"><?php echo TaskCertView::e($noCycleReason); ?></div>
<?php endif; ?>
<?php endif; ?>
<dialog id="task-cert-mark-completed-dialog" class="task-cert-dialog"></dialog>
<div
id="task-cert-mark-completed-config"
hidden
data-methods-url="<?php echo TaskCertView::e($markCompletedMethodsUrl); ?>"
data-submit-url="<?php echo TaskCertView::e($markCompletedUrl); ?>"
data-task-id="<?php echo $taskId; ?>"
></div>
</div>