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>
This commit is contained in:
68
module/tasks/endpoints/admin/approval_decision.php
Normal file
68
module/tasks/endpoints/admin/approval_decision.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Methode nicht erlaubt'], 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Keine Berechtigung'], 403);
|
||||
}
|
||||
|
||||
$contentType = trim((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
|
||||
$isJson = strpos($contentType, 'application/json') !== false;
|
||||
|
||||
if ($isJson) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$body = json_decode($rawBody ?: '{}', true);
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$csrfToken = trim((string) ($body['csrf_token'] ?? ''));
|
||||
if ($csrfToken !== '') {
|
||||
$_POST['csrf_token'] = $csrfToken;
|
||||
}
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$submissionId = (int) ($body['submission_id'] ?? 0);
|
||||
$decision = trim((string) ($body['decision'] ?? ''));
|
||||
$note = trim((string) ($body['note'] ?? ''));
|
||||
} else {
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
$submissionId = TaskCertRequest::postInt('submission_id', 0);
|
||||
$decision = TaskCertRequest::postString('decision', '');
|
||||
$note = TaskCertRequest::postString('note', '');
|
||||
}
|
||||
|
||||
if ($submissionId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige submission_id'], 400);
|
||||
}
|
||||
|
||||
if (!in_array($decision, ['approve', 'reject'], true)) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige decision'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
|
||||
if ($decision === 'approve') {
|
||||
$services['approvalService']->approveSubmission($submissionId, $auth->contactId(), $note);
|
||||
} else {
|
||||
$services['approvalService']->rejectSubmission($submissionId, $auth->contactId(), $note);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'submission_id' => $submissionId,
|
||||
'decision' => $decision,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
47
module/tasks/endpoints/admin/contact_search.php
Normal file
47
module/tasks/endpoints/admin/contact_search.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
$term = TaskCertRequest::queryString('term', '');
|
||||
$limit = TaskCertRequest::queryInt('limit', 15);
|
||||
|
||||
$services = task_cert_services();
|
||||
$lookupRepo = $services['lookupRepo'] ?? null;
|
||||
|
||||
if (!$lookupRepo instanceof TaskLookupRepository) {
|
||||
TaskCertResponse::json(['results' => []]);
|
||||
}
|
||||
|
||||
$contacts = $lookupRepo->searchContacts($term, $limit);
|
||||
|
||||
$results = [];
|
||||
foreach ($contacts as $c) {
|
||||
$id = (int) ($c['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) ($c['name'] ?? ''));
|
||||
$email = trim((string) ($c['email'] ?? ''));
|
||||
|
||||
$label = $name;
|
||||
if ($label === '') {
|
||||
$label = $email !== '' ? $email : 'Kontakt #' . $id;
|
||||
} elseif ($email !== '') {
|
||||
$label .= ' (' . $email . ')';
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
|
||||
TaskCertResponse::json(['results' => $results]);
|
||||
99
module/tasks/endpoints/admin/download_submission_file.php
Normal file
99
module/tasks/endpoints/admin/download_submission_file.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (TaskCertRequest::method() !== 'GET') {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405, false);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->isLoggedIn()) {
|
||||
TaskCertResponse::error('Nicht eingeloggt', 401, false);
|
||||
}
|
||||
|
||||
$csrfToken = TaskCertRequest::queryString('csrf_token', '');
|
||||
if (!TaskCertCsrf::validate($csrfToken)) {
|
||||
TaskCertResponse::error('CSRF validation failed', 419, false);
|
||||
}
|
||||
|
||||
$submissionId = TaskCertRequest::queryInt('submission_id', 0);
|
||||
$fieldKey = TaskCertRequest::queryString('field_key', '');
|
||||
$index = TaskCertRequest::queryInt('index', -1);
|
||||
|
||||
if ($submissionId <= 0 || $fieldKey === '' || $index < 0) {
|
||||
TaskCertResponse::error('Ungültige Parameter.', 400, false);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$submission = $services['submissionRepo']->findById($submissionId);
|
||||
if ($submission === null) {
|
||||
TaskCertResponse::error('Einreichung nicht gefunden.', 404, false);
|
||||
}
|
||||
|
||||
// Zugriff: Admin ODER Eigentümer der Einreichung.
|
||||
$isAdmin = $auth->hasPermission('r-task-cert-admin');
|
||||
$isOwner = (int) ($submission['main_contact_id'] ?? 0) === $auth->contactId();
|
||||
if (!$isAdmin && !$isOwner) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403, false);
|
||||
}
|
||||
|
||||
$payload = is_array($submission['payload'] ?? null) ? $submission['payload'] : [];
|
||||
$formValues = is_array($payload['form_values'] ?? null) ? $payload['form_values'] : [];
|
||||
$fieldFiles = is_array($formValues[$fieldKey] ?? null) ? $formValues[$fieldKey] : [];
|
||||
$fileMeta = is_array($fieldFiles[$index] ?? null) ? $fieldFiles[$index] : null;
|
||||
|
||||
if ($fileMeta === null || !isset($fileMeta['file_path'])) {
|
||||
TaskCertResponse::error('Datei nicht gefunden.', 404, false);
|
||||
}
|
||||
|
||||
$absolutePath = TaskUploadStorage::absolutePath((string) $fileMeta['file_path']);
|
||||
if ($absolutePath === null || !is_file($absolutePath)) {
|
||||
TaskCertResponse::error('Datei nicht gefunden.', 404, false);
|
||||
}
|
||||
|
||||
$fileName = (string) ($fileMeta['file_name'] ?? 'datei');
|
||||
|
||||
// Content-Type IMMER aus der (erlaubten) Endung ableiten — nie aus dem
|
||||
// gespeicherten/Client-MIME (schützt auch Alt-Datensätze vor Stored-XSS,
|
||||
// z. B. als Bild getarntes SVG).
|
||||
$mime = TaskUploadPolicy::mimeForFilename($fileName);
|
||||
|
||||
// RFC-5987: ASCII-Fallback für alte Clients + UTF-8-URL-encoded für moderne.
|
||||
$asciiFallback = preg_replace('/[^\x20-\x7E]+/', '_', $fileName) ?: 'datei';
|
||||
|
||||
// Inline nur für eine feste, sichere MIME-Menge (kein SVG/HTML/XML),
|
||||
// alles andere als Download.
|
||||
$disposition = TaskUploadPolicy::isInlineSafeMime($mime) ? 'inline' : 'attachment';
|
||||
|
||||
if (!headers_sent()) {
|
||||
http_response_code(200);
|
||||
header('Content-Type: ' . $mime);
|
||||
header(sprintf(
|
||||
'Content-Disposition: %s; filename="%s"; filename*=UTF-8\'\'%s',
|
||||
$disposition,
|
||||
addcslashes($asciiFallback, '"\\'),
|
||||
rawurlencode($fileName)
|
||||
));
|
||||
header('Content-Length: ' . (int) filesize($absolutePath));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
readfile($absolutePath);
|
||||
exit;
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertLogger::error('Submission-Datei-Download fehlgeschlagen', [
|
||||
'submission_id' => $submissionId,
|
||||
'field_key' => $fieldKey,
|
||||
'index' => $index,
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
TaskCertResponse::error('Download fehlgeschlagen.', 500, false);
|
||||
}
|
||||
196
module/tasks/endpoints/admin/export_answers_csv.php
Normal file
196
module/tasks/endpoints/admin/export_answers_csv.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_csv_safe_slug')) {
|
||||
function task_cert_csv_safe_slug(string $value): string
|
||||
{
|
||||
$value = strtolower(trim($value));
|
||||
$value = preg_replace('/[^a-z0-9_-]+/', '-', $value) ?? '';
|
||||
$value = trim($value, '-_');
|
||||
return $value !== '' ? $value : 'export';
|
||||
}
|
||||
}
|
||||
|
||||
if (TaskCertRequest::method() !== 'GET') {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405, false);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403, false);
|
||||
}
|
||||
|
||||
$csrfToken = TaskCertRequest::queryString('csrf_token', '');
|
||||
if (!TaskCertCsrf::validate($csrfToken)) {
|
||||
TaskCertResponse::error('CSRF validation failed', 419, false);
|
||||
}
|
||||
|
||||
$taskId = TaskCertRequest::queryInt('id', 0);
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::error('Ungültige Aufgaben-ID.', 400, false);
|
||||
}
|
||||
|
||||
$queryMode = TaskCertRequest::queryString('mode', '');
|
||||
$queryCycleKey = TaskCertRequest::queryString('cycle_key', '');
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$taskService = $services['taskDefinitionService'] ?? null;
|
||||
$monitoringService = $services['taskMonitoringService'] ?? null;
|
||||
|
||||
if (!$taskService instanceof TaskDefinitionService) {
|
||||
throw new RuntimeException('TaskDefinitionService konnte nicht geladen werden.');
|
||||
}
|
||||
if (!$monitoringService instanceof TaskMonitoringService) {
|
||||
throw new RuntimeException('TaskMonitoringService konnte nicht geladen werden.');
|
||||
}
|
||||
|
||||
$task = $taskService->getTaskDetail($taskId);
|
||||
if ($task === null) {
|
||||
TaskCertResponse::error('Aufgabe nicht gefunden.', 404, false);
|
||||
}
|
||||
|
||||
$export = $monitoringService->buildAnswersExport($task, [
|
||||
'mode' => $queryMode,
|
||||
'cycle_key' => $queryCycleKey,
|
||||
]);
|
||||
|
||||
$mode = trim((string) ($export['mode'] ?? 'cycle'));
|
||||
if (!in_array($mode, ['cycle', 'snapshot'], true)) {
|
||||
$mode = 'cycle';
|
||||
}
|
||||
$selectedCycleKey = trim((string) ($export['cycle_key'] ?? ''));
|
||||
$rows = is_array($export['rows'] ?? null) ? $export['rows'] : [];
|
||||
$taskTitle = trim((string) ($task['title'] ?? 'Aufgabe'));
|
||||
|
||||
$filenameCyclePart = $mode === 'snapshot'
|
||||
? 'snapshot'
|
||||
: ('cycle-' . task_cert_csv_safe_slug($selectedCycleKey !== '' ? $selectedCycleKey : 'none'));
|
||||
$filename = sprintf(
|
||||
'task-answers-%d-%s-%s.csv',
|
||||
$taskId,
|
||||
$filenameCyclePart,
|
||||
date('Ymd_His')
|
||||
);
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
if (!is_resource($output)) {
|
||||
throw new RuntimeException('CSV-Ausgabe konnte nicht initialisiert werden.');
|
||||
}
|
||||
|
||||
// UTF-8 BOM for Excel on German locale systems.
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
|
||||
$header = [
|
||||
'task_id',
|
||||
'task_title',
|
||||
'export_mode',
|
||||
'selected_cycle_key',
|
||||
'row_cycle_key',
|
||||
'submission_id',
|
||||
'assignment_id',
|
||||
'contact_id',
|
||||
'contact_name',
|
||||
'contact_email',
|
||||
'method_type',
|
||||
'method_label',
|
||||
'submission_status',
|
||||
'assignment_status',
|
||||
'submitted_at',
|
||||
'due_at',
|
||||
'overdue_at',
|
||||
'completed_at',
|
||||
'escalation_level',
|
||||
'score',
|
||||
'max_score',
|
||||
'review_note',
|
||||
'answer_summary',
|
||||
'answer_detail_json',
|
||||
];
|
||||
fputcsv($output, $header, ';');
|
||||
|
||||
$toCsvCell = static function ($value): string {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($value === false) {
|
||||
$value = '';
|
||||
}
|
||||
}
|
||||
|
||||
$stringValue = (string) $value;
|
||||
$trimmed = ltrim($stringValue);
|
||||
if ($trimmed !== '') {
|
||||
$firstChar = substr($trimmed, 0, 1);
|
||||
if (in_array($firstChar, ['=', '+', '-', '@'], true)) {
|
||||
$stringValue = "'" . $stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $stringValue;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$row = is_array($row) ? $row : [];
|
||||
$line = [
|
||||
$taskId,
|
||||
$taskTitle,
|
||||
$mode,
|
||||
$selectedCycleKey,
|
||||
(string) ($row['cycle_key'] ?? ''),
|
||||
(int) ($row['submission_id'] ?? 0),
|
||||
(int) ($row['assignment_id'] ?? 0),
|
||||
(int) ($row['contact_id'] ?? 0),
|
||||
(string) ($row['contact_name'] ?? ''),
|
||||
(string) ($row['contact_email'] ?? ''),
|
||||
(string) ($row['method_type'] ?? ''),
|
||||
(string) ($row['method_label'] ?? ''),
|
||||
(string) ($row['submission_status'] ?? ''),
|
||||
(string) ($row['assignment_status'] ?? ''),
|
||||
(string) ($row['submitted_at'] ?? ''),
|
||||
(string) ($row['due_at'] ?? ''),
|
||||
(string) ($row['overdue_at'] ?? ''),
|
||||
(string) ($row['completed_at'] ?? ''),
|
||||
(int) ($row['escalation_level'] ?? 0),
|
||||
$row['score'] !== null ? (int) $row['score'] : '',
|
||||
$row['max_score'] !== null ? (int) $row['max_score'] : '',
|
||||
(string) ($row['review_note'] ?? ''),
|
||||
(string) ($row['answer_summary'] ?? ''),
|
||||
(string) ($row['answer_detail_json'] ?? '{}'),
|
||||
];
|
||||
|
||||
$csvLine = array_map($toCsvCell, $line);
|
||||
fputcsv($output, $csvLine, ';');
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
} catch (Throwable $throwable) {
|
||||
$statusCode = $throwable instanceof InvalidArgumentException ? 400 : 500;
|
||||
TaskCertResponse::error($throwable->getMessage(), $statusCode, false);
|
||||
}
|
||||
98
module/tasks/endpoints/admin/mark_completed.php
Normal file
98
module/tasks/endpoints/admin/mark_completed.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Methode nicht erlaubt'], 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Keine Berechtigung'], 403);
|
||||
}
|
||||
|
||||
$contentType = trim((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
|
||||
$isJson = strpos($contentType, 'application/json') !== false;
|
||||
|
||||
if ($isJson) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$body = json_decode($rawBody ?: '{}', true);
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$csrfToken = trim((string) ($body['csrf_token'] ?? ''));
|
||||
if ($csrfToken !== '') {
|
||||
$_POST['csrf_token'] = $csrfToken;
|
||||
}
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = (int) ($body['task_id'] ?? 0);
|
||||
$assignmentId = (int) ($body['assignment_id'] ?? 0);
|
||||
$adminNote = trim((string) ($body['admin_note'] ?? ''));
|
||||
$methodPayloads = [];
|
||||
$rawMethods = $body['method'] ?? [];
|
||||
if (is_array($rawMethods)) {
|
||||
foreach ($rawMethods as $methodId => $fields) {
|
||||
$methodId = (int) $methodId;
|
||||
if ($methodId <= 0 || !is_array($fields)) {
|
||||
continue;
|
||||
}
|
||||
$methodPayloads[$methodId] = $fields;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
$assignmentId = TaskCertRequest::postInt('assignment_id', 0);
|
||||
$adminNote = TaskCertRequest::postString('admin_note', '');
|
||||
$methodPayloads = [];
|
||||
$rawMethods = $_POST['method'] ?? [];
|
||||
if (is_array($rawMethods)) {
|
||||
foreach ($rawMethods as $methodId => $fields) {
|
||||
$methodId = (int) $methodId;
|
||||
if ($methodId <= 0 || !is_array($fields)) {
|
||||
continue;
|
||||
}
|
||||
$methodPayloads[$methodId] = $fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige task_id'], 400);
|
||||
}
|
||||
|
||||
if ($assignmentId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige assignment_id'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$taskService = $services['taskDefinitionService'] ?? null;
|
||||
if (!$taskService instanceof TaskDefinitionService) {
|
||||
throw new RuntimeException('TaskDefinitionService konnte nicht geladen werden.');
|
||||
}
|
||||
|
||||
$stats = $taskService->adminMarkCompleted(
|
||||
$taskId,
|
||||
$assignmentId,
|
||||
$auth->contactId(),
|
||||
$methodPayloads,
|
||||
$adminNote
|
||||
);
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'assignment_id' => $assignmentId,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
102
module/tasks/endpoints/admin/mark_completed_methods.php
Normal file
102
module/tasks/endpoints/admin/mark_completed_methods.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::json(['error' => 'Keine Berechtigung'], 403);
|
||||
}
|
||||
|
||||
$assignmentId = (int) ($_GET['assignment_id'] ?? 0);
|
||||
if ($assignmentId <= 0) {
|
||||
TaskCertResponse::json(['error' => 'Ungültige assignment_id'], 400);
|
||||
}
|
||||
|
||||
$services = task_cert_services();
|
||||
$assignment = $services['assignmentRepo']->findById($assignmentId);
|
||||
if ($assignment === null) {
|
||||
TaskCertResponse::json(['error' => 'Zuweisung nicht gefunden'], 404);
|
||||
}
|
||||
|
||||
$taskId = (int) ($assignment['task_id'] ?? 0);
|
||||
$task = $services['definitionRepo']->findById($taskId);
|
||||
if ($task === null) {
|
||||
TaskCertResponse::json(['error' => 'Aufgabe nicht gefunden'], 404);
|
||||
}
|
||||
|
||||
$contactId = (int) ($assignment['main_contact_id'] ?? 0);
|
||||
$contact = null;
|
||||
if ($contactId > 0) {
|
||||
$contact = TaskCertDb::fetchOne(
|
||||
'SELECT id, name, email FROM main_contact WHERE id = ?',
|
||||
'i',
|
||||
[$contactId]
|
||||
);
|
||||
}
|
||||
|
||||
$methods = array_values(array_filter(
|
||||
$services['ruleRepo']->getMethodsByTask($taskId),
|
||||
static fn(array $m): bool => (int) ($m['active'] ?? 1) === 1
|
||||
));
|
||||
|
||||
$existingSubmissions = $services['submissionRepo']->listByAssignment($assignmentId);
|
||||
$fulfilledMethodIds = [];
|
||||
foreach ($existingSubmissions as $sub) {
|
||||
$subStatus = (string) ($sub['status'] ?? '');
|
||||
if (in_array($subStatus, ['auto_passed', 'approved'], true)) {
|
||||
$fulfilledMethodIds[(int) ($sub['method_id'] ?? 0)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$methodsOut = [];
|
||||
foreach ($methods as $method) {
|
||||
$methodId = (int) ($method['id'] ?? 0);
|
||||
$methodType = (string) ($method['method_type'] ?? '');
|
||||
|
||||
$formFields = [];
|
||||
if ($methodType === 'form_submit') {
|
||||
foreach ((array) ($method['form_fields'] ?? []) as $field) {
|
||||
$options = [];
|
||||
$rawOptions = is_array($field['options'] ?? null) ? $field['options'] : [];
|
||||
foreach ($rawOptions as $opt) {
|
||||
$options[] = [
|
||||
'value' => (string) ($opt['value'] ?? ''),
|
||||
'label' => (string) ($opt['label'] ?? $opt['value'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$formFields[] = [
|
||||
'field_key' => (string) ($field['field_key'] ?? ''),
|
||||
'label' => (string) ($field['label'] ?? ''),
|
||||
'field_type' => (string) ($field['field_type'] ?? 'text'),
|
||||
'is_required' => (int) ($field['is_required'] ?? 0),
|
||||
'options' => $options,
|
||||
'placeholder' => (string) ($field['placeholder'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$methodsOut[] = [
|
||||
'id' => $methodId,
|
||||
'method_type' => $methodType,
|
||||
'is_required' => (int) ($method['is_required'] ?? 1),
|
||||
'already_fulfilled' => isset($fulfilledMethodIds[$methodId]),
|
||||
'knowledge_post_count' => count((array) ($method['knowledge_posts'] ?? [])),
|
||||
'quiz_question_count' => count((array) ($method['quiz_questions'] ?? [])),
|
||||
'form_fields' => $formFields,
|
||||
];
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'task_id' => $taskId,
|
||||
'task_title' => (string) ($task['title'] ?? ''),
|
||||
'assignment_id' => $assignmentId,
|
||||
'contact_name' => (string) ($contact['name'] ?? '-'),
|
||||
'contact_email' => (string) ($contact['email'] ?? ''),
|
||||
'cycle_key' => (string) ($assignment['cycle_key'] ?? ''),
|
||||
'status' => (string) ($assignment['status'] ?? ''),
|
||||
'due_at' => (string) ($assignment['due_at'] ?? ''),
|
||||
'methods' => $methodsOut,
|
||||
'csrf_token' => TaskCertCsrf::token(),
|
||||
]);
|
||||
36
module/tasks/endpoints/admin/materialize_assignments.php
Normal file
36
module/tasks/endpoints/admin/materialize_assignments.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
$isCli = PHP_SAPI === 'cli';
|
||||
|
||||
if (!$isCli && !TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
if (!$isCli) {
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$count = $services['assignmentResolverService']->materializeAssignmentsForAllActiveTasks();
|
||||
|
||||
CronHeartbeat::record('materialize_assignments', ['materialized' => $count]);
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'materialized' => $count,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
63
module/tasks/endpoints/admin/recalculate_escalations.php
Normal file
63
module/tasks/endpoints/admin/recalculate_escalations.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
$isCli = PHP_SAPI === 'cli';
|
||||
|
||||
if (!$isCli && !TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
if (!$isCli) {
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$apply = true;
|
||||
$taskId = null;
|
||||
|
||||
if ($isCli) {
|
||||
$options = getopt('', ['task_id::', 'apply', 'dry-run']);
|
||||
if ($options !== false) {
|
||||
if (isset($options['task_id']) && !is_array($options['task_id'])) {
|
||||
$parsedTaskId = (int) $options['task_id'];
|
||||
if ($parsedTaskId > 0) {
|
||||
$taskId = $parsedTaskId;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['dry-run'])) {
|
||||
$apply = false;
|
||||
}
|
||||
|
||||
if (isset($options['apply'])) {
|
||||
$apply = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$summary = $services['escalationService']->recalculateEscalationsDetailed($apply, $taskId);
|
||||
$changed = (int) ($summary['changed_assignments'] ?? 0);
|
||||
|
||||
if ($apply) {
|
||||
CronHeartbeat::record('recalculate_escalations', ['changed' => $changed]);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'changed' => $changed,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
67
module/tasks/endpoints/admin/reset_assignment_progress.php
Normal file
67
module/tasks/endpoints/admin/reset_assignment_progress.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Methode nicht erlaubt'], 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Keine Berechtigung'], 403);
|
||||
}
|
||||
|
||||
$contentType = trim((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
|
||||
$isJson = strpos($contentType, 'application/json') !== false;
|
||||
|
||||
if ($isJson) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$body = json_decode($rawBody ?: '{}', true);
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$csrfToken = trim((string) ($body['csrf_token'] ?? ''));
|
||||
if ($csrfToken !== '') {
|
||||
$_POST['csrf_token'] = $csrfToken;
|
||||
}
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = (int) ($body['task_id'] ?? 0);
|
||||
$assignmentId = (int) ($body['assignment_id'] ?? 0);
|
||||
} else {
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
$assignmentId = TaskCertRequest::postInt('assignment_id', 0);
|
||||
}
|
||||
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige task_id'], 400);
|
||||
}
|
||||
|
||||
if ($assignmentId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige assignment_id'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$taskService = $services['taskDefinitionService'] ?? null;
|
||||
if (!$taskService instanceof TaskDefinitionService) {
|
||||
throw new RuntimeException('TaskDefinitionService konnte nicht geladen werden.');
|
||||
}
|
||||
|
||||
$stats = $taskService->resetAssignmentProgress($taskId, $assignmentId, $auth->contactId());
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'assignment_id' => $assignmentId,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
60
module/tasks/endpoints/admin/reset_cycle_progress.php
Normal file
60
module/tasks/endpoints/admin/reset_cycle_progress.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Methode nicht erlaubt'], 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Keine Berechtigung'], 403);
|
||||
}
|
||||
|
||||
$contentType = trim((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
|
||||
$isJson = strpos($contentType, 'application/json') !== false;
|
||||
|
||||
if ($isJson) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$body = json_decode($rawBody ?: '{}', true);
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$csrfToken = trim((string) ($body['csrf_token'] ?? ''));
|
||||
if ($csrfToken !== '') {
|
||||
$_POST['csrf_token'] = $csrfToken;
|
||||
}
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = (int) ($body['task_id'] ?? 0);
|
||||
} else {
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
}
|
||||
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::json(['success' => false, 'error' => 'Ungültige task_id'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$taskService = $services['taskDefinitionService'] ?? null;
|
||||
if (!$taskService instanceof TaskDefinitionService) {
|
||||
throw new RuntimeException('TaskDefinitionService konnte nicht geladen werden.');
|
||||
}
|
||||
|
||||
$stats = $taskService->resetCurrentCycleProgress($taskId, $auth->contactId());
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
284
module/tasks/endpoints/admin/sync_assignments.php
Normal file
284
module/tasks/endpoints/admin/sync_assignments.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_sync_assignments_append_query_params')) {
|
||||
function task_cert_sync_assignments_append_query_params(string $url, array $params): string
|
||||
{
|
||||
if ($url === '' || $params === []) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
$queryString = http_build_query($params);
|
||||
if ($queryString === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$separator = strpos($url, '?') === false ? '?' : '&';
|
||||
return $url . $separator . $queryString;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
if ($path !== '' && substr($path, -1) !== '/') {
|
||||
$parts['path'] = $path . '/';
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
$rawQuery = (string) ($parts['query'] ?? '');
|
||||
if ($rawQuery !== '') {
|
||||
parse_str($rawQuery, $existing);
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
if (!is_string($key) || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
unset($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[$key] = $value;
|
||||
}
|
||||
|
||||
$parts['query'] = http_build_query($existing);
|
||||
|
||||
return task_cert_sync_assignments_build_url($parts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_sync_assignments_build_url')) {
|
||||
function task_cert_sync_assignments_build_url(array $parts): string
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if (isset($parts['scheme']) && $parts['scheme'] !== '') {
|
||||
$url .= $parts['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (isset($parts['user']) && $parts['user'] !== '') {
|
||||
$url .= $parts['user'];
|
||||
if (isset($parts['pass']) && $parts['pass'] !== '') {
|
||||
$url .= ':' . $parts['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host']) && $parts['host'] !== '') {
|
||||
$url .= $parts['host'];
|
||||
}
|
||||
|
||||
if (isset($parts['port'])) {
|
||||
$url .= ':' . (int) $parts['port'];
|
||||
}
|
||||
|
||||
$url .= (string) ($parts['path'] ?? '');
|
||||
|
||||
$query = (string) ($parts['query'] ?? '');
|
||||
if ($query !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
$fragment = (string) ($parts['fragment'] ?? '');
|
||||
if ($fragment !== '') {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_sync_assignments_zero_totals')) {
|
||||
function task_cert_sync_assignments_zero_totals(bool $applied): array
|
||||
{
|
||||
return [
|
||||
'task_count' => 0,
|
||||
'target_count' => 0,
|
||||
'existing_count' => 0,
|
||||
'create_count' => 0,
|
||||
'update_count' => 0,
|
||||
'stale_count' => 0,
|
||||
'protected_completed_count' => 0,
|
||||
'protected_certificate_count' => 0,
|
||||
'remove_count' => 0,
|
||||
'remove_with_submissions_count' => 0,
|
||||
'applied' => $applied,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_sync_assignments_sum_totals')) {
|
||||
function task_cert_sync_assignments_sum_totals(array $results, bool $applied): array
|
||||
{
|
||||
$totals = task_cert_sync_assignments_zero_totals($applied);
|
||||
foreach ($results as $result) {
|
||||
if (!is_array($result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totals['task_count']++;
|
||||
foreach ([
|
||||
'target_count',
|
||||
'existing_count',
|
||||
'create_count',
|
||||
'update_count',
|
||||
'stale_count',
|
||||
'protected_completed_count',
|
||||
'protected_certificate_count',
|
||||
'remove_count',
|
||||
'remove_with_submissions_count',
|
||||
] as $key) {
|
||||
$totals[$key] += (int) ($result[$key] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
$isCli = PHP_SAPI === 'cli';
|
||||
|
||||
if (!$isCli && !TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
if (!$isCli) {
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
}
|
||||
|
||||
$taskId = 0;
|
||||
$mode = 'dry_run';
|
||||
$scope = 'current_cycle';
|
||||
$redirectTo = '';
|
||||
$tabIndex = 0;
|
||||
|
||||
if ($isCli) {
|
||||
$options = getopt('', ['task_id::', 'mode::', 'scope::', 'apply', 'dry-run']);
|
||||
if ($options !== false) {
|
||||
if (isset($options['task_id'])) {
|
||||
$taskId = (int) $options['task_id'];
|
||||
}
|
||||
|
||||
if (isset($options['mode']) && !is_array($options['mode'])) {
|
||||
$mode = trim((string) $options['mode']);
|
||||
}
|
||||
|
||||
if (isset($options['scope']) && !is_array($options['scope'])) {
|
||||
$scope = trim((string) $options['scope']);
|
||||
}
|
||||
|
||||
if (isset($options['apply'])) {
|
||||
$mode = 'apply';
|
||||
}
|
||||
|
||||
if (isset($options['dry-run'])) {
|
||||
$mode = 'dry_run';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
$mode = TaskCertRequest::postString('mode', 'dry_run');
|
||||
$scope = TaskCertRequest::postString('scope', 'current_cycle');
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
$tabIndex = TaskCertRequest::postInt('tab', 0);
|
||||
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::error('Ungültige task_id', 400);
|
||||
}
|
||||
}
|
||||
|
||||
if ($tabIndex < 0) {
|
||||
$tabIndex = 0;
|
||||
}
|
||||
|
||||
if (!in_array($mode, ['dry_run', 'apply'], true)) {
|
||||
TaskCertResponse::error('Ungültiger mode', 400);
|
||||
}
|
||||
|
||||
if ($scope !== 'current_cycle') {
|
||||
TaskCertResponse::error('Ungültiger scope', 400);
|
||||
}
|
||||
|
||||
$apply = $mode === 'apply';
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$resolver = $services['assignmentResolverService'] ?? null;
|
||||
if (!$resolver instanceof AssignmentResolverService) {
|
||||
throw new RuntimeException('AssignmentResolverService nicht verfügbar.');
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$singleResult = null;
|
||||
|
||||
if ($taskId > 0) {
|
||||
$singleResult = $resolver->syncAssignmentsForTask($taskId, $apply, $scope);
|
||||
$results[] = $singleResult;
|
||||
} else {
|
||||
$results = $resolver->syncAssignmentsForAllActiveTasks($apply, $scope);
|
||||
}
|
||||
|
||||
$totals = task_cert_sync_assignments_sum_totals($results, $apply);
|
||||
|
||||
if ($apply && $taskId <= 0) {
|
||||
CronHeartbeat::record('sync_assignments', [
|
||||
'task_count' => (int) ($totals['task_count'] ?? 0),
|
||||
'create_count' => (int) ($totals['create_count'] ?? 0),
|
||||
'remove_count' => (int) ($totals['remove_count'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$isCli && $redirectTo !== '' && $singleResult !== null) {
|
||||
$redirectTo = task_cert_sync_assignments_append_query_params($redirectTo, [
|
||||
'sync_done' => 1,
|
||||
'sync_mode' => $mode,
|
||||
'sync_applied' => $apply ? 1 : 0,
|
||||
'sync_scope' => $scope,
|
||||
'tab' => $tabIndex,
|
||||
'sync_cycle_key' => (string) ($singleResult['cycle_key'] ?? ''),
|
||||
'sync_target_count' => (int) ($singleResult['target_count'] ?? 0),
|
||||
'sync_existing_count' => (int) ($singleResult['existing_count'] ?? 0),
|
||||
'sync_create_count' => (int) ($singleResult['create_count'] ?? 0),
|
||||
'sync_update_count' => (int) ($singleResult['update_count'] ?? 0),
|
||||
'sync_stale_count' => (int) ($singleResult['stale_count'] ?? 0),
|
||||
'sync_protected_completed_count' => (int) ($singleResult['protected_completed_count'] ?? 0),
|
||||
'sync_protected_certificate_count' => (int) ($singleResult['protected_certificate_count'] ?? 0),
|
||||
'sync_remove_count' => (int) ($singleResult['remove_count'] ?? 0),
|
||||
'sync_remove_with_submissions_count' => (int) ($singleResult['remove_with_submissions_count'] ?? 0),
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'mode' => $mode,
|
||||
'scope' => $scope,
|
||||
'task_id' => $taskId > 0 ? $taskId : null,
|
||||
'result' => $singleResult,
|
||||
'results' => $taskId > 0 ? [] : $results,
|
||||
'totals' => $totals,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
if (!$isCli && $redirectTo !== '') {
|
||||
$redirectTo = task_cert_sync_assignments_append_query_params($redirectTo, [
|
||||
'sync_done' => 0,
|
||||
'tab' => $tabIndex,
|
||||
'sync_error' => $throwable->getMessage(),
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
148
module/tasks/endpoints/admin/task_category_delete.php
Normal file
148
module/tasks/endpoints/admin/task_category_delete.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_category_delete_append_query_params')) {
|
||||
function task_cert_category_delete_append_query_params(string $url, array $params): string
|
||||
{
|
||||
if ($url === '' || $params === []) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
$queryString = http_build_query($params);
|
||||
if ($queryString === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$separator = strpos($url, '?') === false ? '?' : '&';
|
||||
return $url . $separator . $queryString;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
if ($path !== '' && substr($path, -1) !== '/') {
|
||||
$parts['path'] = $path . '/';
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
$rawQuery = (string) ($parts['query'] ?? '');
|
||||
if ($rawQuery !== '') {
|
||||
parse_str($rawQuery, $existing);
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
if (!is_string($key) || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
unset($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[$key] = $value;
|
||||
}
|
||||
|
||||
$parts['query'] = http_build_query($existing);
|
||||
return task_cert_category_delete_build_url($parts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_category_delete_build_url')) {
|
||||
function task_cert_category_delete_build_url(array $parts): string
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if (isset($parts['scheme']) && $parts['scheme'] !== '') {
|
||||
$url .= $parts['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (isset($parts['user']) && $parts['user'] !== '') {
|
||||
$url .= $parts['user'];
|
||||
if (isset($parts['pass']) && $parts['pass'] !== '') {
|
||||
$url .= ':' . $parts['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host']) && $parts['host'] !== '') {
|
||||
$url .= $parts['host'];
|
||||
}
|
||||
|
||||
if (isset($parts['port'])) {
|
||||
$url .= ':' . (int) $parts['port'];
|
||||
}
|
||||
|
||||
$url .= (string) ($parts['path'] ?? '');
|
||||
|
||||
$query = (string) ($parts['query'] ?? '');
|
||||
if ($query !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
$fragment = (string) ($parts['fragment'] ?? '');
|
||||
if ($fragment !== '') {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$categoryId = TaskCertRequest::postInt('category_id', 0);
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
|
||||
if ($categoryId <= 0) {
|
||||
TaskCertResponse::error('Ungültige category_id', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$categoryService = $services['taskCategoryService'] ?? null;
|
||||
if (!$categoryService instanceof TaskCategoryService) {
|
||||
throw new RuntimeException('Kategorie-Service nicht verfügbar.');
|
||||
}
|
||||
|
||||
$result = $categoryService->deleteCategory($categoryId, $auth->contactId());
|
||||
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_delete_append_query_params($redirectTo, [
|
||||
'cat_done' => 1,
|
||||
'cat_action' => 'category_delete',
|
||||
'cat_error' => null,
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'action' => 'category_delete',
|
||||
'result' => $result,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_delete_append_query_params($redirectTo, [
|
||||
'cat_done' => 0,
|
||||
'cat_error' => $throwable->getMessage(),
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
154
module/tasks/endpoints/admin/task_category_set_active.php
Normal file
154
module/tasks/endpoints/admin/task_category_set_active.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_category_active_append_query_params')) {
|
||||
function task_cert_category_active_append_query_params(string $url, array $params): string
|
||||
{
|
||||
if ($url === '' || $params === []) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
$queryString = http_build_query($params);
|
||||
if ($queryString === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$separator = strpos($url, '?') === false ? '?' : '&';
|
||||
return $url . $separator . $queryString;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
if ($path !== '' && substr($path, -1) !== '/') {
|
||||
$parts['path'] = $path . '/';
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
$rawQuery = (string) ($parts['query'] ?? '');
|
||||
if ($rawQuery !== '') {
|
||||
parse_str($rawQuery, $existing);
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
if (!is_string($key) || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
unset($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[$key] = $value;
|
||||
}
|
||||
|
||||
$parts['query'] = http_build_query($existing);
|
||||
return task_cert_category_active_build_url($parts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_category_active_build_url')) {
|
||||
function task_cert_category_active_build_url(array $parts): string
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if (isset($parts['scheme']) && $parts['scheme'] !== '') {
|
||||
$url .= $parts['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (isset($parts['user']) && $parts['user'] !== '') {
|
||||
$url .= $parts['user'];
|
||||
if (isset($parts['pass']) && $parts['pass'] !== '') {
|
||||
$url .= ':' . $parts['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host']) && $parts['host'] !== '') {
|
||||
$url .= $parts['host'];
|
||||
}
|
||||
|
||||
if (isset($parts['port'])) {
|
||||
$url .= ':' . (int) $parts['port'];
|
||||
}
|
||||
|
||||
$url .= (string) ($parts['path'] ?? '');
|
||||
|
||||
$query = (string) ($parts['query'] ?? '');
|
||||
if ($query !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
$fragment = (string) ($parts['fragment'] ?? '');
|
||||
if ($fragment !== '') {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$categoryId = TaskCertRequest::postInt('category_id', 0);
|
||||
$active = TaskCertRequest::postInt('active', -1);
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
|
||||
if ($categoryId <= 0) {
|
||||
TaskCertResponse::error('Ungültige category_id', 400);
|
||||
}
|
||||
|
||||
if (!in_array($active, [0, 1], true)) {
|
||||
TaskCertResponse::error('Ungültiger active-Wert', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$categoryService = $services['taskCategoryService'] ?? null;
|
||||
if (!$categoryService instanceof TaskCategoryService) {
|
||||
throw new RuntimeException('Kategorie-Service nicht verfügbar.');
|
||||
}
|
||||
|
||||
$result = $categoryService->setActive($categoryId, $active, $auth->contactId());
|
||||
$action = (string) ($result['action'] ?? ($active === 1 ? 'category_activate' : 'category_deactivate'));
|
||||
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_active_append_query_params($redirectTo, [
|
||||
'cat_done' => 1,
|
||||
'cat_action' => $action,
|
||||
'cat_error' => null,
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'action' => $action,
|
||||
'category' => $result['category'] ?? null,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_active_append_query_params($redirectTo, [
|
||||
'cat_done' => 0,
|
||||
'cat_error' => $throwable->getMessage(),
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
155
module/tasks/endpoints/admin/task_category_upsert.php
Normal file
155
module/tasks/endpoints/admin/task_category_upsert.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_category_upsert_append_query_params')) {
|
||||
function task_cert_category_upsert_append_query_params(string $url, array $params): string
|
||||
{
|
||||
if ($url === '' || $params === []) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
$queryString = http_build_query($params);
|
||||
if ($queryString === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$separator = strpos($url, '?') === false ? '?' : '&';
|
||||
return $url . $separator . $queryString;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
if ($path !== '' && substr($path, -1) !== '/') {
|
||||
$parts['path'] = $path . '/';
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
$rawQuery = (string) ($parts['query'] ?? '');
|
||||
if ($rawQuery !== '') {
|
||||
parse_str($rawQuery, $existing);
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
if (!is_string($key) || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
unset($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[$key] = $value;
|
||||
}
|
||||
|
||||
$parts['query'] = http_build_query($existing);
|
||||
return task_cert_category_upsert_build_url($parts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('task_cert_category_upsert_build_url')) {
|
||||
function task_cert_category_upsert_build_url(array $parts): string
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if (isset($parts['scheme']) && $parts['scheme'] !== '') {
|
||||
$url .= $parts['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (isset($parts['user']) && $parts['user'] !== '') {
|
||||
$url .= $parts['user'];
|
||||
if (isset($parts['pass']) && $parts['pass'] !== '') {
|
||||
$url .= ':' . $parts['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host']) && $parts['host'] !== '') {
|
||||
$url .= $parts['host'];
|
||||
}
|
||||
|
||||
if (isset($parts['port'])) {
|
||||
$url .= ':' . (int) $parts['port'];
|
||||
}
|
||||
|
||||
$url .= (string) ($parts['path'] ?? '');
|
||||
|
||||
$query = (string) ($parts['query'] ?? '');
|
||||
if ($query !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
$fragment = (string) ($parts['fragment'] ?? '');
|
||||
if ($fragment !== '') {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$categoryId = TaskCertRequest::postInt('category_id', 0);
|
||||
$name = TaskCertRequest::postString('name', '');
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
$activeRaw = TaskCertRequest::postString('active', '');
|
||||
$activeOverride = null;
|
||||
if ($activeRaw !== '') {
|
||||
$activeValue = (int) $activeRaw;
|
||||
if (!in_array($activeValue, [0, 1], true)) {
|
||||
TaskCertResponse::error('Ungültiger Statuswert', 400);
|
||||
}
|
||||
$activeOverride = $activeValue;
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$categoryService = $services['taskCategoryService'] ?? null;
|
||||
if (!$categoryService instanceof TaskCategoryService) {
|
||||
throw new RuntimeException('Kategorie-Service nicht verfügbar.');
|
||||
}
|
||||
|
||||
$result = $categoryService->upsert($categoryId, $name, $auth->contactId(), $activeOverride);
|
||||
$action = (string) ($result['action'] ?? ($categoryId > 0 ? 'category_update' : 'category_create'));
|
||||
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_upsert_append_query_params($redirectTo, [
|
||||
'cat_done' => 1,
|
||||
'cat_action' => $action,
|
||||
'cat_error' => null,
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'category' => $result['category'] ?? null,
|
||||
'action' => $action,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
if ($redirectTo !== '') {
|
||||
$redirectTo = task_cert_category_upsert_append_query_params($redirectTo, [
|
||||
'cat_done' => 0,
|
||||
'cat_error' => $throwable->getMessage(),
|
||||
]);
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
40
module/tasks/endpoints/admin/task_delete.php
Normal file
40
module/tasks/endpoints/admin/task_delete.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::error('Ungültige task_id', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$services['taskDefinitionService']->softDeleteTask($taskId);
|
||||
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
if ($redirectTo !== '') {
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
53
module/tasks/endpoints/admin/task_delete_hard.php
Normal file
53
module/tasks/endpoints/admin/task_delete_hard.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
if ($taskId <= 0) {
|
||||
TaskCertResponse::error('Ungültige task_id', 400);
|
||||
}
|
||||
|
||||
$confirmed = TaskCertRequest::postInt('confirm_hard_delete', 0) === 1;
|
||||
if (!$confirmed) {
|
||||
TaskCertResponse::error('Bitte endgültiges Löschen bestätigen.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = task_cert_services();
|
||||
$impact = $services['taskDefinitionService']->hardDeleteTask($taskId);
|
||||
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
if ($redirectTo !== '') {
|
||||
TaskCertResponse::redirect($redirectTo);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'deleted' => [
|
||||
'task' => 1,
|
||||
'methods' => (int) ($impact['methods'] ?? 0),
|
||||
'assignments' => (int) ($impact['assignments'] ?? 0),
|
||||
'submissions' => (int) ($impact['submissions'] ?? 0),
|
||||
'certificates' => (int) ($impact['certificates'] ?? 0),
|
||||
'escalation_events' => (int) ($impact['escalation_events'] ?? 0),
|
||||
],
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
418
module/tasks/endpoints/admin/task_upsert.php
Normal file
418
module/tasks/endpoints/admin/task_upsert.php
Normal file
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
if (!function_exists('task_cert_append_query_params')) {
|
||||
function task_cert_append_query_params(string $url, array $params): string
|
||||
{
|
||||
if ($url === '' || $params === []) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$queryString = http_build_query($params);
|
||||
if ($queryString === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$separator = strpos($url, '?') === false ? '?' : '&';
|
||||
return $url . $separator . $queryString;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TaskCertRequest::isPost()) {
|
||||
TaskCertResponse::error('Methode nicht erlaubt', 405);
|
||||
}
|
||||
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
if (!$auth->hasPermission('r-task-cert-admin')) {
|
||||
TaskCertResponse::error('Keine Berechtigung', 403);
|
||||
}
|
||||
|
||||
TaskCertCsrf::assertValidFromRequest(true);
|
||||
|
||||
$services = task_cert_services();
|
||||
$taskService = $services['taskDefinitionService'];
|
||||
|
||||
$parseCsvOptions = static function (string $raw): array {
|
||||
$result = [];
|
||||
$parts = preg_split('/[,\n]/', $raw) ?: [];
|
||||
foreach ($parts as $part) {
|
||||
$item = trim((string) $part);
|
||||
if ($item === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'value' => $item,
|
||||
'label' => $item,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
};
|
||||
|
||||
$slugifyFieldKey = static function (string $label): string {
|
||||
$map = [
|
||||
'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss',
|
||||
'Ä' => 'ae', 'Ö' => 'oe', 'Ü' => 'ue',
|
||||
];
|
||||
$slug = strtr(mb_strtolower($label, 'UTF-8'), $map);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', $slug) ?? '';
|
||||
return trim($slug, '_');
|
||||
};
|
||||
|
||||
$buildMethods = static function () use ($parseCsvOptions, $slugifyFieldKey): array {
|
||||
$methodEnabled = TaskCertRequest::postArray('method_enabled');
|
||||
$orderedTypes = TaskCertConstants::METHOD_TYPES;
|
||||
|
||||
$methods = [];
|
||||
$sortOrder = 0;
|
||||
|
||||
foreach ($orderedTypes as $methodType) {
|
||||
if ((int) ($methodEnabled[$methodType] ?? 0) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = [
|
||||
'method_type' => $methodType,
|
||||
'is_required' => 1,
|
||||
'sort_order' => $sortOrder,
|
||||
'active' => 1,
|
||||
'settings' => [],
|
||||
];
|
||||
|
||||
if ($methodType === 'knowledge_read') {
|
||||
$knowledgePosts = [];
|
||||
$postIds = TaskCertRequest::postIntArray('knowledge_post_ids');
|
||||
foreach ($postIds as $index => $postId) {
|
||||
$knowledgePosts[] = [
|
||||
'knowledge_post_id' => $postId,
|
||||
'required' => 1,
|
||||
'sort_order' => $index,
|
||||
];
|
||||
}
|
||||
|
||||
$method['knowledge_posts'] = $knowledgePosts;
|
||||
}
|
||||
|
||||
if ($methodType === 'form_submit') {
|
||||
$formFields = [];
|
||||
$rows = TaskCertRequest::postArray('form_fields');
|
||||
|
||||
foreach ($rows as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = trim((string) ($row['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldKey = trim((string) ($row['field_key'] ?? ''));
|
||||
if ($fieldKey === '') {
|
||||
$base = $slugifyFieldKey($label);
|
||||
if ($base === '') {
|
||||
$base = 'field';
|
||||
}
|
||||
$fieldKey = $base;
|
||||
$usedKeys = array_column($formFields, 'field_key');
|
||||
$suffix = 2;
|
||||
while (in_array($fieldKey, $usedKeys, true)) {
|
||||
$fieldKey = $base . '_' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
$fieldType = (string) ($row['field_type'] ?? 'text');
|
||||
if (!TaskCertConstants::isValidFieldType($fieldType)) {
|
||||
$fieldType = 'text';
|
||||
}
|
||||
|
||||
$formFields[] = [
|
||||
'field_key' => $fieldKey,
|
||||
'label' => $label,
|
||||
'field_type' => $fieldType,
|
||||
'is_required' => (int) (($row['is_required'] ?? 0) ? 1 : 0),
|
||||
'options' => $parseCsvOptions((string) ($row['options_text'] ?? '')),
|
||||
'placeholder' => trim((string) ($row['placeholder'] ?? '')),
|
||||
'prefill_column' => ContactPrefillColumns::sanitize(trim((string) ($row['prefill_column'] ?? ''))),
|
||||
'sort_order' => count($formFields),
|
||||
'active' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$method['form_fields'] = $formFields;
|
||||
}
|
||||
|
||||
if ($methodType === 'quiz') {
|
||||
$quizQuestions = [];
|
||||
$rows = TaskCertRequest::postArray('quiz_questions');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$questionText = trim((string) ($row['question_text'] ?? ''));
|
||||
if ($questionText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$correctOption = strtolower(trim((string) ($row['correct_option'] ?? 'a')));
|
||||
if (!in_array($correctOption, ['a', 'b', 'c', 'd'], true)) {
|
||||
$correctOption = 'a';
|
||||
}
|
||||
|
||||
$optionsMap = [
|
||||
'a' => trim((string) ($row['option_a'] ?? '')),
|
||||
'b' => trim((string) ($row['option_b'] ?? '')),
|
||||
'c' => trim((string) ($row['option_c'] ?? '')),
|
||||
'd' => trim((string) ($row['option_d'] ?? '')),
|
||||
];
|
||||
|
||||
$options = [];
|
||||
foreach (['a', 'b', 'c', 'd'] as $position => $letter) {
|
||||
$optionText = $optionsMap[$letter];
|
||||
if ($optionText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[] = [
|
||||
'option_text' => $optionText,
|
||||
'is_correct' => $letter === $correctOption ? 1 : 0,
|
||||
'sort_order' => $position,
|
||||
];
|
||||
}
|
||||
|
||||
if ($options === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quizQuestions[] = [
|
||||
'question_text' => $questionText,
|
||||
'sort_order' => count($quizQuestions),
|
||||
'active' => 1,
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
|
||||
$method['quiz_questions'] = $quizQuestions;
|
||||
}
|
||||
|
||||
$methods[] = $method;
|
||||
$sortOrder++;
|
||||
}
|
||||
|
||||
return $methods;
|
||||
};
|
||||
|
||||
$buildTargetRules = static function (): array {
|
||||
$mandantIds = array_values(array_unique(TaskCertRequest::postIntArray('target_mandant_ids')));
|
||||
$departmentIds = array_values(array_unique(TaskCertRequest::postIntArray('target_department_ids')));
|
||||
$roleIds = array_values(array_unique(TaskCertRequest::postIntArray('target_role_ids')));
|
||||
|
||||
$mandantValues = $mandantIds === [] ? [0] : $mandantIds;
|
||||
$departmentValues = $departmentIds === [] ? [0] : $departmentIds;
|
||||
$roleValues = $roleIds === [] ? [0] : $roleIds;
|
||||
|
||||
$rules = [];
|
||||
$count = 0;
|
||||
$maxRules = 5000;
|
||||
|
||||
foreach ($mandantValues as $mandantId) {
|
||||
foreach ($departmentValues as $departmentId) {
|
||||
foreach ($roleValues as $roleId) {
|
||||
if ((int) $mandantId === 0 && (int) $departmentId === 0 && (int) $roleId === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[] = [
|
||||
'main_mandant_id' => (int) $mandantId ?: null,
|
||||
'main_department_id' => (int) $departmentId ?: null,
|
||||
'main_role_id' => (int) $roleId ?: null,
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
$count++;
|
||||
if ($count > $maxRules) {
|
||||
throw new RuntimeException('Zu viele Regelkombinationen. Bitte Auswahl einschränken.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
};
|
||||
|
||||
$buildTargetOverrides = static function (): array {
|
||||
$includeReason = TaskCertRequest::postString('override_include_reason', '');
|
||||
$excludeReason = TaskCertRequest::postString('override_exclude_reason', '');
|
||||
$includeIds = array_values(array_unique(TaskCertRequest::postIntArray('override_include_contact_ids')));
|
||||
$excludeIds = array_values(array_unique(TaskCertRequest::postIntArray('override_exclude_contact_ids')));
|
||||
|
||||
// exclude wins when the same contact is selected in both lists
|
||||
$includeIds = array_values(array_diff($includeIds, $excludeIds));
|
||||
|
||||
$overrides = [];
|
||||
|
||||
foreach ($includeIds as $contactId) {
|
||||
$overrides[] = [
|
||||
'main_contact_id' => $contactId,
|
||||
'override_type' => 'include',
|
||||
'reason' => $includeReason,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($excludeIds as $contactId) {
|
||||
$overrides[] = [
|
||||
'main_contact_id' => $contactId,
|
||||
'override_type' => 'exclude',
|
||||
'reason' => $excludeReason,
|
||||
];
|
||||
}
|
||||
|
||||
return $overrides;
|
||||
};
|
||||
|
||||
try {
|
||||
$methods = $buildMethods();
|
||||
if ($methods === []) {
|
||||
throw new RuntimeException('Mindestens eine Erledigungsart muss aktiviert sein.');
|
||||
}
|
||||
|
||||
$deadlineRuleType = TaskCertRequest::postString('deadline_rule_type', 'cycle_end');
|
||||
if (!in_array($deadlineRuleType, ['cycle_end', 'offset_days', 'weekday', 'monthday'], true)) {
|
||||
$deadlineRuleType = 'cycle_end';
|
||||
}
|
||||
|
||||
$deadlineOffsetDays = null;
|
||||
$deadlineWeekday = null;
|
||||
$deadlineMonthday = null;
|
||||
|
||||
if ($deadlineRuleType === 'offset_days') {
|
||||
$deadlineOffsetDays = TaskCertRequest::postInt('deadline_offset_days', 0);
|
||||
if ($deadlineOffsetDays <= 0) {
|
||||
throw new RuntimeException('Bitte Frist in Tagen größer 0 angeben.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($deadlineRuleType === 'weekday') {
|
||||
$deadlineWeekday = TaskCertRequest::postInt('deadline_weekday', 0);
|
||||
if ($deadlineWeekday < 1 || $deadlineWeekday > 7) {
|
||||
throw new RuntimeException('Bitte einen gültigen Wochentag (1-7) auswählen.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($deadlineRuleType === 'monthday') {
|
||||
$deadlineMonthday = TaskCertRequest::postInt('deadline_monthday', 0);
|
||||
if ($deadlineMonthday < 1 || $deadlineMonthday > 31) {
|
||||
throw new RuntimeException('Bitte einen gültigen Monatstag (1-31) auswählen.');
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'actor_id' => $auth->contactId(),
|
||||
'title' => TaskCertRequest::postString('title'),
|
||||
'description' => TaskCertHtml::sanitize(TaskCertRequest::postString('description')),
|
||||
'task_category_id' => TaskCertRequest::postInt('task_category_id', 0),
|
||||
'frequency_type' => TaskCertRequest::postString('frequency_type', 'once'),
|
||||
'recurrence_basis' => TaskCertRequest::postString('recurrence_basis', 'calendar'),
|
||||
'recurrence_unit' => TaskCertRequest::postString('recurrence_unit', ''),
|
||||
'recurrence_interval' => TaskCertRequest::postInt('recurrence_interval', 1),
|
||||
'start_date' => TaskCertRequest::postString('start_date', date('Y-m-d')),
|
||||
'deadline_rule_type' => $deadlineRuleType,
|
||||
'deadline_offset_days' => $deadlineOffsetDays,
|
||||
'deadline_weekday' => $deadlineWeekday,
|
||||
'deadline_monthday' => $deadlineMonthday,
|
||||
'fulfillment_mode' => TaskCertRequest::postString('fulfillment_mode', 'all'),
|
||||
'certificate_enabled' => TaskCertRequest::postInt('certificate_enabled', 1),
|
||||
'certificate_title' => TaskCertRequest::postString('certificate_title', ''),
|
||||
'certificate_validity_mode' => TaskCertRequest::postString('certificate_validity_mode', ''),
|
||||
'certificate_validity_days' => TaskCertRequest::postString('certificate_validity_days', ''),
|
||||
'show_cycle_period' => TaskCertRequest::postInt('show_cycle_period', 0) === 1 ? 1 : 0,
|
||||
'active' => TaskCertRequest::postInt('active', 1),
|
||||
'methods' => $methods,
|
||||
'target_rules' => $buildTargetRules(),
|
||||
'target_overrides' => $buildTargetOverrides(),
|
||||
'responsible_contact_ids' => array_values(array_unique(TaskCertRequest::postIntArray('responsible_contact_ids'))),
|
||||
'reminder_policy' => [
|
||||
'base_mode' => 'due_date',
|
||||
'first_escalation_days' => TaskCertRequest::postInt('reminder_first_escalation_days', 0),
|
||||
'step_days' => TaskCertRequest::postInt('reminder_step_days', 7),
|
||||
'max_level' => TaskCertRequest::postInt('reminder_max_level', 3),
|
||||
'grace_days' => TaskCertRequest::postInt('reminder_grace_days', 7),
|
||||
'active' => TaskCertRequest::postInt('reminder_active', 0) === 1 ? 1 : 0,
|
||||
],
|
||||
];
|
||||
|
||||
$taskId = TaskCertRequest::postInt('task_id', 0);
|
||||
$resetExistingRecords = $taskId > 0 && TaskCertRequest::postInt('reset_existing_records', 0) === 1;
|
||||
$resetStats = [
|
||||
'cycle_key' => '',
|
||||
'assignment_count' => 0,
|
||||
'submission_count' => 0,
|
||||
'certificate_count' => 0,
|
||||
];
|
||||
|
||||
if ($taskId > 0) {
|
||||
$taskService->updateTask($taskId, $payload);
|
||||
if ($resetExistingRecords) {
|
||||
$resetStats = $taskService->resetCurrentCycleProgress($taskId, $auth->contactId());
|
||||
}
|
||||
} else {
|
||||
$taskId = $taskService->createTask($payload);
|
||||
}
|
||||
|
||||
$afterSave = TaskCertRequest::postString('after_save', 'stay');
|
||||
if (!in_array($afterSave, ['stay', 'close'], true)) {
|
||||
$afterSave = 'stay';
|
||||
}
|
||||
|
||||
$redirectTo = TaskCertRequest::postString('redirect_to', '');
|
||||
$redirectToStay = TaskCertRequest::postString('redirect_to_stay', '');
|
||||
$redirectToClose = TaskCertRequest::postString('redirect_to_close', '');
|
||||
|
||||
if ($redirectToStay === '') {
|
||||
$redirectToStay = task_cert_view_url('admin.task_form', [
|
||||
'id' => $taskId,
|
||||
'saved' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($redirectToClose === '') {
|
||||
$redirectToClose = task_cert_view_url('admin.task_list');
|
||||
}
|
||||
|
||||
$resolvedRedirect = $redirectTo;
|
||||
if ($resolvedRedirect === '') {
|
||||
$resolvedRedirect = $afterSave === 'close' ? $redirectToClose : $redirectToStay;
|
||||
}
|
||||
|
||||
if ($resolvedRedirect !== '') {
|
||||
$resolvedRedirect = str_replace('{task_id}', (string) $taskId, $resolvedRedirect);
|
||||
if ($resetExistingRecords) {
|
||||
$resolvedRedirect = task_cert_append_query_params($resolvedRedirect, [
|
||||
'reset' => 1,
|
||||
'reset_assignments' => (int) ($resetStats['assignment_count'] ?? 0),
|
||||
'reset_submissions' => (int) ($resetStats['submission_count'] ?? 0),
|
||||
'reset_certificates' => (int) ($resetStats['certificate_count'] ?? 0),
|
||||
]);
|
||||
}
|
||||
TaskCertResponse::redirect($resolvedRedirect);
|
||||
}
|
||||
|
||||
TaskCertResponse::json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'redirect' => $resolvedRedirect,
|
||||
'reset' => $resetExistingRecords,
|
||||
'reset_stats' => $resetStats,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
TaskCertResponse::json([
|
||||
'success' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
Reference in New Issue
Block a user