<?php
namespace ApplicationBundle\Controller;
use ApplicationBundle\Interfaces\SessionCheckInterface;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Request;
use ApplicationBundle\Service\NotificationService;
class TaskDashboardController extends GenericController implements SessionCheckInterface
{
private function getCompanyId(Request $request): int
{
return (int) $request->getSession()->get(UserConstants::USER_COMPANY_ID, 0);
}
private function getEmployeeId(Request $request): int
{
return (int) $request->getSession()->get(UserConstants::USER_EMPLOYEE_ID, 0);
}
private function isAdminOverride(Request $request): bool
{
$session = $request->getSession();
return
(int) $session->get(UserConstants::USER_TYPE) === UserConstants::USER_TYPE_SYSTEM ||
(int) $session->get(UserConstants::IS_BUDDYBEE_ADMIN, 0) === 1;
}
private function isManagerScope(Request $request): bool
{
$session = $request->getSession();
return
$this->isAdminOverride($request) ||
(int) $session->get(UserConstants::USER_TYPE) === UserConstants::USER_TYPE_MANAGEMENT_USER;
}
private function notificationService(): NotificationService
{
return $this->get('app.notification_service');
}
private function buildTaskAlerts(Request $request, string $dashboardType): array
{
$companyId = $this->getCompanyId($request);
$employeeId = $this->getEmployeeId($request);
$service = $this->notificationService();
$alerts = [];
if ($dashboardType === 'reviewer') {
$outWarning = $service->getUserTaskOutWarnings($employeeId, $companyId);
$reviewerPending = $service->getReviewerPendingTasks($employeeId, $companyId);
$overdue = $service->getOverdueReviews($companyId, '/tasks/review?filter=overdue');
foreach ([$overdue, $reviewerPending, $outWarning] as $alert) {
if (!empty($alert['hasAlert'])) {
$alerts[] = $alert;
}
}
} elseif ($dashboardType === 'my') {
$outWarning = $service->getUserTaskOutWarnings($employeeId, $companyId);
$rejected = $service->getRejectedTasks($employeeId, $companyId);
foreach ([$rejected, $outWarning] as $alert) {
if (!empty($alert['hasAlert'])) {
$alerts[] = $alert;
}
}
} elseif ($dashboardType === 'manager') {
$overdue = $service->getOverdueReviews($companyId, '/tasks/manager?filter=overdue');
if (!empty($overdue['hasAlert'])) {
$alerts[] = $overdue;
}
}
usort($alerts, static function (array $left, array $right): int {
$priorityCompare = ((int) ($left['priority'] ?? 99)) <=> ((int) ($right['priority'] ?? 99));
if ($priorityCompare !== 0) {
return $priorityCompare;
}
return ((int) ($right['count'] ?? 0)) <=> ((int) ($left['count'] ?? 0));
});
return array_slice($alerts, 0, 3);
}
private function getDashboardFilter(Request $request, string $dashboardType): string
{
$filter = trim((string) $request->query->get('filter', ''));
$allowed = [
'reviewer' => ['overdue', 'pending'],
'my' => ['active', 'rejected'],
'manager' => ['overdue', 'non_compliant'],
];
if (!isset($allowed[$dashboardType])) {
return '';
}
if ($filter === '' || !in_array($filter, $allowed[$dashboardType], true)) {
return '';
}
return $filter;
}
private function applyDashboardFilter(array $rows, string $dashboardType, string $filter): array
{
if ($filter === '') {
return $rows;
}
return array_values(array_filter($rows, static function (array $row) use ($dashboardType, $filter): bool {
$state = (string) ($row['currentState'] ?? '');
$submittedAgeHours = (int) ($row['submittedAgeHours'] ?? 0);
if ($dashboardType === 'reviewer') {
if ($filter === 'overdue') {
return $submittedAgeHours >= 48;
}
return $state === 'submitted';
}
if ($dashboardType === 'my') {
if ($filter === 'rejected') {
return $state === 'rejected';
}
return in_array($state, ['created', 'in_progress'], true);
}
if ($dashboardType === 'manager') {
if ($filter === 'non_compliant') {
return false;
}
return $submittedAgeHours >= 48;
}
return true;
}));
}
private function summarizeAlerts(array $alerts): array
{
return $alerts;
}
private function formatHoursSince($submittedAt): string
{
if (empty($submittedAt)) {
return 'N/A';
}
try {
$submitted = new \DateTime((string) $submittedAt);
} catch (\Exception $e) {
return 'N/A';
}
$diff = (new \DateTime())->diff($submitted);
$hours = ((int) $diff->days * 24) + (int) $diff->h;
if ($hours >= 48) {
return round($hours / 24, 1) . 'd';
}
return $hours . 'h';
}
private function fetchLatestLogMap(Connection $conn, array $planningItemIds, ?string $logType = null): array
{
$planningItemIds = array_values(array_unique(array_filter(array_map('intval', $planningItemIds))));
if (empty($planningItemIds)) {
return [];
}
$sql = 'SELECT t.* FROM task_log t WHERE t.planning_item_id IN (' . implode(',', $planningItemIds) . ')';
if ($logType !== null) {
$sql .= ' AND t.log_type = ' . $conn->quote($logType);
}
$sql .= ' ORDER BY t.id DESC';
$rows = $conn->fetchAllAssociative($sql);
$map = [];
foreach ($rows as $row) {
$planningItemId = (int) ($row['planning_item_id'] ?? 0);
if ($planningItemId === 0 || isset($map[$planningItemId])) {
continue;
}
$map[$planningItemId] = $row;
}
return $map;
}
private function baseTaskQuery(Connection $conn, int $companyId)
{
$qb = $conn->createQueryBuilder();
$qb->from('planning_item', 'p')
->leftJoin('p', 'employee', 'e', 'e.employee_id = p.assigned_to AND p.assigned_to_type = 1')
->leftJoin('p', 'sys_user', 'u', 'u.user_id = p.assigned_to AND p.assigned_to_type = 2')
->leftJoin('p', 'employee', 'r', 'r.employee_id = p.reviewer_id')
->leftJoin('p', 'project', 'pr', 'pr.project_id = p.project_id')
->select(
'p.id AS planningItemId',
'p.item_alias AS taskTitle',
'p.project_id AS projectId',
'p.task_context_type AS taskContextType',
'p.current_state AS currentState',
'p.review_status AS reviewStatus',
'p.reviewer_id AS reviewerId',
'p.assigned_to AS assignedTo',
'p.assigned_to_type AS assignedToType',
'p.completion_percentage AS completionPercentage',
'p.submitted_at AS submittedAt',
'p.approved_at AS approvedAt',
'p.rejected_at AS rejectedAt',
'p.expected_output AS expectedOutput',
'p.kpi_counted AS kpiCounted',
'COALESCE(NULLIF(pr.project_name, \'\'), \'General\') AS projectName',
'COALESCE(NULLIF(e.name, \'\'), NULLIF(u.name, \'\'), CONCAT(COALESCE(e.first_name, \'\'), \' \', COALESCE(e.last_name, \'\')), CONCAT(\'User #\', p.assigned_to)) AS assignedName',
'COALESCE(NULLIF(r.name, \'\'), CONCAT(COALESCE(r.first_name, \'\'), \' \', COALESCE(r.last_name, \'\')), CONCAT(\'Employee #\', p.reviewer_id)) AS reviewerName',
'TIMESTAMPDIFF(HOUR, p.submitted_at, NOW()) AS submittedAgeHours'
)
->where('p.company_id = :companyId')
->setParameter('companyId', $companyId);
return $qb;
}
private function decorateRows(array $rows, array $latestLogs = []): array
{
foreach ($rows as &$row) {
$planningItemId = (int) ($row['planningItemId'] ?? 0);
$log = $latestLogs[$planningItemId] ?? [];
$row['submissionEvidenceExists'] = !empty(trim((string) ($log['evidence_files'] ?? '')));
$row['submissionWorkCompleted'] = (string) ($log['work_completed'] ?? '');
$row['submissionEvidenceFiles'] = (string) ($log['evidence_files'] ?? '');
$row['submissionEvidenceNote'] = (string) ($log['evidence_note'] ?? '');
$row['submissionBlockerDetail'] = (string) ($log['blocker_detail'] ?? '');
$row['submissionNextAction'] = (string) ($log['next_action'] ?? '');
$row['submissionReviewDecision'] = (string) ($log['review_decision'] ?? '');
$row['submissionReviewComment'] = (string) ($log['review_comment'] ?? '');
$row['latestActivityType'] = (string) ($log['log_type'] ?? '');
$row['latestActivityAt'] = (string) ($log['created_at'] ?? '');
$row['latestActivityLabel'] = ucfirst(str_replace('_', ' ', (string) ($log['log_type'] ?? 'session')));
$row['submittedAgeLabel'] = $this->formatHoursSince($row['submittedAt'] ?? null);
$row['stateLabel'] = $this->buildStateLabel($row);
$row['stateClass'] = $this->buildStateClass($row);
}
unset($row);
return $rows;
}
private function buildStateLabel(array $row): string
{
$state = (string) ($row['currentState'] ?? '');
$review = (string) ($row['reviewStatus'] ?? '');
$progress = (int) ($row['completionPercentage'] ?? 0);
if ($state === 'approved' || $review === 'approved' || (int) ($row['kpiCounted'] ?? 0) === 1) {
return 'KPI Counted';
}
if ($state === 'submitted' || $review === 'pending') {
return 'Work Finished (Pending Approval)';
}
if ($state === 'rejected' || $review === 'rejected') {
return 'Rework Needed';
}
if ($progress >= 100) {
return 'Work Finished (Pending Approval)';
}
if ($state === 'in_progress') {
return 'In Progress';
}
return 'Created';
}
private function buildStateClass(array $row): string
{
$state = (string) ($row['currentState'] ?? '');
$review = (string) ($row['reviewStatus'] ?? '');
$progress = (int) ($row['completionPercentage'] ?? 0);
if ($state === 'approved' || $review === 'approved' || (int) ($row['kpiCounted'] ?? 0) === 1) {
return 'is-approved';
}
if ($state === 'submitted' || $review === 'pending' || $progress >= 100) {
return 'is-submitted';
}
if ($state === 'rejected' || $review === 'rejected') {
return 'is-rejected';
}
return 'is-progress';
}
public function reviewerDashboardAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
$companyId = $this->getCompanyId($request);
$currentEmployeeId = $this->getEmployeeId($request);
$isOverride = $this->isAdminOverride($request);
$qb = $this->baseTaskQuery($conn, $companyId)
->andWhere('p.current_state = :submittedState')
->andWhere('p.review_status = :pendingReview')
->setParameter('submittedState', 'submitted')
->setParameter('pendingReview', 'pending')
->orderBy('p.submitted_at', 'ASC')
->addOrderBy('p.id', 'DESC');
if (!$isOverride) {
$qb->andWhere('p.reviewer_id = :reviewerId')
->setParameter('reviewerId', $currentEmployeeId);
}
$tasks = $qb->executeQuery()->fetchAllAssociative();
$latestLogs = $this->fetchLatestLogMap($conn, array_column($tasks, 'planningItemId'), 'submission');
$tasks = $this->decorateRows($tasks, $latestLogs);
$filter = $this->getDashboardFilter($request, 'reviewer');
$tasks = $this->applyDashboardFilter($tasks, 'reviewer', $filter);
foreach ($tasks as &$task) {
$task['isOverdueReview'] = (int) ($task['submittedAgeHours'] ?? 0) >= 48;
}
unset($task);
return $this->render('@Application/pages/tasks/dashboard_reviewer.html.twig', [
'page_title' => 'Reviewer Dashboard',
'currentEmployeeId' => $currentEmployeeId,
'isAdminOverride' => $isOverride,
'activeFilter' => $filter,
'tasks' => $tasks,
'pendingCount' => count($tasks),
'overdueCount' => count(array_filter($tasks, static function ($row) {
return !empty($row['isOverdueReview']);
})),
'taskAlerts' => $this->buildTaskAlerts($request, 'reviewer'),
]);
}
public function myTasksDashboardAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
$companyId = $this->getCompanyId($request);
$currentEmployeeId = $this->getEmployeeId($request);
$qb = $this->baseTaskQuery($conn, $companyId)
->andWhere('p.assigned_to = :assignedTo')
->andWhere('p.assigned_to_type = 1')
->setParameter('assignedTo', $currentEmployeeId)
->orderBy('p.updated_at', 'DESC')
->addOrderBy('p.id', 'DESC');
$tasks = $qb->executeQuery()->fetchAllAssociative();
$latestLogs = $this->fetchLatestLogMap($conn, array_column($tasks, 'planningItemId'));
$tasks = $this->decorateRows($tasks, $latestLogs);
$filter = $this->getDashboardFilter($request, 'my');
$tasks = $this->applyDashboardFilter($tasks, 'my', $filter);
$sections = [
'in_progress' => [],
'submitted' => [],
'rejected' => [],
'approved' => [],
];
foreach ($tasks as $task) {
$state = (string) ($task['currentState'] ?? 'created');
if (!isset($sections[$state])) {
$sections['in_progress'][] = $task;
continue;
}
$sections[$state][] = $task;
}
return $this->render('@Application/pages/tasks/dashboard_my_tasks.html.twig', [
'page_title' => 'My Tasks',
'currentEmployeeId' => $currentEmployeeId,
'activeFilter' => $filter,
'sections' => $sections,
'totalTasks' => count($tasks),
'taskAlerts' => $this->buildTaskAlerts($request, 'my'),
]);
}
public function managerDashboardAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
$companyId = $this->getCompanyId($request);
$currentEmployeeId = $this->getEmployeeId($request);
$isOverride = $this->isAdminOverride($request);
$pendingQb = $this->baseTaskQuery($conn, $companyId)
->andWhere('p.current_state = :submittedState')
->andWhere('p.review_status = :pendingReview')
->setParameter('submittedState', 'submitted')
->setParameter('pendingReview', 'pending')
->orderBy('p.submitted_at', 'ASC')
->addOrderBy('p.id', 'DESC');
$pendingTasks = $pendingQb->executeQuery()->fetchAllAssociative();
$pendingLogs = $this->fetchLatestLogMap($conn, array_column($pendingTasks, 'planningItemId'), 'submission');
$pendingTasks = $this->decorateRows($pendingTasks, $pendingLogs);
$managerFilter = $this->getDashboardFilter($request, 'manager');
$pendingTasks = $this->applyDashboardFilter($pendingTasks, 'manager', $managerFilter);
foreach ($pendingTasks as &$task) {
$task['isOverdueReview'] = (int) ($task['submittedAgeHours'] ?? 0) >= 48;
}
unset($task);
$todayStart = (new \DateTime('today'))->format('Y-m-d 00:00:00');
$todayEnd = (new \DateTime('today'))->format('Y-m-d 23:59:59');
$todayStartEpoch = (new \DateTime('today'))->getTimestamp();
$nowEpoch = time();
$fortyEightHoursAgo = (new \DateTime('-48 hours'))->format('Y-m-d H:i:s');
$submittedToday = (int) $conn->fetchOne(
'SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND submitted_at BETWEEN :startTs AND :endTs',
[
'companyId' => $companyId,
'startTs' => $todayStart,
'endTs' => $todayEnd,
]
);
$approvedToday = (int) $conn->fetchOne(
'SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND approved_at BETWEEN :startTs AND :endTs',
[
'companyId' => $companyId,
'startTs' => $todayStart,
'endTs' => $todayEnd,
]
);
$rejectedToday = (int) $conn->fetchOne(
'SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND rejected_at BETWEEN :startTs AND :endTs',
[
'companyId' => $companyId,
'startTs' => $todayStart,
'endTs' => $todayEnd,
]
);
$pendingReviews = (int) $conn->fetchOne(
"SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND current_state = 'submitted' AND review_status = 'pending'",
['companyId' => $companyId]
);
$overdueReviews = (int) $conn->fetchOne(
"SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND current_state = 'submitted' AND review_status = 'pending' AND submitted_at <= :deadline",
[
'companyId' => $companyId,
'deadline' => $fortyEightHoursAgo,
]
);
$todayInProgress = (int) $conn->fetchOne(
"SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND current_state IN ('created', 'in_progress')",
['companyId' => $companyId]
);
$systemSubmitted = (int) $conn->fetchOne(
"SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND current_state = 'submitted'",
['companyId' => $companyId]
);
$systemRejected = (int) $conn->fetchOne(
"SELECT COUNT(*) FROM planning_item WHERE company_id = :companyId AND current_state = 'rejected'",
['companyId' => $companyId]
);
$reviewMetricsSql = "
SELECT
AVG(CASE
WHEN p.review_status = 'approved' AND p.approved_at IS NOT NULL AND p.submitted_at IS NOT NULL
THEN TIMESTAMPDIFF(HOUR, p.submitted_at, p.approved_at)
ELSE NULL
END) AS avgReviewTimeHours,
SUM(CASE WHEN p.current_state = 'submitted' AND p.review_status = 'pending' AND p.submitted_at <= :deadline THEN 1 ELSE 0 END) AS overdueReviewCount,
SUM(CASE WHEN p.review_status = 'approved' THEN 1 ELSE 0 END) AS approvedReviewCount,
SUM(CASE WHEN p.review_status = 'rejected' THEN 1 ELSE 0 END) AS rejectedReviewCount
FROM planning_item p
WHERE p.company_id = :companyId
";
$reviewMetrics = $conn->fetchAssociative($reviewMetricsSql, [
'companyId' => $companyId,
'deadline' => $fortyEightHoursAgo,
]) ?: [];
$avgReviewTimeHours = isset($reviewMetrics['avgReviewTimeHours']) ? round((float) $reviewMetrics['avgReviewTimeHours'], 1) : 0;
$reviewDecisionTotal = (int) ($reviewMetrics['approvedReviewCount'] ?? 0) + (int) ($reviewMetrics['rejectedReviewCount'] ?? 0);
$rejectionRate = $reviewDecisionTotal > 0
? round(((int) ($reviewMetrics['rejectedReviewCount'] ?? 0) / $reviewDecisionTotal) * 100, 1)
: 0;
$reviewSpeedBadge = 'On Track';
$reviewSpeedClass = 'is-approved';
if ($avgReviewTimeHours > 48) {
$reviewSpeedBadge = 'Critical Delay';
$reviewSpeedClass = 'is-rejected';
} elseif ($avgReviewTimeHours > 24) {
$reviewSpeedBadge = 'Slow Review System';
$reviewSpeedClass = 'is-submitted';
}
$systemHealth = [
'inProgress' => $todayInProgress,
'submitted' => $systemSubmitted,
'overdue' => (int) ($reviewMetrics['overdueReviewCount'] ?? 0),
'rejected' => $systemRejected,
];
$openSessionsSql = "
SELECT
t.id AS taskLogId,
t.user_id AS userId,
t.planning_item_id AS planningItemId,
t.actual_start_ts AS actualStartTs,
p.item_alias AS taskTitle,
COALESCE(NULLIF(e.name, ''), u.name, CONCAT('User #', t.user_id)) AS userName
FROM task_log t
LEFT JOIN planning_item p ON p.id = t.planning_item_id
LEFT JOIN sys_user u ON u.user_id = t.user_id
LEFT JOIN employee e ON e.user_id = t.user_id
WHERE p.company_id = :companyId
AND t.working_status = 1
AND t.actual_start_ts BETWEEN :todayStartEpoch AND :nowEpoch
ORDER BY t.actual_start_ts DESC, t.id DESC
";
$openSessions = $conn->fetchAllAssociative($openSessionsSql, [
'companyId' => $companyId,
'todayStartEpoch' => $todayStartEpoch,
'nowEpoch' => $nowEpoch,
]);
foreach ($openSessions as &$sessionRow) {
$startTs = (int) ($sessionRow['actualStartTs'] ?? 0);
$sessionRow['openHours'] = $startTs > 0 ? round(((time() - $startTs) / 3600), 1) : 0;
}
unset($sessionRow);
$nonCompliantUsers = [];
foreach ($openSessions as $sessionRow) {
$userId = (int) ($sessionRow['userId'] ?? 0);
if ($userId === 0) {
continue;
}
if (!isset($nonCompliantUsers[$userId])) {
$nonCompliantUsers[$userId] = [
'userId' => $userId,
'userName' => $sessionRow['userName'] ?? ('User #' . $userId),
'activeSessions' => 0,
'oldestStartTs' => (int) ($sessionRow['actualStartTs'] ?? 0),
'oldestLabel' => $sessionRow['actualStartTs'] ? date('Y-m-d H:i', (int) $sessionRow['actualStartTs']) : 'N/A',
'openHours' => 0,
];
}
$nonCompliantUsers[$userId]['activeSessions']++;
$startTs = (int) ($sessionRow['actualStartTs'] ?? 0);
if ($startTs > 0 && ($nonCompliantUsers[$userId]['oldestStartTs'] === 0 || $startTs < $nonCompliantUsers[$userId]['oldestStartTs'])) {
$nonCompliantUsers[$userId]['oldestStartTs'] = $startTs;
$nonCompliantUsers[$userId]['oldestLabel'] = date('Y-m-d H:i', $startTs);
}
}
foreach ($nonCompliantUsers as &$userRow) {
$userRow['openHours'] = $userRow['oldestStartTs'] > 0 ? round(((time() - $userRow['oldestStartTs']) / 3600), 1) : 0;
}
unset($userRow);
$nonCompliantUsers = array_values($nonCompliantUsers);
return $this->render('@Application/pages/tasks/dashboard_manager.html.twig', [
'page_title' => 'Manager Dashboard',
'currentEmployeeId' => $currentEmployeeId,
'isAdminOverride' => $isOverride,
'activeFilter' => $managerFilter,
'pendingTasks' => $pendingTasks,
'submittedToday' => $submittedToday,
'approvedToday' => $approvedToday,
'rejectedToday' => $rejectedToday,
'pendingReviews' => $pendingReviews,
'overdueReviews' => $overdueReviews,
'openSessions' => $openSessions,
'nonCompliantUsers' => $nonCompliantUsers,
'managerFilter' => $managerFilter,
'avgReviewTimeHours' => $avgReviewTimeHours,
'rejectionRate' => $rejectionRate,
'reviewSpeedBadge' => $reviewSpeedBadge,
'reviewSpeedClass' => $reviewSpeedClass,
'funnelCounts' => [
'inProgress' => $todayInProgress,
'submitted' => $systemSubmitted,
'approved' => (int) ($reviewMetrics['approvedReviewCount'] ?? 0),
'rejected' => $systemRejected,
],
'systemHealth' => $systemHealth,
'taskAlerts' => $this->buildTaskAlerts($request, 'manager'),
]);
}
public function kpiDashboardAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
$companyId = $this->getCompanyId($request);
$sql = "
SELECT
p.assigned_to AS assignedTo,
p.assigned_to_type AS assignedToType,
COALESCE(NULLIF(e.name, ''), NULLIF(u.name, ''), CONCAT(COALESCE(e.first_name, ''), ' ', COALESCE(e.last_name, '')), CONCAT('User #', p.assigned_to)) AS userName,
COUNT(*) AS totalTasks,
SUM(CASE WHEN p.current_state = 'submitted' THEN 1 ELSE 0 END) AS submittedTasks,
SUM(CASE WHEN p.review_status = 'approved' THEN 1 ELSE 0 END) AS approvedTasks,
SUM(CASE WHEN p.review_status = 'rejected' THEN 1 ELSE 0 END) AS rejectedTasks,
SUM(CASE WHEN p.review_status = 'approved' AND COALESCE(p.kpi_counted, 0) = 1 THEN 1 ELSE 0 END) AS kpiCountedTasks
FROM planning_item p
LEFT JOIN employee e ON e.employee_id = p.assigned_to AND p.assigned_to_type = 1
LEFT JOIN sys_user u ON u.user_id = p.assigned_to AND p.assigned_to_type = 2
WHERE p.company_id = :companyId
GROUP BY
p.assigned_to,
p.assigned_to_type,
e.employee_id,
e.name,
e.first_name,
e.last_name,
u.user_id,
u.name
ORDER BY approvedTasks DESC, submittedTasks DESC, totalTasks DESC
";
$rows = $conn->fetchAllAssociative($sql, ['companyId' => $companyId]);
$totals = [
'submitted' => 0,
'approved' => 0,
'rejected' => 0,
'kpiCounted' => 0,
'total' => 0,
];
foreach ($rows as &$row) {
$row['submittedTasks'] = (int) ($row['submittedTasks'] ?? 0);
$row['approvedTasks'] = (int) ($row['approvedTasks'] ?? 0);
$row['rejectedTasks'] = (int) ($row['rejectedTasks'] ?? 0);
$row['kpiCountedTasks'] = (int) ($row['kpiCountedTasks'] ?? 0);
$row['totalTasks'] = (int) ($row['totalTasks'] ?? 0);
$decisions = max($row['approvedTasks'] + $row['rejectedTasks'], 0);
$row['approvalRate'] = $decisions > 0 ? round(($row['approvedTasks'] / $decisions) * 100, 1) : 0;
$totals['submitted'] += $row['submittedTasks'];
$totals['approved'] += $row['approvedTasks'];
$totals['rejected'] += $row['rejectedTasks'];
$totals['kpiCounted'] += $row['kpiCountedTasks'];
$totals['total'] += $row['totalTasks'];
}
unset($row);
$totals['approvalRate'] = ($totals['approved'] + $totals['rejected']) > 0
? round(($totals['approved'] / ($totals['approved'] + $totals['rejected'])) * 100, 1)
: 0;
return $this->render('@Application/pages/tasks/dashboard_kpi.html.twig', [
'page_title' => 'Basic KPI Dashboard',
'rows' => $rows,
'totals' => $totals,
'taskAlerts' => [],
]);
}
}