diff --git a/src/Controller/Reporting/TeamSummaryController.php b/src/Controller/Reporting/TeamSummaryController.php index 8049c253..6f3efc82 100644 --- a/src/Controller/Reporting/TeamSummaryController.php +++ b/src/Controller/Reporting/TeamSummaryController.php @@ -2,13 +2,10 @@ namespace App\Controller\Reporting; -use App\Entity\User; use App\Repository\TeamRepository; -use App\Repository\TimesheetRepository; -use App\Reporting\Report; -use App\Reporting\ReportingService; use DateTime; use Doctrine\DBAL\Connection; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -16,7 +13,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/reporting')] #[IsGranted('view_reporting')] -final class TeamSummaryController +final class TeamSummaryController extends AbstractController { public function __construct( private Connection $connection, @@ -27,8 +24,8 @@ final class TeamSummaryController public function summary(Request $request): Response { $teams = $this->teamRepository->findAll(); - - $selectedTeamId = $request->query->getInt('team', $request->query->getInt('team_id', 0)); + + $selectedTeamId = $request->query->getInt('team', 0); $from = $request->query->get('from', (new DateTime('first day of this month'))->format('Y-m-d')); $to = $request->query->get('to', (new DateTime('last day of this month'))->format('Y-m-d')); @@ -36,23 +33,39 @@ final class TeamSummaryController $rows = []; $projectRows = []; $userColumns = []; + $nonbillRows = []; + $userColumnsNB = []; if ($selectedTeamId > 0) { $team = $this->teamRepository->find($selectedTeamId); if ($team) { - // Section 1: Time Analysis per User $rows = $this->getUserTimeAnalysis($selectedTeamId, $from, $to); - // Section 2: Billable Projects Pivot - [$projectRows, $userColumns] = $this->getProjectPivot($selectedTeamId, $from, $to); + [$projectRows, $userColumns] = $this->getProjectPivot($selectedTeamId, $from, $to, '= 1'); + [$nonbillRows, $userColumnsNB] = $this->getProjectPivot($selectedTeamId, $from, $to, '= 0'); + [$unprodRows, $userColumnsUP] = $this->getProjectPivot($selectedTeamId, $from, $to, '= 2'); } } - return new Response($this->renderTemplate($teams, $team, $rows, $projectRows, $userColumns, $from, $to)); + return $this->render('reporting/team_summary.html.twig', [ + 'report_title' => 'report_team_summary', + 'teams' => $teams, + 'team' => $team, + 'rows' => $rows, + 'projectRows' => $projectRows, + 'userColumns' => $userColumns, + 'nonbillRows' => $nonbillRows, + 'userColumnsNB' => $userColumnsNB, + 'unprodRows' => $unprodRows, + 'userColumnsUP' => $userColumnsUP, + 'from' => $from, + 'to' => $to, + 'selectedTeamId' => $selectedTeamId, + ]); } private function getUserTimeAnalysis(int $teamId, string $from, string $to): array { - $sql = "SELECT + $sql = "SELECT u.username, u.alias, COALESCE(SUM(CASE WHEN p.billable = 1 THEN ts.duration END), 0) / 3600 AS billable_hours, @@ -61,9 +74,9 @@ final class TeamSummaryController COALESCE(SUM(ts.duration), 0) / 3600 AS total_hours FROM kimai2_users u JOIN kimai2_users_teams ut ON u.id = ut.user_id - LEFT JOIN kimai2_timesheet ts ON u.id = ts.user - AND DATE(ts.start_time) >= :from - AND DATE(ts.start_time) <= :to + LEFT JOIN kimai2_timesheet ts ON u.id = ts.user + AND DATE(ts.start_time) >= :from + AND DATE(ts.start_time) <= :to LEFT JOIN kimai2_projects p ON ts.project_id = p.id WHERE ut.team_id = :teamId AND u.enabled = 1 GROUP BY u.id, u.username, u.alias @@ -75,17 +88,15 @@ final class TeamSummaryController ])->fetchAllAssociative(); } - private function getProjectPivot(int $teamId, string $from, string $to): array + private function getProjectPivot(int $teamId, string $from, string $to, string $billableComparison): array { - // Get users in this team (columns of the pivot) $userSql = "SELECT u.id, u.alias FROM kimai2_users u JOIN kimai2_users_teams ut ON u.id = ut.user_id WHERE ut.team_id = :teamId AND u.enabled = 1 ORDER BY u.alias"; $users = $this->connection->executeQuery($userSql, ['teamId' => $teamId])->fetchAllAssociative(); $userColumns = array_column($users, 'alias'); - // Get project totals per user - $sql = "SELECT + $sql = "SELECT c.name AS company, p.name AS project, COALESCE(SUM(ts.duration), 0) / 3600 AS project_total, @@ -97,8 +108,8 @@ final class TeamSummaryController JOIN kimai2_users_teams ut ON u.id = ut.user_id WHERE ut.team_id = :teamId AND DATE(ts.start_time) >= :from - AND DATE(ts.start_time) <= :to - AND p.billable = 1 + AND DATE(ts.start_time) <= :to + AND p.billable " . $billableComparison . " GROUP BY c.id, p.id, u.id ORDER BY c.name, p.name"; @@ -106,7 +117,6 @@ final class TeamSummaryController 'teamId' => $teamId, 'from' => $from, 'to' => $to ])->fetchAllAssociative(); - // Pivot: group by company+project $projectRows = []; foreach ($raw as $row) { $key = $row['company'] . '|||' . $row['project']; @@ -124,84 +134,4 @@ final class TeamSummaryController return [array_values($projectRows), $userColumns]; } - - private function renderTemplate($teams, $team, $rows, $projectRows, $userColumns, $from, $to): string - { - $teamOptions = ''; - foreach ($teams as $t) { - $sel = $team && $t->getId() === $team->getId() ? 'selected' : ''; - $teamOptions .= sprintf('', $t->getId(), $sel, htmlspecialchars($t->getName())); - } - - $html = 'Team Summary - -

