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('%s ', $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 .= '
- Name Billable Non-Billable Unproductive Total Hours
- ';
- $totalBillable = $totalNonBill = $totalUnprod = $totalHours = 0;
- foreach ($rows as $r) {
- $html .= sprintf('%s %.2f %.2f %.2f %.2f ',
- 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('TOTAL %.2f %.2f %.2f %.2f ',
- $totalBillable, $totalNonBill, $totalUnprod, $totalHours);
- $html .= '
';
- } else {
- $html .= 'No time entries found for this period.
';
- }
-
- // Section 2: Project Pivot
- if ($projectRows && $userColumns) {
- $html .= 'Billable Projects ';
- $html .= 'Company Project ';
- foreach ($userColumns as $uc) {
- $html .= '' . htmlspecialchars($uc) . ' ';
- }
- $html .= 'TOTAL ';
- foreach ($projectRows as $pr) {
- $html .= '' . htmlspecialchars($pr['company']) . ' ' . htmlspecialchars($pr['project']) . ' ';
- foreach ($userColumns as $uc) {
- $val = $pr['users'][$uc] ?? 0;
- $html .= '' . ($val > 0 ? sprintf('%.1f', $val) : '') . ' ';
- }
- $html .= '' . sprintf('%.1f', $pr['total']) . ' ';
- }
- $html .= '
';
- }
- }
-
- $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 %}
+
+
+
+ Name
+ Billable
+ Non-Billable
+ Unproductive
+ Total Hours
+
+
+
+ {% 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 %}
+
+ {{ 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) }}
+
+ {% endfor %}
+
+ 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 }}
+
+
+
+ Company Project
+ {% for uc in cols %}{{ uc }} {% endfor %}
+ TOTAL
+
+
+ {% for pr in rows %}
+
+ {{ pr.company }}
+ {{ pr.project }}
+ {% for uc in cols %}
+ {{ pr.users[uc]|default(0) > 0 ? pr.users[uc]|number_format(1) }}
+ {% endfor %}
+ {{ pr.total|number_format(1) }}
+
+ {% endfor %}
+
+
+ {% 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 %}