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:
2026-06-22 09:20:02 +02:00
parent 2e75ff7d59
commit 743476f64a
136 changed files with 38100 additions and 627 deletions

View 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);
}

View 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]);

View 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);
}

View 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);
}

View 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);
}

View 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(),
]);

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}