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>
515 lines
16 KiB
PHP
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;
|
|
}
|
|
}
|