Files
awo-hamburg-intranet/module/tasks/repo/TaskAssignmentRepository.php
Moritz Weinmann 743476f64a Module tasks + knowledgecenter_update aus Kundenprojekt übernehmen (bereinigt)
Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt:

Knowledgecenter:
- Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten
- Files-Gallery-Lizenzschlüssel entfernt (config.php)
- Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur,
  nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss),
  Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge
- 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …)

Tasks:
- Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments)
- Task-Definitionen und Verlinkungen geleert
- Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 09:20:02 +02:00

515 lines
16 KiB
PHP

<?php
declare(strict_types=1);
class TaskAssignmentRepository
{
public function createIfNotExists(
int $taskId,
int $contactId,
string $cycleKey,
string $assignedSource,
?string $cycleStartAt,
?string $cycleEndAt,
?string $dueAt,
?string $graceUntil = null
): int {
$existing = TaskCertDb::fetchOne(
'SELECT id FROM task_assignment WHERE task_id = ? AND main_contact_id = ? AND cycle_key = ? LIMIT 1',
'iis',
[$taskId, $contactId, $cycleKey]
);
if ($existing !== null) {
$this->syncAssignmentCycleData((int) $existing['id'], $cycleStartAt, $cycleEndAt, $dueAt);
return 0;
}
if ($cycleEndAt !== null && trim($cycleEndAt) !== '') {
$cycleEndMatch = TaskCertDb::fetchOne(
'SELECT id
FROM task_assignment
WHERE task_id = ?
AND main_contact_id = ?
AND cycle_end_at = ?
ORDER BY id DESC
LIMIT 1',
'iis',
[$taskId, $contactId, $cycleEndAt]
);
if ($cycleEndMatch !== null) {
$this->rekeyAndSyncAssignment(
(int) $cycleEndMatch['id'],
$cycleKey,
$cycleStartAt,
$cycleEndAt,
$dueAt
);
return 0;
}
}
return TaskCertDb::insert(
'INSERT INTO task_assignment (task_id, main_contact_id, cycle_key, status, assigned_source, cycle_start_at, cycle_end_at, due_at, grace_until)
VALUES (?, ?, ?, "open", ?, ?, ?, ?, ?)',
'iissssss',
[$taskId, $contactId, $cycleKey, $assignedSource, $cycleStartAt, $cycleEndAt, $dueAt, $graceUntil]
);
}
private function syncAssignmentCycleData(
int $assignmentId,
?string $cycleStartAt,
?string $cycleEndAt,
?string $dueAt
): void {
TaskCertDb::execute(
'UPDATE task_assignment
SET cycle_start_at = ?,
cycle_end_at = ?,
due_at = ?,
modified_at = NOW()
WHERE id = ?',
'sssi',
[$cycleStartAt, $cycleEndAt, $dueAt, $assignmentId]
);
}
private function rekeyAndSyncAssignment(
int $assignmentId,
string $cycleKey,
?string $cycleStartAt,
?string $cycleEndAt,
?string $dueAt
): void {
TaskCertDb::execute(
'UPDATE task_assignment
SET cycle_key = ?,
cycle_start_at = ?,
cycle_end_at = ?,
due_at = ?,
modified_at = NOW()
WHERE id = ?',
'ssssi',
[$cycleKey, $cycleStartAt, $cycleEndAt, $dueAt, $assignmentId]
);
}
public function findById(int $assignmentId): ?array
{
return TaskCertDb::fetchOne('SELECT * FROM task_assignment WHERE id = ?', 'i', [$assignmentId]);
}
public function listForUser(int $contactId): array
{
return TaskCertDb::fetchAll(
'SELECT
a.*,
d.title,
d.description,
d.task_category_id,
cat.name AS task_category_name,
cat.slug AS task_category_slug,
d.fulfillment_mode,
d.show_cycle_period,
d.certificate_enabled,
d.certificate_title,
cert.valid_until AS certificate_valid_until,
cert.status AS certificate_status
FROM task_assignment a
INNER JOIN task_definition d ON d.id = a.task_id
LEFT JOIN task_category cat ON cat.id = d.task_category_id
LEFT JOIN task_certificate cert ON cert.assignment_id = a.id
WHERE a.main_contact_id = ?
ORDER BY
CASE a.status
WHEN "overdue" THEN 1
WHEN "rejected" THEN 2
WHEN "open" THEN 3
WHEN "in_progress" THEN 4
WHEN "approval_pending" THEN 5
WHEN "completed" THEN 6
WHEN "expired" THEN 7
ELSE 7
END,
a.due_at IS NULL,
a.due_at ASC,
a.id DESC',
'i',
[$contactId]
);
}
public function listForAdmin(int $taskId = 0, string $status = '', string $searchQuery = ''): array
{
$searchQuery = trim($searchQuery);
$sql = 'SELECT
a.*,
d.title AS task_title,
c.name AS contact_name,
c.email AS contact_email
FROM task_assignment a
INNER JOIN task_definition d ON d.id = a.task_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE 1=1';
$types = '';
$params = [];
if ($taskId > 0) {
$sql .= ' AND a.task_id = ?';
$types .= 'i';
$params[] = $taskId;
}
if ($status !== '') {
$sql .= ' AND a.status = ?';
$types .= 's';
$params[] = $status;
}
if ($searchQuery !== '') {
$searchLike = '%' . $searchQuery . '%';
$sql .= ' AND (
CAST(a.id AS CHAR) LIKE ?
OR d.title LIKE ?
OR COALESCE(c.name, "") LIKE ?
OR COALESCE(c.email, "") LIKE ?
OR a.status LIKE ?
)';
$types .= 'sssss';
$params[] = $searchLike;
$params[] = $searchLike;
$params[] = $searchLike;
$params[] = $searchLike;
$params[] = $searchLike;
}
$sql .= ' ORDER BY a.escalation_level DESC, a.due_at ASC, a.id DESC';
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listByTaskAndCycleKey(int $taskId, string $cycleKey): array
{
return TaskCertDb::fetchAll(
'SELECT *
FROM task_assignment
WHERE task_id = ?
AND cycle_key = ?
ORDER BY id ASC',
'is',
[$taskId, $cycleKey]
);
}
public function listByTask(int $taskId): array
{
return TaskCertDb::fetchAll(
'SELECT *
FROM task_assignment
WHERE task_id = ?
ORDER BY id ASC',
'i',
[$taskId]
);
}
public function listByContactId(int $contactId): array
{
return TaskCertDb::fetchAll(
'SELECT
a.id, a.task_id, a.status, a.due_at, a.grace_until,
a.created_at, a.cycle_key, a.escalation_level,
d.title AS task_title,
d.active AS task_active,
tc.name AS task_category_name,
ls.last_submitted_at
FROM task_assignment a
INNER JOIN task_definition d ON d.id = a.task_id
LEFT JOIN task_category tc ON tc.id = d.task_category_id
LEFT JOIN (
SELECT assignment_id, MAX(submitted_at) AS last_submitted_at
FROM task_submission
GROUP BY assignment_id
) ls ON ls.assignment_id = a.id
WHERE a.main_contact_id = ?
ORDER BY
CASE a.status
WHEN "overdue" THEN 1
WHEN "open" THEN 2
WHEN "in_progress" THEN 3
WHEN "approval_pending" THEN 4
WHEN "rejected" THEN 5
WHEN "completed" THEN 6
WHEN "expired" THEN 7
ELSE 8
END,
a.due_at ASC',
'i',
[$contactId]
);
}
public function findLatestByTaskAndContact(int $taskId, int $contactId): ?array
{
return TaskCertDb::fetchOne(
'SELECT *
FROM task_assignment
WHERE task_id = ?
AND main_contact_id = ?
ORDER BY id DESC
LIMIT 1',
'ii',
[$taskId, $contactId]
);
}
public function listLatestByTaskForContacts(int $taskId, array $contactIds): array
{
$contactIds = array_values(array_unique(array_map('intval', $contactIds)));
$contactIds = array_values(array_filter($contactIds, static function (int $contactId): bool {
return $contactId > 0;
}));
if ($taskId <= 0 || $contactIds === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($contactIds), '?'));
$types = 'i' . str_repeat('i', count($contactIds));
$params = array_merge([$taskId], $contactIds);
return TaskCertDb::fetchAll(
'SELECT a.*
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
AND main_contact_id IN (' . $placeholders . ')
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
ORDER BY a.main_contact_id ASC',
$types,
$params
);
}
public function listRemovableOpenByTaskAndContacts(int $taskId, array $contactIds): array
{
$contactIds = array_values(array_unique(array_map('intval', $contactIds)));
$contactIds = array_values(array_filter($contactIds, static function (int $contactId): bool {
return $contactId > 0;
}));
if ($taskId <= 0 || $contactIds === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($contactIds), '?'));
$types = 'i' . str_repeat('i', count($contactIds));
$params = array_merge([$taskId], $contactIds);
return TaskCertDb::fetchAll(
'SELECT *
FROM task_assignment
WHERE task_id = ?
AND main_contact_id IN (' . $placeholders . ')
AND status IN ("open", "in_progress", "approval_pending", "overdue", "rejected", "completed", "expired")
ORDER BY id ASC',
$types,
$params
);
}
public function updateStatus(int $assignmentId, string $status, ?string $completedAt = null): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET status = ?, completed_at = ?, modified_at = NOW()
WHERE id = ?',
'ssi',
[$status, $completedAt, $assignmentId]
);
}
public function markInProgressIfOpen(int $assignmentId): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET status = "in_progress", modified_at = NOW()
WHERE id = ? AND status = "open"',
'i',
[$assignmentId]
);
}
public function setEscalationLevel(int $assignmentId, int $level): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET escalation_level = ?, last_escalated_at = NOW(), modified_at = NOW()
WHERE id = ?',
'ii',
[$level, $assignmentId]
);
}
public function clearEscalation(int $assignmentId): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET escalation_level = 0, last_escalated_at = NULL, modified_at = NOW()
WHERE id = ?',
'i',
[$assignmentId]
);
}
public function addEscalationEvent(int $assignmentId, int $level, string $eventType, array $snapshot): int
{
return TaskCertDb::insert(
'INSERT INTO task_escalation_event (assignment_id, escalation_level, event_type, snapshot_json)
VALUES (?, ?, ?, ?)',
'iiss',
[
$assignmentId,
$level,
$eventType,
json_encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]
);
}
public function findEscalationCandidates(?int $taskId = null): array
{
$sql = 'SELECT a.*, d.title AS task_title
FROM task_assignment a
INNER JOIN task_definition d ON d.id = a.task_id
WHERE d.active = 1
AND a.status IN ("open", "in_progress", "approval_pending", "overdue", "rejected")
AND a.due_at IS NOT NULL';
$types = '';
$params = [];
if ($taskId !== null && $taskId > 0) {
$sql .= ' AND a.task_id = ?';
$types = 'i';
$params[] = $taskId;
}
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function markOverdue(int $assignmentId, string $overdueAt): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET status = "overdue",
overdue_at = COALESCE(overdue_at, ?),
modified_at = NOW()
WHERE id = ?
AND status IN ("open", "in_progress", "approval_pending", "overdue", "rejected")',
'si',
[$overdueAt, $assignmentId]
);
}
public function clearOverdue(int $assignmentId): void
{
TaskCertDb::execute(
'UPDATE task_assignment
SET status = "in_progress",
overdue_at = NULL,
modified_at = NOW()
WHERE id = ?
AND status = "overdue"',
'i',
[$assignmentId]
);
}
public function resetByIds(array $assignmentIds): int
{
$assignmentIds = array_values(array_unique(array_map('intval', $assignmentIds)));
$assignmentIds = array_values(array_filter($assignmentIds, static function (int $assignmentId): bool {
return $assignmentId > 0;
}));
if ($assignmentIds === []) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($assignmentIds), '?'));
return TaskCertDb::execute(
'UPDATE task_assignment
SET status = "open",
completed_at = NULL,
overdue_at = NULL,
escalation_level = 0,
last_escalated_at = NULL,
modified_at = NOW()
WHERE id IN (' . $placeholders . ')',
str_repeat('i', count($assignmentIds)),
$assignmentIds
);
}
public function deleteByIds(array $assignmentIds): int
{
$assignmentIds = array_values(array_unique(array_map('intval', $assignmentIds)));
$assignmentIds = array_values(array_filter($assignmentIds, static function (int $assignmentId): bool {
return $assignmentId > 0;
}));
if ($assignmentIds === []) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($assignmentIds), '?'));
return TaskCertDb::execute(
'DELETE FROM task_assignment
WHERE id IN (' . $placeholders . ')',
str_repeat('i', count($assignmentIds)),
$assignmentIds
);
}
public function listTaskAssignmentSummary(int $taskId): array
{
$rows = TaskCertDb::fetchAll(
'SELECT status, COUNT(*) AS cnt
FROM task_assignment
WHERE task_id = ?
GROUP BY status',
'i',
[$taskId]
);
$summary = [
'open' => 0,
'in_progress' => 0,
'approval_pending' => 0,
'overdue' => 0,
'completed' => 0,
'expired' => 0,
'rejected' => 0,
];
foreach ($rows as $row) {
$status = (string) $row['status'];
if (!array_key_exists($status, $summary)) {
continue;
}
$summary[$status] = (int) $row['cnt'];
}
return $summary;
}
}