*/ public function getDailyAggregation(User $user, \DateTimeInterface $begin, \DateTimeInterface $end, ?int $projectId = null, ?int $customerId = null, ?int $activityId = null): array { $qb = $this->repository->createQueryBuilder('t'); $qb ->select('COALESCE(SUM(t.duration), 0) as duration') ->addSelect('COUNT(t.id) as count') ->addSelect('DATE(t.date) as day') ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) ->andWhere($qb->expr()->eq('t.user', ':user')) ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('begin', $begin->format('Y-m-d')) ->setParameter('end', $end->format('Y-m-d')) ->setParameter('user', $user) ->groupBy('day') ->orderBy('day', 'ASC') ; if ($projectId !== null) { $qb->andWhere($qb->expr()->eq('t.project', ':project')) ->setParameter('project', $projectId); } if ($activityId !== null) { $qb->andWhere($qb->expr()->eq('t.activity', ':activity')) ->setParameter('activity', $activityId); } if ($customerId !== null) { $qb->join('t.project', 'p') ->andWhere($qb->expr()->eq('p.customer', ':customer')) ->setParameter('customer', $customerId); } $results = $qb->getQuery()->getResult(); return array_map(function (array $row) { return [ 'date' => $row['day'], 'hours' => round((int) $row['duration'] / 3600, 2), 'count' => (int) $row['count'], ]; }, $results); } /** * @return array */ public function getHourlyAggregation(User $user, \DateTimeInterface $begin, \DateTimeInterface $end, ?int $projectId = null, ?int $customerId = null, ?int $activityId = null): array { $conn = $this->entityManager->getConnection(); $offsetStr = $this->computeTimezoneOffset($user); $sql = 'SELECT HOUR(CONVERT_TZ(t.start_time, \'+00:00\', :tz)) as hour_slot, COALESCE(SUM(t.duration), 0) as duration, COUNT(t.id) as count FROM kimai2_timesheet t WHERE t.user = :user AND t.date_tz BETWEEN :begin AND :end AND t.end_time IS NOT NULL'; $params = [ 'tz' => $offsetStr, 'user' => $user->getId(), 'begin' => $begin->format('Y-m-d'), 'end' => $end->format('Y-m-d'), ]; if ($projectId !== null) { $sql .= ' AND t.project_id = :project'; $params['project'] = $projectId; } if ($activityId !== null) { $sql .= ' AND t.activity_id = :activity'; $params['activity'] = $activityId; } if ($customerId !== null) { $sql .= ' AND t.project_id IN (SELECT p.id FROM kimai2_projects p WHERE p.customer_id = :customer)'; $params['customer'] = $customerId; } $sql .= ' GROUP BY hour_slot ORDER BY hour_slot ASC'; $results = $conn->executeQuery($sql, $params)->fetchAllAssociative(); return array_map(fn(array $row) => [ 'hour' => (int) $row['hour_slot'], 'hours' => round((int) $row['duration'] / 3600, 2), 'count' => (int) $row['count'], ], $results); } /** * @return array */ public function getDayHourAggregation(User $user, \DateTimeInterface $begin, \DateTimeInterface $end, ?int $projectId = null, ?int $customerId = null, ?int $activityId = null, string $weekStart = 'monday'): array { $conn = $this->entityManager->getConnection(); $offsetStr = $this->computeTimezoneOffset($user); $sql = 'SELECT DAYOFWEEK(t.date_tz) as dow, HOUR(CONVERT_TZ(t.start_time, \'+00:00\', :tz)) as hour_slot, COALESCE(SUM(t.duration), 0) as duration, COUNT(t.id) as count FROM kimai2_timesheet t WHERE t.user = :user AND t.date_tz BETWEEN :begin AND :end AND t.end_time IS NOT NULL'; $params = [ 'tz' => $offsetStr, 'user' => $user->getId(), 'begin' => $begin->format('Y-m-d'), 'end' => $end->format('Y-m-d'), ]; if ($projectId !== null) { $sql .= ' AND t.project_id = :project'; $params['project'] = $projectId; } if ($activityId !== null) { $sql .= ' AND t.activity_id = :activity'; $params['activity'] = $activityId; } if ($customerId !== null) { $sql .= ' AND t.project_id IN (SELECT p.id FROM kimai2_projects p WHERE p.customer_id = :customer)'; $params['customer'] = $customerId; } $sql .= ' GROUP BY dow, hour_slot ORDER BY dow, hour_slot'; $results = $conn->executeQuery($sql, $params)->fetchAllAssociative(); $weekStartOffset = ($weekStart === 'sunday') ? 1 : 2; return array_map(fn(array $row) => [ 'day' => ((int) $row['dow'] - $weekStartOffset + 7) % 7, 'hour' => (int) $row['hour_slot'], 'hours' => round((int) $row['duration'] / 3600, 2), 'count' => (int) $row['count'], ], $results); } /** * @return array */ public function getUserProjects(User $user): array { $qb = $this->repository->createQueryBuilder('t'); $qb->select('DISTINCT IDENTITY(t.project) as projectId, p.name') ->join('t.project', 'p') ->andWhere($qb->expr()->eq('t.user', ':user')) ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('user', $user) ->orderBy('p.name', 'ASC'); return array_map(fn(array $row) => [ 'id' => (int) $row['projectId'], 'name' => $row['name'], ], $qb->getQuery()->getResult()); } /** * @return array */ public function getUserCustomers(User $user): array { $qb = $this->repository->createQueryBuilder('t'); $qb->select('DISTINCT IDENTITY(p.customer) as customerId, c.name') ->join('t.project', 'p') ->join('p.customer', 'c') ->andWhere($qb->expr()->eq('t.user', ':user')) ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('user', $user) ->orderBy('c.name', 'ASC'); return array_map(fn(array $row) => [ 'id' => (int) $row['customerId'], 'name' => $row['name'], ], $qb->getQuery()->getResult()); } /** * @return array */ public function getUserActivities(User $user, ?int $projectId = null): array { $qb = $this->repository->createQueryBuilder('t'); $qb->select('DISTINCT IDENTITY(t.activity) as activityId, a.name') ->join('t.activity', 'a') ->andWhere($qb->expr()->eq('t.user', ':user')) ->andWhere($qb->expr()->isNotNull('t.end')) ->setParameter('user', $user) ->orderBy('a.name', 'ASC'); if ($projectId !== null) { $qb->andWhere($qb->expr()->eq('t.project', ':project')) ->setParameter('project', $projectId); } return array_map(fn(array $row) => [ 'id' => (int) $row['activityId'], 'name' => $row['name'], ], $qb->getQuery()->getResult()); } private function computeTimezoneOffset(User $user): string { $tz = new \DateTimeZone($user->getTimezone()); $offset = $tz->getOffset(new \DateTime('now', new \DateTimeZone('UTC'))); $hours = intdiv($offset, 3600); $minutes = abs(intdiv($offset % 3600, 60)); return sprintf('%+03d:%02d', $hours, $minutes); } }