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:
999
module/tasks/repo/TaskMonitoringRepository.php
Normal file
999
module/tasks/repo/TaskMonitoringRepository.php
Normal file
@@ -0,0 +1,999 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user