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:
275
module/tasks/service/EscalationService.php
Normal file
275
module/tasks/service/EscalationService.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class EscalationService
|
||||
{
|
||||
private TaskAssignmentRepository $assignmentRepo;
|
||||
private TaskRuleRepository $ruleRepo;
|
||||
private TaskChangeLogRepository $changeLogRepo;
|
||||
|
||||
public function __construct(
|
||||
TaskAssignmentRepository $assignmentRepo,
|
||||
TaskRuleRepository $ruleRepo,
|
||||
TaskChangeLogRepository $changeLogRepo
|
||||
) {
|
||||
$this->assignmentRepo = $assignmentRepo;
|
||||
$this->ruleRepo = $ruleRepo;
|
||||
$this->changeLogRepo = $changeLogRepo;
|
||||
}
|
||||
|
||||
public function recalculateEscalations(): int
|
||||
{
|
||||
$summary = $this->recalculateEscalationsDetailed(true, null);
|
||||
return (int) ($summary['changed_assignments'] ?? 0);
|
||||
}
|
||||
|
||||
public function recalculateEscalationsDetailed(bool $apply = true, ?int $taskId = null): array
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$auth = TaskCertAuthContext::fromGlobals();
|
||||
$changedBy = $auth->isLoggedIn() ? $auth->contactId() : 0;
|
||||
$scope = $taskId !== null && $taskId > 0 ? 'single_task' : 'all_active_tasks';
|
||||
$jobRunId = $this->createJobRunId();
|
||||
$trigger = PHP_SAPI === 'cli' ? 'cron_cli' : 'admin_ui';
|
||||
|
||||
$summary = [
|
||||
'apply' => $apply,
|
||||
'task_id' => $taskId !== null && $taskId > 0 ? $taskId : null,
|
||||
'scope' => $scope,
|
||||
'job_run_id' => $jobRunId,
|
||||
'trigger' => $trigger,
|
||||
'processed_candidates' => 0,
|
||||
'changed_assignments' => 0,
|
||||
'entered_count' => 0,
|
||||
'cleared_count' => 0,
|
||||
'tasks' => [],
|
||||
];
|
||||
$taskSummary = [];
|
||||
|
||||
foreach ($this->assignmentRepo->findEscalationCandidates($taskId) as $assignment) {
|
||||
$summary['processed_candidates']++;
|
||||
$taskId = (int) $assignment['task_id'];
|
||||
$dueAtRaw = (string) ($assignment['due_at'] ?? '');
|
||||
if ($dueAtRaw === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$dueAt = new DateTimeImmutable($dueAtRaw);
|
||||
} catch (Throwable $throwable) {
|
||||
continue;
|
||||
}
|
||||
$graceUntilRaw = trim((string) ($assignment['grace_until'] ?? ''));
|
||||
$graceUntil = null;
|
||||
if ($graceUntilRaw !== '') {
|
||||
try {
|
||||
$graceUntil = new DateTimeImmutable($graceUntilRaw);
|
||||
} catch (Throwable $throwable) {
|
||||
$graceUntil = null;
|
||||
}
|
||||
}
|
||||
$effectiveDueAt = ($graceUntil !== null && $graceUntil > $dueAt) ? $graceUntil : $dueAt;
|
||||
$isOverdue = $now > $effectiveDueAt;
|
||||
$assignmentId = (int) $assignment['id'];
|
||||
$currentStatus = (string) ($assignment['status'] ?? '');
|
||||
$oldLevel = (int) ($assignment['escalation_level'] ?? 0);
|
||||
$assignmentChanged = false;
|
||||
$assignmentEntered = false;
|
||||
$assignmentCleared = false;
|
||||
|
||||
if (!$isOverdue) {
|
||||
if ($currentStatus === 'overdue') {
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->clearOverdue($assignmentId);
|
||||
}
|
||||
$assignmentChanged = true;
|
||||
}
|
||||
|
||||
if ($oldLevel !== 0) {
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->clearEscalation($assignmentId);
|
||||
$this->assignmentRepo->addEscalationEvent(
|
||||
$assignmentId,
|
||||
0,
|
||||
'cleared',
|
||||
['reason' => 'due_date_not_reached']
|
||||
);
|
||||
}
|
||||
$assignmentChanged = true;
|
||||
$assignmentCleared = true;
|
||||
}
|
||||
|
||||
$this->collectEscalationStats($summary, $taskSummary, $taskId, $assignmentChanged, $assignmentEntered, $assignmentCleared);
|
||||
continue;
|
||||
}
|
||||
|
||||
$overdueAtRaw = trim((string) ($assignment['overdue_at'] ?? ''));
|
||||
$overdueAt = $overdueAtRaw !== '' ? new DateTimeImmutable($overdueAtRaw) : $effectiveDueAt;
|
||||
if ($currentStatus !== 'overdue') {
|
||||
$overdueAt = $effectiveDueAt;
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->markOverdue($assignmentId, $overdueAt->format('Y-m-d H:i:s'));
|
||||
}
|
||||
$assignmentChanged = true;
|
||||
} elseif ($overdueAtRaw === '') {
|
||||
$overdueAt = $effectiveDueAt;
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->markOverdue($assignmentId, $overdueAt->format('Y-m-d H:i:s'));
|
||||
}
|
||||
$assignmentChanged = true;
|
||||
}
|
||||
|
||||
$policy = $this->ruleRepo->getReminderPolicyByTask($taskId);
|
||||
if ($policy === null || (int) ($policy['active'] ?? 1) !== 1) {
|
||||
if ($oldLevel !== 0) {
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->clearEscalation($assignmentId);
|
||||
$this->assignmentRepo->addEscalationEvent(
|
||||
$assignmentId,
|
||||
0,
|
||||
'cleared',
|
||||
['reason' => 'policy_inactive']
|
||||
);
|
||||
}
|
||||
$assignmentChanged = true;
|
||||
$assignmentCleared = true;
|
||||
}
|
||||
|
||||
$this->collectEscalationStats($summary, $taskSummary, $taskId, $assignmentChanged, $assignmentEntered, $assignmentCleared);
|
||||
continue;
|
||||
}
|
||||
|
||||
$daysOverdue = (int) floor(($now->getTimestamp() - $overdueAt->getTimestamp()) / 86400);
|
||||
$firstDays = max(0, (int) ($policy['first_escalation_days'] ?? 0));
|
||||
$stepDays = max(1, (int) ($policy['step_days'] ?? 7));
|
||||
$maxLevel = max(1, (int) ($policy['max_level'] ?? 3));
|
||||
|
||||
if ($daysOverdue < $firstDays) {
|
||||
$newLevel = 0;
|
||||
} else {
|
||||
$newLevel = 1 + (int) floor(($daysOverdue - $firstDays) / $stepDays);
|
||||
$newLevel = min($newLevel, $maxLevel);
|
||||
}
|
||||
|
||||
if ($newLevel !== $oldLevel) {
|
||||
$assignmentChanged = true;
|
||||
if ($newLevel === 0) {
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->clearEscalation($assignmentId);
|
||||
$this->assignmentRepo->addEscalationEvent(
|
||||
$assignmentId,
|
||||
0,
|
||||
'cleared',
|
||||
[
|
||||
'days_overdue' => $daysOverdue,
|
||||
'old_level' => $oldLevel,
|
||||
]
|
||||
);
|
||||
}
|
||||
$assignmentCleared = true;
|
||||
} else {
|
||||
if ($apply) {
|
||||
$this->assignmentRepo->setEscalationLevel($assignmentId, $newLevel);
|
||||
$this->assignmentRepo->addEscalationEvent(
|
||||
$assignmentId,
|
||||
$newLevel,
|
||||
'entered',
|
||||
[
|
||||
'days_overdue' => $daysOverdue,
|
||||
'old_level' => $oldLevel,
|
||||
'new_level' => $newLevel,
|
||||
]
|
||||
);
|
||||
}
|
||||
$assignmentEntered = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->collectEscalationStats($summary, $taskSummary, $taskId, $assignmentChanged, $assignmentEntered, $assignmentCleared);
|
||||
}
|
||||
|
||||
$summary['tasks'] = array_values($taskSummary);
|
||||
|
||||
if (!$apply) {
|
||||
return $summary;
|
||||
}
|
||||
|
||||
foreach ($summary['tasks'] as $taskStat) {
|
||||
$changedAssignments = (int) ($taskStat['changed_assignments'] ?? 0);
|
||||
if ($changedAssignments <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskIdForLog = (int) ($taskStat['task_id'] ?? 0);
|
||||
if ($taskIdForLog <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->changeLogRepo->create(
|
||||
$taskIdForLog,
|
||||
'recalculate_escalations_apply',
|
||||
$changedBy,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
'changed_assignments' => $changedAssignments,
|
||||
'entered_count' => (int) ($taskStat['entered_count'] ?? 0),
|
||||
'cleared_count' => (int) ($taskStat['cleared_count'] ?? 0),
|
||||
'scope' => $scope,
|
||||
'trigger' => $trigger,
|
||||
'job_run_id' => $jobRunId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function collectEscalationStats(
|
||||
array &$summary,
|
||||
array &$taskSummary,
|
||||
int $taskId,
|
||||
bool $assignmentChanged,
|
||||
bool $assignmentEntered,
|
||||
bool $assignmentCleared
|
||||
): void {
|
||||
if ($taskId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($taskSummary[$taskId])) {
|
||||
$taskSummary[$taskId] = [
|
||||
'task_id' => $taskId,
|
||||
'changed_assignments' => 0,
|
||||
'entered_count' => 0,
|
||||
'cleared_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if ($assignmentChanged) {
|
||||
$summary['changed_assignments']++;
|
||||
$taskSummary[$taskId]['changed_assignments']++;
|
||||
}
|
||||
|
||||
if ($assignmentEntered) {
|
||||
$summary['entered_count']++;
|
||||
$taskSummary[$taskId]['entered_count']++;
|
||||
}
|
||||
|
||||
if ($assignmentCleared) {
|
||||
$summary['cleared_count']++;
|
||||
$taskSummary[$taskId]['cleared_count']++;
|
||||
}
|
||||
}
|
||||
|
||||
private function createJobRunId(): string
|
||||
{
|
||||
try {
|
||||
$entropy = bin2hex(random_bytes(6));
|
||||
} catch (Throwable $throwable) {
|
||||
$entropy = substr(sha1((string) microtime(true)), 0, 12);
|
||||
}
|
||||
|
||||
return 'esc-' . date('YmdHis') . '-' . $entropy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user