Team Summary Report

-
- - - - -
'; - - if ($team) { - $html .= '

Time Analysis — ' . htmlspecialchars($team->getName()) . '

'; - $html .= '

' . $from . ' to ' . $to . '

'; - - if ($rows) { - $html .= ' - - '; - $totalBillable = $totalNonBill = $totalUnprod = $totalHours = 0; - foreach ($rows as $r) { - $html .= sprintf('', - htmlspecialchars($r['alias'] ?: $r['username']), - $r['billable_hours'], $r['nonbillable_hours'], - $r['unproductive_hours'], $r['total_hours'] - ); - $totalBillable += $r['billable_hours']; - $totalNonBill += $r['nonbillable_hours']; - $totalUnprod += $r['unproductive_hours']; - $totalHours += $r['total_hours']; - } - $html .= sprintf('', - $totalBillable, $totalNonBill, $totalUnprod, $totalHours); - $html .= '
NameBillableNon-BillableUnproductiveTotal Hours
%s%.2f%.2f%.2f%.2f
TOTAL%.2f%.2f%.2f%.2f
'; - } else { - $html .= '

No time entries found for this period.

'; - } - - // Section 2: Project Pivot - if ($projectRows && $userColumns) { - $html .= '

Billable Projects

'; - $html .= '
'; - foreach ($userColumns as $uc) { - $html .= ''; - } - $html .= ''; - foreach ($projectRows as $pr) { - $html .= ''; - foreach ($userColumns as $uc) { - $val = $pr['users'][$uc] ?? 0; - $html .= ''; - } - $html .= ''; - } - $html .= '
CompanyProject' . htmlspecialchars($uc) . 'TOTAL
' . htmlspecialchars($pr['company']) . '' . htmlspecialchars($pr['project']) . '' . ($val > 0 ? sprintf('%.1f', $val) : '') . '' . sprintf('%.1f', $pr['total']) . '
'; - } - } - - $html .= ''; - return $html; - } } diff --git a/templates/reporting/team_summary.html.twig b/templates/reporting/team_summary.html.twig new file mode 100644 index 00000000..d3f832ed --- /dev/null +++ b/templates/reporting/team_summary.html.twig @@ -0,0 +1,101 @@ +{% extends 'reporting/layout.html.twig' %} + +{% block report %} +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + {% if team %} +
+

Time Analysis — {{ team.name }}

+ {{ from }} – {{ to }} + + {% if rows is empty %} +
No time entries found for this period.
+ {% else %} + + + + + + + + + + + + {% set tb = 0 %}{% set tn = 0 %}{% set tu = 0 %}{% set tt = 0 %} + {% for r in rows %} + {% set tb = tb + r.billable_hours %} + {% set tn = tn + r.nonbillable_hours %} + {% set tu = tu + r.unproductive_hours %} + {% set tt = tt + r.total_hours %} + + + + + + + + {% endfor %} + + + + + + + + +
NameBillableNon-BillableUnproductiveTotal Hours
{{ r.alias ?: r.username }}{{ r.billable_hours|number_format(2) }}{{ r.nonbillable_hours|number_format(2) }}{{ r.unproductive_hours|number_format(2) }}{{ r.total_hours|number_format(2) }}
TOTAL{{ tb|number_format(2) }}{{ tn|number_format(2) }}{{ tu|number_format(2) }}{{ tt|number_format(2) }}
+ + {% macro pivot(rows, cols, title) %} + {% if rows is not empty %} +

{{ title }}

+
+ + + + {% for uc in cols %}{% endfor %} + + + + {% for pr in rows %} + + + + {% for uc in cols %} + + {% endfor %} + + + {% endfor %} + +
CompanyProject{{ uc }}TOTAL
{{ pr.company }}{{ pr.project }}{{ pr.users[uc]|default(0) > 0 ? pr.users[uc]|number_format(1) }}{{ pr.total|number_format(1) }}
+ {% endif %} + {% endmacro %} + + {% import _self as self %} + {{ self.pivot(projectRows, userColumns, 'Billable Projects') }} + {{ self.pivot(nonbillRows, userColumnsNB, 'Non-Billable Projects') }} + {{ self.pivot(unprodRows, userColumnsUP, 'Unproductive Projects') }} + {% endif %} +
+ {% endif %} +
+{% endblock %}