Files
awo-hamburg-intranet/module/tasks/repo/TaskMonitoringRepository.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

1000 lines
32 KiB
PHP

<?php
declare(strict_types=1);
class TaskMonitoringRepository
{
public function listCycleKeysForTask(int $taskId, int $limit = 24): array
{
$limit = max(1, min(200, $limit));
return TaskCertDb::fetchAll(
'SELECT
a.cycle_key,
MIN(a.cycle_start_at) AS cycle_start_at,
MAX(a.cycle_end_at) AS cycle_end_at,
COUNT(*) AS assignment_count
FROM task_assignment a
WHERE a.task_id = ?
AND a.cycle_key <> ""
GROUP BY a.cycle_key
ORDER BY COALESCE(MAX(a.cycle_start_at), MAX(a.assigned_at)) DESC, a.cycle_key DESC
LIMIT ' . $limit,
'i',
[$taskId]
);
}
public function findCycleKeyForTask(int $taskId, string $cycleKey): ?array
{
$cycleKey = trim($cycleKey);
if ($taskId <= 0 || $cycleKey === '') {
return null;
}
return TaskCertDb::fetchOne(
'SELECT
a.cycle_key,
MIN(a.cycle_start_at) AS cycle_start_at,
MAX(a.cycle_end_at) AS cycle_end_at,
COUNT(*) AS assignment_count
FROM task_assignment a
WHERE a.task_id = ?
AND a.cycle_key = ?
GROUP BY a.cycle_key
LIMIT 1',
'is',
[$taskId, $cycleKey]
);
}
public function getKpisForTaskLatest(int $taskId): array
{
$statusRows = TaskCertDb::fetchAll(
'SELECT a.status, COUNT(*) AS cnt
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
GROUP BY a.status',
'i',
[$taskId]
);
$statusCounts = [
'open' => 0,
'in_progress' => 0,
'approval_pending' => 0,
'overdue' => 0,
'completed' => 0,
'expired' => 0,
'rejected' => 0,
];
foreach ($statusRows as $row) {
$status = (string) ($row['status'] ?? '');
if (!array_key_exists($status, $statusCounts)) {
continue;
}
$statusCounts[$status] = (int) ($row['cnt'] ?? 0);
}
$totalAssigned = array_sum($statusCounts);
$completedCount = (int) $statusCounts['completed'];
$todoCount = $totalAssigned - $completedCount;
$awaitingApprovalCount = (int) TaskCertDb::scalar(
'SELECT COUNT(*)
FROM task_submission s
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = s.assignment_id
WHERE s.status = "awaiting_approval"',
'i',
[$taskId]
);
$escalatedCount = (int) TaskCertDb::scalar(
'SELECT COUNT(*)
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
WHERE a.escalation_level > 0',
'i',
[$taskId]
);
$withSubmissionCount = (int) TaskCertDb::scalar(
'SELECT COUNT(DISTINCT s.assignment_id)
FROM task_submission s
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = s.assignment_id',
'i',
[$taskId]
);
$completionRate = $totalAssigned > 0
? round(($completedCount / $totalAssigned) * 100, 1)
: 0.0;
return [
'total_assigned' => $totalAssigned,
'completed_count' => $completedCount,
'todo_count' => $todoCount,
'overdue_count' => (int) $statusCounts['overdue'],
'awaiting_approval_count' => $awaitingApprovalCount,
'escalated_count' => $escalatedCount,
'with_submission_count' => $withSubmissionCount,
'completion_rate' => $completionRate,
'status_counts' => $statusCounts,
];
}
public function listTodoAssignmentsLatest(int $taskId, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$where = [
'a.status <> "completed"',
];
$types = 'i';
$params = [$taskId];
$status = (string) ($filters['status'] ?? '');
if ($status !== '') {
$where[] = 'a.status = ?';
$types .= 's';
$params[] = $status;
}
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$sql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
ls.last_submitted_at
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
LEFT JOIN main_contact c ON c.id = a.main_contact_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 ' . implode(' AND ', $where) . '
ORDER BY
CASE a.status
WHEN "overdue" THEN 1
WHEN "approval_pending" THEN 2
WHEN "open" THEN 3
WHEN "in_progress" THEN 4
WHEN "rejected" THEN 5
WHEN "expired" THEN 6
ELSE 7
END,
a.due_at IS NULL,
a.due_at ASC,
a.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listCompletedAssignmentsLatest(int $taskId, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$status = (string) ($filters['status'] ?? '');
if ($status !== '' && $status !== 'completed') {
return [];
}
$where = [
'a.status = "completed"',
];
$types = 'i';
$params = [$taskId];
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$sql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
tc.id AS certificate_id,
tc.status AS certificate_status,
tc.valid_from AS certificate_valid_from,
tc.valid_until AS certificate_valid_until,
tc.certificate_code
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
LEFT JOIN task_certificate tc ON tc.assignment_id = a.id
WHERE ' . implode(' AND ', $where) . '
ORDER BY a.completed_at DESC, a.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listApprovalAndOverdueLatest(int $taskId, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$query = trim((string) ($filters['q'] ?? ''));
$methodType = trim((string) ($filters['method_type'] ?? ''));
$submissionStatus = trim((string) ($filters['submission_status'] ?? ''));
$status = trim((string) ($filters['status'] ?? ''));
$approvalWhere = [
's.status = "awaiting_approval"',
];
$approvalTypes = 'i';
$approvalParams = [$taskId];
if ($methodType !== '') {
$approvalWhere[] = 'm.method_type = ?';
$approvalTypes .= 's';
$approvalParams[] = $methodType;
}
if ($submissionStatus !== '') {
$approvalWhere[] = 's.status = ?';
$approvalTypes .= 's';
$approvalParams[] = $submissionStatus;
}
if ($status !== '') {
$approvalWhere[] = 'a.status = ?';
$approvalTypes .= 's';
$approvalParams[] = $status;
}
if ($query !== '') {
$like = '%' . $query . '%';
$approvalWhere[] = '(c.name LIKE ? OR c.email LIKE ?)';
$approvalTypes .= 'ss';
$approvalParams[] = $like;
$approvalParams[] = $like;
}
$approvalSql = 'SELECT
s.*,
a.status AS assignment_status,
a.escalation_level,
a.due_at,
a.overdue_at,
a.cycle_key,
c.name AS contact_name,
c.email AS contact_email,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE ' . implode(' AND ', $approvalWhere) . '
ORDER BY s.submitted_at ASC, s.id ASC
LIMIT ' . $limit;
$overdueWhere = [
'a.status <> "completed"',
'(a.status = "overdue" OR a.escalation_level > 0)',
];
$overdueTypes = 'i';
$overdueParams = [$taskId];
if ($status !== '') {
$overdueWhere[] = 'a.status = ?';
$overdueTypes .= 's';
$overdueParams[] = $status;
}
if ($query !== '') {
$like = '%' . $query . '%';
$overdueWhere[] = '(c.name LIKE ? OR c.email LIKE ?)';
$overdueTypes .= 'ss';
$overdueParams[] = $like;
$overdueParams[] = $like;
}
$overdueSql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
ls.last_submitted_at
FROM task_assignment a
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
LEFT JOIN main_contact c ON c.id = a.main_contact_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 ' . implode(' AND ', $overdueWhere) . '
ORDER BY a.escalation_level DESC, a.due_at ASC, a.id DESC
LIMIT ' . $limit;
return [
'approvals' => TaskCertDb::fetchAll($approvalSql, $approvalTypes, $approvalParams),
'overdue' => TaskCertDb::fetchAll($overdueSql, $overdueTypes, $overdueParams),
];
}
public function listSubmissionAuditLatest(int $taskId, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$where = [];
$types = 'i';
$params = [$taskId];
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$methodType = trim((string) ($filters['method_type'] ?? ''));
if ($methodType !== '') {
$where[] = 'm.method_type = ?';
$types .= 's';
$params[] = $methodType;
}
$submissionStatus = trim((string) ($filters['submission_status'] ?? ''));
if ($submissionStatus !== '') {
$where[] = 's.status = ?';
$types .= 's';
$params[] = $submissionStatus;
}
$status = trim((string) ($filters['status'] ?? ''));
if ($status !== '') {
$where[] = 'a.status = ?';
$types .= 's';
$params[] = $status;
}
$whereClause = $where === [] ? '' : (' AND ' . implode(' AND ', $where));
$sql = 'SELECT
s.*,
a.status AS assignment_status,
a.cycle_key,
a.due_at,
a.overdue_at,
a.completed_at,
a.escalation_level,
c.name AS contact_name,
c.email AS contact_email,
m.id AS method_id,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE 1=1' . $whereClause . '
ORDER BY s.submitted_at DESC, s.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listSubmissionAuditLatestForExport(int $taskId): array
{
return TaskCertDb::fetchAll(
'SELECT
s.*,
a.status AS assignment_status,
a.cycle_key,
a.due_at,
a.overdue_at,
a.completed_at,
a.escalation_level,
c.name AS contact_name,
c.email AS contact_email,
m.id AS method_id,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a.id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
ORDER BY s.submitted_at DESC, s.id DESC',
'i',
[$taskId]
);
}
public function listMethodCompletionStatsLatest(int $taskId, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(200, $limit));
$where = [
'm.task_id = ?',
'm.active = 1',
];
$whereTypes = 'i';
$whereParams = [$taskId];
$methodType = trim((string) ($filters['method_type'] ?? ''));
if ($methodType !== '') {
$where[] = 'm.method_type = ?';
$whereTypes .= 's';
$whereParams[] = $methodType;
}
$joinTypes = '';
$joinParams = [];
$query = trim((string) ($filters['q'] ?? ''));
$assignmentContactFilter = '';
if ($query !== '') {
$like = '%' . $query . '%';
$assignmentContactFilter = ' AND EXISTS (
SELECT 1
FROM main_contact c
WHERE c.id = a.main_contact_id
AND (c.name LIKE ? OR c.email LIKE ?)
)';
$joinTypes .= 'ss';
$joinParams[] = $like;
$joinParams[] = $like;
}
$sql = 'SELECT
m.id AS method_id,
m.method_type,
m.is_required,
m.sort_order,
COUNT(DISTINCT a.id) AS assignment_count,
SUM(
CASE
WHEN a.id IS NULL THEN 0
WHEN EXISTS (
SELECT 1
FROM task_submission s2
WHERE s2.assignment_id = a.id
AND s2.method_id = m.id
AND s2.status IN ("auto_passed", "approved")
) THEN 1
ELSE 0
END
) AS fulfilled_count
FROM task_method m
LEFT JOIN (
SELECT a1.*
FROM task_assignment a1
INNER JOIN (
SELECT main_contact_id, MAX(id) AS latest_id
FROM task_assignment
WHERE task_id = ?
GROUP BY main_contact_id
) latest ON latest.latest_id = a1.id
) a ON a.task_id = m.task_id' . $assignmentContactFilter . '
WHERE ' . implode(' AND ', $where) . '
GROUP BY m.id, m.method_type, m.is_required, m.sort_order
ORDER BY m.sort_order ASC, m.id ASC
LIMIT ' . $limit;
// Placeholder order: derived latest task_id, JOIN filters, WHERE filters.
$types = 'i' . $joinTypes . $whereTypes;
$params = array_merge([$taskId], $joinParams, $whereParams);
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function getKpisForTaskCycle(int $taskId, string $cycleKey): array
{
$statusRows = TaskCertDb::fetchAll(
'SELECT a.status, COUNT(*) AS cnt
FROM task_assignment a
WHERE a.task_id = ?
AND a.cycle_key = ?
GROUP BY a.status',
'is',
[$taskId, $cycleKey]
);
$statusCounts = [
'open' => 0,
'in_progress' => 0,
'approval_pending' => 0,
'overdue' => 0,
'completed' => 0,
'expired' => 0,
'rejected' => 0,
];
foreach ($statusRows as $row) {
$status = (string) ($row['status'] ?? '');
if (!array_key_exists($status, $statusCounts)) {
continue;
}
$statusCounts[$status] = (int) ($row['cnt'] ?? 0);
}
$totalAssigned = array_sum($statusCounts);
$completedCount = (int) $statusCounts['completed'];
$todoCount = $totalAssigned - $completedCount;
$awaitingApprovalCount = (int) TaskCertDb::scalar(
'SELECT COUNT(*)
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
WHERE a.task_id = ?
AND a.cycle_key = ?
AND s.status = "awaiting_approval"',
'is',
[$taskId, $cycleKey]
);
$escalatedCount = (int) TaskCertDb::scalar(
'SELECT COUNT(*)
FROM task_assignment a
WHERE a.task_id = ?
AND a.cycle_key = ?
AND a.escalation_level > 0',
'is',
[$taskId, $cycleKey]
);
$withSubmissionCount = (int) TaskCertDb::scalar(
'SELECT COUNT(DISTINCT s.assignment_id)
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
WHERE a.task_id = ?
AND a.cycle_key = ?',
'is',
[$taskId, $cycleKey]
);
$completionRate = $totalAssigned > 0
? round(($completedCount / $totalAssigned) * 100, 1)
: 0.0;
return [
'total_assigned' => $totalAssigned,
'completed_count' => $completedCount,
'todo_count' => $todoCount,
'overdue_count' => (int) $statusCounts['overdue'],
'awaiting_approval_count' => $awaitingApprovalCount,
'escalated_count' => $escalatedCount,
'with_submission_count' => $withSubmissionCount,
'completion_rate' => $completionRate,
'status_counts' => $statusCounts,
];
}
public function listTodoAssignments(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$where = [
'a.task_id = ?',
'a.cycle_key = ?',
'a.status <> "completed"',
];
$types = 'is';
$params = [$taskId, $cycleKey];
$status = (string) ($filters['status'] ?? '');
if ($status !== '') {
$where[] = 'a.status = ?';
$types .= 's';
$params[] = $status;
}
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$sql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
ls.last_submitted_at
FROM task_assignment a
LEFT JOIN main_contact c ON c.id = a.main_contact_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 ' . implode(' AND ', $where) . '
ORDER BY
CASE a.status
WHEN "overdue" THEN 1
WHEN "approval_pending" THEN 2
WHEN "open" THEN 3
WHEN "in_progress" THEN 4
WHEN "rejected" THEN 5
WHEN "expired" THEN 6
ELSE 7
END,
a.due_at IS NULL,
a.due_at ASC,
a.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listCompletedAssignments(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$status = (string) ($filters['status'] ?? '');
if ($status !== '' && $status !== 'completed') {
return [];
}
$where = [
'a.task_id = ?',
'a.cycle_key = ?',
'a.status = "completed"',
];
$types = 'is';
$params = [$taskId, $cycleKey];
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$sql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
tc.id AS certificate_id,
tc.status AS certificate_status,
tc.valid_from AS certificate_valid_from,
tc.valid_until AS certificate_valid_until,
tc.certificate_code
FROM task_assignment a
LEFT JOIN main_contact c ON c.id = a.main_contact_id
LEFT JOIN task_certificate tc ON tc.assignment_id = a.id
WHERE ' . implode(' AND ', $where) . '
ORDER BY a.completed_at DESC, a.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listApprovalAndOverdue(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$query = trim((string) ($filters['q'] ?? ''));
$methodType = trim((string) ($filters['method_type'] ?? ''));
$submissionStatus = trim((string) ($filters['submission_status'] ?? ''));
$status = trim((string) ($filters['status'] ?? ''));
$approvalWhere = [
'a.task_id = ?',
'a.cycle_key = ?',
's.status = "awaiting_approval"',
];
$approvalTypes = 'is';
$approvalParams = [$taskId, $cycleKey];
if ($methodType !== '') {
$approvalWhere[] = 'm.method_type = ?';
$approvalTypes .= 's';
$approvalParams[] = $methodType;
}
if ($submissionStatus !== '') {
$approvalWhere[] = 's.status = ?';
$approvalTypes .= 's';
$approvalParams[] = $submissionStatus;
}
if ($status !== '') {
$approvalWhere[] = 'a.status = ?';
$approvalTypes .= 's';
$approvalParams[] = $status;
}
if ($query !== '') {
$like = '%' . $query . '%';
$approvalWhere[] = '(c.name LIKE ? OR c.email LIKE ?)';
$approvalTypes .= 'ss';
$approvalParams[] = $like;
$approvalParams[] = $like;
}
$approvalSql = 'SELECT
s.*,
a.status AS assignment_status,
a.escalation_level,
a.due_at,
a.overdue_at,
a.cycle_key,
c.name AS contact_name,
c.email AS contact_email,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE ' . implode(' AND ', $approvalWhere) . '
ORDER BY s.submitted_at ASC, s.id ASC
LIMIT ' . $limit;
$overdueWhere = [
'a.task_id = ?',
'a.cycle_key = ?',
'a.status <> "completed"',
'(a.status = "overdue" OR a.escalation_level > 0)',
];
$overdueTypes = 'is';
$overdueParams = [$taskId, $cycleKey];
if ($status !== '') {
$overdueWhere[] = 'a.status = ?';
$overdueTypes .= 's';
$overdueParams[] = $status;
}
if ($query !== '') {
$like = '%' . $query . '%';
$overdueWhere[] = '(c.name LIKE ? OR c.email LIKE ?)';
$overdueTypes .= 'ss';
$overdueParams[] = $like;
$overdueParams[] = $like;
}
$overdueSql = 'SELECT
a.*,
c.name AS contact_name,
c.email AS contact_email,
ls.last_submitted_at
FROM task_assignment a
LEFT JOIN main_contact c ON c.id = a.main_contact_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 ' . implode(' AND ', $overdueWhere) . '
ORDER BY a.escalation_level DESC, a.due_at ASC, a.id DESC
LIMIT ' . $limit;
return [
'approvals' => TaskCertDb::fetchAll($approvalSql, $approvalTypes, $approvalParams),
'overdue' => TaskCertDb::fetchAll($overdueSql, $overdueTypes, $overdueParams),
];
}
public function listSubmissionAudit(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(500, $limit));
$where = [
'a.task_id = ?',
'a.cycle_key = ?',
];
$types = 'is';
$params = [$taskId, $cycleKey];
$query = trim((string) ($filters['q'] ?? ''));
if ($query !== '') {
$like = '%' . $query . '%';
$where[] = '(c.name LIKE ? OR c.email LIKE ?)';
$types .= 'ss';
$params[] = $like;
$params[] = $like;
}
$methodType = trim((string) ($filters['method_type'] ?? ''));
if ($methodType !== '') {
$where[] = 'm.method_type = ?';
$types .= 's';
$params[] = $methodType;
}
$submissionStatus = trim((string) ($filters['submission_status'] ?? ''));
if ($submissionStatus !== '') {
$where[] = 's.status = ?';
$types .= 's';
$params[] = $submissionStatus;
}
$status = trim((string) ($filters['status'] ?? ''));
if ($status !== '') {
$where[] = 'a.status = ?';
$types .= 's';
$params[] = $status;
}
$sql = 'SELECT
s.*,
a.status AS assignment_status,
a.cycle_key,
a.due_at,
a.overdue_at,
a.completed_at,
a.escalation_level,
c.name AS contact_name,
c.email AS contact_email,
m.id AS method_id,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE ' . implode(' AND ', $where) . '
ORDER BY s.submitted_at DESC, s.id DESC
LIMIT ' . $limit;
return TaskCertDb::fetchAll($sql, $types, $params);
}
public function listSubmissionAuditForCycleExport(int $taskId, string $cycleKey): array
{
return TaskCertDb::fetchAll(
'SELECT
s.*,
a.status AS assignment_status,
a.cycle_key,
a.due_at,
a.overdue_at,
a.completed_at,
a.escalation_level,
c.name AS contact_name,
c.email AS contact_email,
m.id AS method_id,
m.method_type
FROM task_submission s
INNER JOIN task_assignment a ON a.id = s.assignment_id
INNER JOIN task_method m ON m.id = s.method_id
LEFT JOIN main_contact c ON c.id = a.main_contact_id
WHERE a.task_id = ?
AND a.cycle_key = ?
ORDER BY s.submitted_at DESC, s.id DESC',
'is',
[$taskId, $cycleKey]
);
}
public function listMethodCompletionStats(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array
{
$limit = max(1, min(200, $limit));
$where = [
'm.task_id = ?',
'm.active = 1',
];
$whereTypes = 'i';
$whereParams = [$taskId];
$methodType = trim((string) ($filters['method_type'] ?? ''));
if ($methodType !== '') {
$where[] = 'm.method_type = ?';
$whereTypes .= 's';
$whereParams[] = $methodType;
}
$joinTypes = '';
$joinParams = [];
$query = trim((string) ($filters['q'] ?? ''));
$assignmentContactFilter = '';
if ($query !== '') {
$like = '%' . $query . '%';
$assignmentContactFilter = ' AND EXISTS (
SELECT 1
FROM main_contact c
WHERE c.id = a.main_contact_id
AND (c.name LIKE ? OR c.email LIKE ?)
)';
$joinTypes .= 'ss';
$joinParams[] = $like;
$joinParams[] = $like;
}
$sql = 'SELECT
m.id AS method_id,
m.method_type,
m.is_required,
m.sort_order,
COUNT(DISTINCT a.id) AS assignment_count,
SUM(
CASE
WHEN a.id IS NULL THEN 0
WHEN EXISTS (
SELECT 1
FROM task_submission s2
WHERE s2.assignment_id = a.id
AND s2.method_id = m.id
AND s2.status IN ("auto_passed", "approved")
) THEN 1
ELSE 0
END
) AS fulfilled_count
FROM task_method m
LEFT JOIN task_assignment a
ON a.task_id = m.task_id
AND a.cycle_key = ?' . $assignmentContactFilter . '
WHERE ' . implode(' AND ', $where) . '
GROUP BY m.id, m.method_type, m.is_required, m.sort_order
ORDER BY m.sort_order ASC, m.id ASC
LIMIT ' . $limit;
// Placeholder order: cycle key, JOIN filters, WHERE filters.
$types = 's' . $joinTypes . $whereTypes;
$params = array_merge([$cycleKey], $joinParams, $whereParams);
return TaskCertDb::fetchAll($sql, $types, $params);
}
}