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>
1000 lines
32 KiB
PHP
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);
|
|
}
|
|
}
|