diff --git a/API/TimesheetApiController.php b/API/TimesheetApiController.php new file mode 100644 index 0000000..ddff390 --- /dev/null +++ b/API/TimesheetApiController.php @@ -0,0 +1,489 @@ +prepareQuery($query, $paramFetcher); + + $seeAll = $this->applyUserFilters($query, $paramFetcher, $userRepository); + $this->applyUserFallback($query, $seeAll); + + $this->applyCustomerFilters($query, $paramFetcher, $customerRepository); + $this->applyProjectFilters($query, $paramFetcher, $projectRepository); + $this->applyActivityFilters($query, $paramFetcher, $activityRepository); + $this->applyTagFilters($query, $paramFetcher); + $this->applyDateFilters($query, $paramFetcher); + $this->applyStateFilters($query, $paramFetcher); + $this->applySearchFilters($query, $paramFetcher); + $this->applyModifiedAfter($query, $paramFetcher); + + [$data, $results, $approvalMap] = $this->applyApprovalFilter($query, $paramFetcher); + + $view = new View($results, Response::HTTP_OK); + $this->applyCollectionGroups($view, $paramFetcher); + $this->applyPaginationHeaders($view, $data); + $response = $this->viewHandler->handle($view); + + return $this->addApprovalToJsonResponse($response, $results, $approvalMap); + } + + private function applyUserFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher, UserRepository $userRepository): bool + { + $seeAll = false; + + if ($this->isGranted('view_other_timesheet')) { + /** @var array $users */ + $users = $paramFetcher->get('users'); + $userId = $paramFetcher->get('user'); + + if ('all' === $userId) { + $seeAll = true; + } elseif (\is_string($userId) && $userId !== '') { + $users[] = (int) $userId; + } + + if (!$seeAll) { + foreach ($userRepository->findByIds($users) as $user) { + $query->addUser($user); + } + } + } + + return $seeAll; + } + + private function applyUserFallback(TimesheetQuery $query, bool $seeAll): void + { + if ($seeAll) { + $query->setUser(null); + + return; + } + + if (!$query->hasUsers()) { + $query->setUser($this->getUser()); + } + } + + private function applyCustomerFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): void + { + /** @var array $customers */ + $customers = $paramFetcher->get('customers'); + $customer = $paramFetcher->get('customer'); + if (\is_string($customer) && $customer !== '') { + $customers[] = $customer; + } + + foreach (array_unique($customers) as $customerId) { + $customer = $customerRepository->find($customerId); + if ($customer === null) { + throw $this->createNotFoundException('Unknown customer: ' . $customerId); + } + $query->addCustomer($customer); + } + } + + private function applyProjectFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher, ProjectRepository $projectRepository): void + { + /** @var array $projects */ + $projects = $paramFetcher->get('projects'); + $project = $paramFetcher->get('project'); + if (\is_string($project) && $project !== '') { + $projects[] = $project; + } + + foreach (array_unique($projects) as $projectId) { + $project = $projectRepository->find($projectId); + if ($project === null) { + throw $this->createNotFoundException('Unknown project: ' . $project); + } + $query->addProject($project); + } + } + + private function applyActivityFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher, ActivityRepository $activityRepository): void + { + /** @var array $activities */ + $activities = $paramFetcher->get('activities'); + $activity = $paramFetcher->get('activity'); + if (\is_string($activity) && $activity !== '') { + $activities[] = $activity; + } + + foreach (array_unique($activities) as $activityId) { + $activity = $activityRepository->find($activityId); + if ($activity === null) { + throw $this->createNotFoundException('Unknown activity: ' . $activity); + } + $query->addActivity($activity); + } + } + + private function applyTagFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): void + { + /** @var array $tags */ + $tags = $paramFetcher->get('tags'); + if (!\is_array($tags) || \count($tags) === 0) { + return; + } + + $tagsByName = $this->tagRepository->findTagsByName($tags, true); + if (\count($tagsByName) === 0) { + throw new BadRequestHttpException('Given tags were not found'); + } + foreach ($tagsByName as $tag) { + $query->addTag($tag); + } + } + + private function applyDateFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): void + { + $factory = $this->getDateTimeFactory(); + + $begin = $paramFetcher->get('begin'); + if (\is_string($begin) && $begin !== '') { + $query->setBegin($factory->createDateTime($begin)); + } + + $end = $paramFetcher->get('end'); + if (\is_string($end) && $end !== '') { + $query->setEnd($factory->createDateTime($end)); + } + } + + private function applyStateFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): void + { + $active = $paramFetcher->get('active'); + if (\is_string($active) && $active !== '') { + $active = (int) $active; + if ($active === 1) { + $query->setState(TimesheetQuery::STATE_RUNNING); + } elseif ($active === 0) { + $query->setState(TimesheetQuery::STATE_STOPPED); + } + } + + $billable = $paramFetcher->get('billable'); + if (\is_string($billable) && $billable !== '') { + $billable = (int) $billable; + if ($billable === 1) { + $query->setBillable(true); + } elseif ($billable === 0) { + $query->setBillable(false); + } + } + + $exported = $paramFetcher->get('exported'); + if (\is_string($exported) && $exported !== '') { + $exported = (int) $exported; + if ($exported === 1) { + $query->setExported(TimesheetQuery::STATE_EXPORTED); + } elseif ($exported === 0) { + $query->setExported(TimesheetQuery::STATE_NOT_EXPORTED); + } + } + } + + private function applySearchFilters(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): void + { + $term = $paramFetcher->get('term'); + if (\is_string($term) && $term !== '') { + $query->setSearchTerm(new SearchTerm($term)); + } + } + + private function applyModifiedAfter(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): void + { + $modifiedAfter = $paramFetcher->get('modified_after'); + if (\is_string($modifiedAfter)) { + $query->setModifiedAfter(new \DateTimeImmutable($modifiedAfter, new \DateTimeZone('UTC'))); + } + } + + private function applyCollectionGroups(View $view, ParamFetcherInterface $paramFetcher): void + { + $full = $paramFetcher->get('full'); + if ($full === '1' || $full === 'true') { + $view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL); + + return; + } + + $view->getContext()->setGroups(self::GROUPS_COLLECTION); + } + + private function applyPaginationHeaders(View $view, Pagerfanta $data): void + { + $view->setHeader('X-Page', (string) $data->getCurrentPage()); + $view->setHeader('X-Total-Count', (string) $data->getNbResults()); + $view->setHeader('X-Total-Pages', (string) $data->getNbPages()); + $view->setHeader('X-Per-Page', (string) $data->getMaxPerPage()); + } + + private function applyApprovalFilter(TimesheetQuery $query, ParamFetcherInterface $paramFetcher): array + { + $approved = $this->getApprovedFilter($paramFetcher); + if ($approved === null) { + $data = $this->repository->getPagerfantaForQuery($query); + $results = (array) $data->getCurrentPageResults(); + $approvalMap = $this->buildApprovalMap($results); + + return [$data, $results, $approvalMap]; + } + + $timesheets = $this->repository->getTimesheetsForQuery($query); + $approvalMap = $this->buildApprovalMap($timesheets); + $filtered = array_values(array_filter( + $timesheets, + function (Timesheet $timesheet) use ($approvalMap, $approved): bool { + return ($approvalMap[$timesheet->getId()] ?? false) === $approved; + } + )); + + $pager = $this->createPagerfanta($filtered, $paramFetcher); + $results = (array) $pager->getCurrentPageResults(); + + return [$pager, $results, $approvalMap]; + } + + private function getApprovedFilter(ParamFetcherInterface $paramFetcher): ?bool + { + $approved = $paramFetcher->get('approved'); + if (!\is_string($approved) || $approved === '') { + return null; + } + + if ($approved === '1' || $approved === 'true') { + return true; + } + + if ($approved === '0' || $approved === 'false') { + return false; + } + + return null; + } + + private function createPagerfanta(array $timesheets, ParamFetcherInterface $paramFetcher): Pagerfanta + { + $pager = new Pagerfanta(new ArrayAdapter($timesheets)); + $pager->setMaxPerPage($this->getPageSize($paramFetcher)); + $pager->setCurrentPage($this->getPageNumber($paramFetcher)); + + return $pager; + } + + private function getPageNumber(ParamFetcherInterface $paramFetcher): int + { + $page = $paramFetcher->get('page'); + if (\is_string($page) && $page !== '') { + return max(1, (int) $page); + } + + return 1; + } + + private function getPageSize(ParamFetcherInterface $paramFetcher): int + { + $size = $paramFetcher->get('size'); + if (\is_string($size) && $size !== '') { + return min(500, max(1, (int) $size)); + } + + return 50; + } + + private function buildApprovalMap(array $timesheets): array + { + if ($timesheets === []) { + return []; + } + + $userIds = []; + $minDate = null; + $maxDate = null; + + foreach ($timesheets as $timesheet) { + if (!$timesheet instanceof Timesheet) { + continue; + } + + $userIds[] = $timesheet->getUser()->getId(); + $date = $timesheet->getBegin(); + $dateOnly = new \DateTimeImmutable($date->format('Y-m-d')); + + if ($minDate === null || $dateOnly < $minDate) { + $minDate = $dateOnly; + } + + if ($maxDate === null || $dateOnly > $maxDate) { + $maxDate = $dateOnly; + } + } + + if ($minDate === null || $maxDate === null) { + return []; + } + + $userIds = array_values(array_unique($userIds)); + $approvals = $this->approvalRepository->findApprovedForUsersAndDateRange($userIds, $minDate, $maxDate); + + $rangesByUser = []; + foreach ($approvals as $approval) { + $userId = $approval->getUser()->getId(); + $rangesByUser[$userId][] = [ + $approval->getStartDate()->format('Y-m-d'), + $approval->getEndDate()->format('Y-m-d'), + ]; + } + + $approvalMap = []; + foreach ($timesheets as $timesheet) { + if (!$timesheet instanceof Timesheet) { + continue; + } + + $timesheetId = $timesheet->getId(); + $userId = $timesheet->getUser()->getId(); + $date = $timesheet->getBegin()->format('Y-m-d'); + + $approved = false; + foreach ($rangesByUser[$userId] ?? [] as [$startDate, $endDate]) { + if ($date >= $startDate && $date <= $endDate) { + $approved = true; + break; + } + } + + $approvalMap[$timesheetId] = $approved; + } + + return $approvalMap; + } + + private function addApprovalToJsonResponse(Response $response, array $results, array $approvalMap): Response + { + if ($results === []) { + return $response; + } + + $contentType = $response->headers->get('Content-Type', ''); + if (!str_contains($contentType, 'json')) { + return $response; + } + + $content = $response->getContent(); + if (!\is_string($content) || $content === '') { + return $response; + } + + try { + $payload = json_decode($content, true, 512, \JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return $response; + } + + if (!\is_array($payload)) { + return $response; + } + + foreach ($payload as $index => &$item) { + if (!isset($results[$index]) || !$results[$index] instanceof Timesheet || !\is_array($item)) { + continue; + } + + $timesheetId = $results[$index]->getId(); + $item['approved'] = $approvalMap[$timesheetId] ?? false; + } + unset($item); + + try { + $response->setContent(json_encode($payload, \JSON_THROW_ON_ERROR)); + } catch (\JsonException) { + return $response; + } + + return $response; + } +} \ No newline at end of file diff --git a/Controller/ApprovalController.php b/Controller/ApprovalController.php index ca94e60..1a78d29 100644 --- a/Controller/ApprovalController.php +++ b/Controller/ApprovalController.php @@ -9,7 +9,13 @@ namespace KimaiPlugin\ApprovalBundle\Controller; +use App\Entity\User; +use App\Entity\Team; +use App\Form\Model\DateRange; use App\Repository\UserRepository; +use App\Repository\TimesheetRepository; +use App\Repository\Query\BaseQuery; +use App\Repository\Query\TimesheetQuery; use DateTime; use KimaiPlugin\ApprovalBundle\Entity\Approval; use KimaiPlugin\ApprovalBundle\Entity\ApprovalHistory; @@ -18,9 +24,11 @@ use KimaiPlugin\ApprovalBundle\Repository\ApprovalRepository; use KimaiPlugin\ApprovalBundle\Repository\ApprovalStatusRepository; use KimaiPlugin\ApprovalBundle\Repository\LockdownRepository; +use KimaiPlugin\ApprovalBundle\Toolbox\BreakTimeCheckToolGER; use KimaiPlugin\ApprovalBundle\Toolbox\EmailTool; use KimaiPlugin\ApprovalBundle\Toolbox\SettingsTool; use KimaiPlugin\ApprovalBundle\Enumeration\ConfigEnum; +use KimaiPlugin\ApprovalBundle\Service\AutoApprovalService; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; @@ -37,7 +45,10 @@ public function __construct( private UserRepository $userRepository, private EmailTool $emailTool, private SettingsTool $settingsTool, - private LockdownRepository $lockdownRepository + private LockdownRepository $lockdownRepository, + private TimesheetRepository $timesheetRepository, + private BreakTimeCheckToolGER $breakTimeCheckToolGER, + private AutoApprovalService $autoApprovalService ) { } @@ -47,7 +58,7 @@ public function addToApprove(Request $request): RedirectResponse $userId = $request->query->get('user'); $date = $request->query->get('date'); $approval = $this->createAddToApproveForm($userId, $date); - if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_SUBMITTED_NY, true)){ + if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_SUBMITTED_NY, true)) { $this->emailTool->sendApproveWeekEmail($approval, $this->approvalRepository); } $this->lockdownRepository->updateLockWeek($approval, $this->approvalRepository); @@ -58,6 +69,89 @@ public function addToApprove(Request $request): RedirectResponse ])); } + #[Route(path: '/auto_approve', name: 'auto_approve', methods: ['GET'])] + public function autoApproveAction(Request $request): RedirectResponse + { + $users = $this->getUsersForAutoApproval(); + $query = $request->getSession()->get('query'); + + if (!empty($query->getUsers())) { + $users = $query->getUsers(); + } + + $submittedApprovals = $this->approvalRepository->getUserApprovalsFiltered($users, $query); + + $result = $this->autoApprovalService->processApprovals($submittedApprovals); + + // Process approved approvals + foreach ($result['processedApprovals'] as $approval) { + $this->finalizeApproval($approval, $request->query->get('date')); + } + + return $this->buildAutoApprovalRedirect($query, $result['successful'], $result['failed'], $request); + } + + private function getUsersForAutoApproval(): array + { + $includeSelf = $this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_TEAMLEAD_SELF_APPROVE_NY) == '1'; + return $this->getUsers($includeSelf); + } + + private function finalizeApproval(Approval $approval, ?string $date): void + { + $approval = $this->createNewApproveHistory($approval->getId(), ApprovalStatus::APPROVED); + + if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_ACTION_NY, true)) { + $this->emailTool->sendStatusChangedEmail( + $approval, + $this->getUser()->getDisplayName(), + $this->approvalRepository->getUrl( + (string) $approval->getUser()->getId(), + $approval->getStartDate()->format('Y-m-d') + ) + ); + } + + $this->lockdownRepository->updateLockWeek($approval, $this->approvalRepository); + $this->emailIfClosedMonth($date); + } + + private function buildAutoApprovalRedirect($query, int $successCount, int $failCount, Request $request): RedirectResponse + { + if (!$query) { + return new RedirectResponse($this->urlGenerator->generate('approval_bundle_to_approve')); + } + + $params = [ + 'page' => $query->getPage(), + 'orderBy' => $query->getOrderBy(), + 'order' => $query->getOrder(), + 'auto_approve_success' => $successCount, + 'auto_approve_fail' => $failCount, + ]; + + if ($query->getSearchTerm() && !empty($query->getSearchTerm()->getSearchTerm())) { + $params['searchTerm'] = $query->getSearchTerm()->getSearchTerm(); + $params['performSearch'] = 'performSearch'; + } + + if ($query->getBegin() && $query->getEnd()) { + $params['daterange'] = $query->getBegin()->format('d.m.Y') . ' - ' . $query->getEnd()->format('d.m.Y'); + } + + if (!empty($query->getUsers())) { + $params['users'] = array_map(fn($user) => $user->getId(), $query->getUsers()); + } + + if (!empty($query->getStatus())) { + $params['status'] = $query->getStatus(); + } + + $params['_token'] = $request->getSession()->get('_csrf/toolbar'); + + return new RedirectResponse($this->urlGenerator->generate('approval_bundle_to_approve', $params)); + } + #[Route(path: '/approve/{approveId}', defaults: ['approveId' => 0], name: 'approve', methods: ['GET'])] public function approveAction(Request $request, string $approveId): RedirectResponse { @@ -71,7 +165,7 @@ public function approveAction(Request $request, string $approveId): RedirectResp ); if ($approval) { $approval = $this->createNewApproveHistory($approveId, ApprovalStatus::APPROVED); - if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_ACTION_NY, true)){ + if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_ACTION_NY, true)) { $this->emailTool->sendStatusChangedEmail( $approval, $this->getUser()->getDisplayName(), @@ -129,7 +223,7 @@ public function deniedAction(Request $request, string $approveId): RedirectRespo ); if ($approval) { $approval = $this->createNewApproveHistory($approveId, ApprovalStatus::DENIED, $request->query->get('input')); - if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_ACTION_NY, true)){ + if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_MAIL_ACTION_NY, true)) { $this->emailTool->sendStatusChangedEmail( $approval, $this->getUser()->getDisplayName(), @@ -211,4 +305,58 @@ private function emailIfClosedMonth($date): void } } } + + private function getUsers(bool $includeOwnForTeam = true): array + { + if ($this->canManageAllPerson()) { + $users = $this->userRepository->findAll(); + } elseif ($this->canManageTeam()) { + $users = []; + $user = $this->getUser(); + /** @var Team $team */ + foreach ($user->getTeams() as $team) { + if (\in_array($user, $team->getTeamleads())) { + array_push($users, ...$team->getUsers()); + } else { + $users[] = $user; + } + } + + if (empty($users)) { + $users = [$user]; + } + + $users = array_unique($users); + + if (!$includeOwnForTeam) { + // remove the active user from the list + $index = array_search($this->getUser(), $users); + if ($index !== false) { + unset($users[$index]); + } + } + } else { + $users = [$this->getUser()]; + } + + $users = array_reduce($users, function ($current, $user) { + $includeSuperAdmin = $this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_INCLUDE_ADMIN_NY) == '1'; + if ($user->isEnabled() && (!$user->isSuperAdmin() || $includeSuperAdmin)) { + $current[] = $user; + } + + return $current; + }, []); + + if (!empty($users)) { + usort( + $users, + function (User $userA, User $userB) { + return strcmp(strtoupper($userA->getUsername()), strtoupper($userB->getUsername())); + } + ); + } + + return $users; + } } diff --git a/Controller/BaseApprovalController.php b/Controller/BaseApprovalController.php index 482c874..9572295 100644 --- a/Controller/BaseApprovalController.php +++ b/Controller/BaseApprovalController.php @@ -30,7 +30,8 @@ protected function getDefaultTemplateParams(SettingsTool $settingsTool): array 'showToApproveTab' => $this->canManageAllPerson() || $this->canManageTeam(), 'showSettings' => $this->isGranted('ROLE_SUPER_ADMIN'), 'showSettingsWorkdays' => $this->isGranted('ROLE_SUPER_ADMIN') && $settingsTool->isOvertimeCheckActive(), - 'showOvertime' => $settingsTool->isOvertimeCheckActive() + 'showOvertime' => $settingsTool->isOvertimeCheckActive(), + 'showWorkingTimeActCheckGER' => $this->isGranted('ROLE_SUPER_ADMIN') && $settingsTool->isBreakTimeCheckActive(), ]; } } diff --git a/Controller/WeekReportController.php b/Controller/WeekReportController.php index 86e33c3..1bec7f0 100644 --- a/Controller/WeekReportController.php +++ b/Controller/WeekReportController.php @@ -38,6 +38,7 @@ use KimaiPlugin\ApprovalBundle\Repository\ApprovalWorkdayHistoryRepository; use KimaiPlugin\ApprovalBundle\Repository\ReportRepository; use KimaiPlugin\ApprovalBundle\Repository\Query\ApprovalQuery; +use KimaiPlugin\ApprovalBundle\Service\ApprovalDataService; use KimaiPlugin\ApprovalBundle\Toolbox\BreakTimeCheckToolGER; use KimaiPlugin\ApprovalBundle\Toolbox\Formatting; use KimaiPlugin\ApprovalBundle\Toolbox\SecurityTool; @@ -65,7 +66,8 @@ public function __construct( private TimesheetRepository $timesheetRepository, private ApprovalTimesheetRepository $approvalTimesheetRepository, private BreakTimeCheckToolGER $breakTimeCheckToolGER, - private ReportRepository $reportRepository + private ReportRepository $reportRepository, + private ApprovalDataService $approvalDataService ) { } @@ -176,137 +178,63 @@ public function toApprove(ApprovalQuery $query, Request $request): Response return $this->redirectToRoute('approval_bundle_to_approve'); } - if ($this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_TEAMLEAD_SELF_APPROVE_NY) == '1') { - $users = $this->getUsers(true); - } else { - $users = $this->getUsers(false); - } - - $warningNoUsers = false; - if (empty($users)) { - $warningNoUsers = true; - $users = [$this->getUser()]; - } + $request->getSession()->set('query', $query); - $allRows = $this->approvalRepository->findAllWeek($users); + $users = $this->getUsersForApproval(); + $warningNoUsers = empty($users); - if ($this->settingsTool->getBooleanConfiguration(ConfigEnum::APPROVAL_HIDE_APPROVED_NY, false)) { - $allRows = $this->approvalRepository->filterWeeksNotApproved($allRows); - } - - $selectedUsers = $query->getUsers(); - if (!empty($selectedUsers)) { - $selectedUserIds = array_map(fn(User $user) => $user->getId(), $selectedUsers); - $allRows = array_filter( - $allRows, - function ($row) use ($selectedUserIds) { - return in_array($row['userId'], $selectedUserIds); - } - ); - } - - $dateRange = $query->getDateRange(); - if ($dateRange !== null && $dateRange->getBegin() !== null && $dateRange->getEnd() !== null) { - $begin = $dateRange->getBegin(); - $end = $dateRange->getEnd(); - - $allRows = array_filter( - $allRows, - function ($row) use ($begin, $end) { - $weekStart = $row['week']->value; - $weekEnd = (clone $weekStart)->modify('+6 days'); - return ($weekStart >= $begin && $weekStart <= $end) || - ($weekEnd >= $begin && $weekEnd <= $end); - } - ); + if ($warningNoUsers) { + $users = [$this->getUser()]; } - $selectedStatus = $query->getStatus(); - if (!empty($selectedStatus)) { - $allRows = array_filter( - $allRows, - function ($row) use ($selectedStatus) { - return in_array($row['status'], $selectedStatus); - } - ); + $allRows = $this->approvalDataService->fetchAndFilterApprovalRows($query, $users); + if ($this->settingsTool->isBreakTimeCheckActive()) { + $allRows = $this->approvalDataService->enrichRowsWithErrors($allRows); } - - $searchTerm = $query->getSearchTerm(); - if ($searchTerm !== null && !empty($searchTerm->getSearchTerm())) { - - $searchParts = $searchTerm->getParts(); - - $allRows = array_filter( - $allRows, - function ($row) use ($searchParts) { - foreach ($searchParts as $part) { - $term = mb_strtolower($part->getTerm()); - $matchedInRow = false; - - $userName = mb_strtolower($row['user']); - if (str_contains($userName, $term)) { - $matchedInRow = true; - } - - $weekLabel = mb_strtolower($row['week']->label ?? ''); - if (str_contains($weekLabel, $term)) { - $matchedInRow = true; - } - - $status = mb_strtolower($row['status']); - if (str_contains($status, $term)) { - $matchedInRow = true; - } - - if (!$matchedInRow) { - return false; - } - } - return true; - } - ); - } - $allRows = $this->sortArrayByQuery($allRows, $query); - $pastRows = []; - $currentRows = []; - $futureRows = []; - $currentWeek = (new DateTime('now'))->modify('next monday')->modify('-2 week')->format('Y-m-d'); - $futureWeek = (new DateTime('now'))->modify('next monday')->modify('-1 week')->format('Y-m-d'); - foreach ($allRows as $row) { - if ($row['startDate'] >= $futureWeek) { - $futureRows[] = $row; - } elseif ($row['startDate'] >= $currentWeek) { - $currentRows[] = $row; - } else { - $pastRows[] = $row; - } - } + [$pastRows, $currentRows, $futureRows] = $this->approvalDataService->categorizeRowsByWeek($allRows); $pastRows = $this->approvalRepository->filterPastWeeksNotApproved($pastRows); - $allRowsDataTable = new DataTable('approval_all_entries', $query); - $allRowsDataTable->deactivateConfiguration(); - $allRowsDataTable->addColumn("user"); - $allRowsDataTable->addColumn("week"); - $allRowsDataTable->addColumn("status"); - $allRowsDataTable->addColumn("actions", ['class' => 'actions alwaysVisible']); - - $allRowsAdapter = new ArrayAdapter($allRows); - $allRowsPagination = new Pagination($allRowsAdapter); - $allRowsDataTable->setPagination($allRowsPagination); - $allRowsDataTable->setSearchForm($form); + $weeksSubmitted = $this->approvalDataService->countSubmittedWeeks($allRows); + $dataTable = $this->buildDataTable($query, $allRows, $form); return $this->render('@Approval/to_approve.html.twig', [ 'current_tab' => 'to_approve', - 'all_rows_datatable' => $allRowsDataTable, + 'all_rows_datatable' => $dataTable, 'past_rows' => $pastRows, 'current_rows' => $currentRows, 'future_rows' => $futureRows, + 'weeks_submitted' => $weeksSubmitted, + 'auto_approve_success' => $request->query->getInt('auto_approve_success', 0), + 'auto_approve_fail' => $request->query->getInt('auto_approve_fail', 0), 'warningNoUsers' => $warningNoUsers ] + $this->getDefaultTemplateParams($this->settingsTool)); } + private function getUsersForApproval(): array + { + $includeSelf = $this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_TEAMLEAD_SELF_APPROVE_NY) == '1'; + return $this->getUsers($includeSelf); + } + + private function buildDataTable(ApprovalQuery $query, array $rows, FormInterface $form): DataTable + { + $dataTable = new DataTable('approval_all_entries', $query); + $dataTable->deactivateConfiguration(); + $dataTable->addColumn("user"); + $dataTable->addColumn("week"); + $dataTable->addColumn("status"); + $dataTable->addColumn("actions", ['class' => 'actions alwaysVisible']); + + $adapter = new ArrayAdapter($rows); + $pagination = new Pagination($adapter); + $dataTable->setPagination($pagination); + $dataTable->setSearchForm($form); + + return $dataTable; + } + protected function getToolbarForm(ApprovalQuery $query): FormInterface { return $this->createSearchForm(ApprovalToolbarForm::class, $query, [ @@ -562,22 +490,25 @@ private function getTimesheets(?User $selectedUser, DateTime $start, DateTime $e $this->settingsTool->isInConfiguration(ConfigEnum::APPROVAL_BREAKCHECKS_NY) == false or $this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_BREAKCHECKS_NY) ) { - $errors = $this->breakTimeCheckToolGER->checkBreakTime($timesheets); + $breakTimeErrors = $this->breakTimeCheckToolGER->checkBreakTimeDetails($timesheets); + $errors = $breakTimeErrors['days']; + $timesheetErrors = $breakTimeErrors['timesheets']; } else { $errors = []; + $timesheetErrors = []; } return [ array_reduce( $timesheets, - function ($result, Timesheet $timesheet) use ($errors) { + function ($result, Timesheet $timesheet) use ($timesheetErrors) { $date = $timesheet->getBegin()->format('Y-m-d'); if ($timesheet->getEnd()) { $result[] = [ 'date' => $date, 'begin' => $timesheet->getBegin()->format('H:i'), 'end' => $timesheet->getEnd()->format('H:i'), - 'error' => \array_key_exists($date, $errors) ? $errors[$date] : [], + 'error' => $timesheetErrors[spl_object_id($timesheet)] ?? [], 'duration' => $timesheet->getDuration(), 'customerName' => $timesheet->getProject()->getCustomer()->getName(), 'projectName' => $timesheet->getProject()->getName(), diff --git a/Controller/WorkingTimeActGERController.php b/Controller/WorkingTimeActGERController.php new file mode 100644 index 0000000..e2fa763 --- /dev/null +++ b/Controller/WorkingTimeActGERController.php @@ -0,0 +1,325 @@ +isGranted('ROLE_SUPER_ADMIN')) { + throw $this->createAccessDeniedException(); + } + + $form = $this->getToolbarForm($query); + if ($this->handleSearch($form, $request)) { + return $this->redirectToRoute('approval_bundle_working_time_act_ger'); + } + + $users = $this->getUsersToProcess($query); + $results = $this->processAllUsersCompliance($users, $query); + $results = $this->filterBySearchTerm($results, $query->getSearchTerm()); + $results = $this->sortArrayByQuery($results, $query); + + $dataTable = $this->buildDataTable($query, $results, $form); + + return $this->render('@Approval/working_time_act_ger_check.html.twig', array_merge($this->getDefaultTemplateParams($this->settingsTool), [ + "current_tab" => "working_time_act_ger_check", + "dataTable" => $dataTable, + ])); + } + + /** + * Get the list of users to process for compliance checking + * + * @param WorkingTimeActQuery $query + * @return User[] + */ + private function getUsersToProcess(WorkingTimeActQuery $query): array + { + return $query->getUsers() ?: $this->userRepository->findAll(); + } + + /** + * Process compliance for all users + * + * @param User[] $users + * @param WorkingTimeActQuery $query + * @return array + */ + private function processAllUsersCompliance(array $users, WorkingTimeActQuery $query): array + { + $results = []; + $hasCustomDateRange = $this->hasCustomDateRange($query); + + foreach ($users as $user) { + $result = $hasCustomDateRange + ? $this->processCustomDateRangeCompliance($user, $query) + : $this->processDefaultPeriodsCompliance($user); + + $results[] = $result; + } + + return $results; + } + + /** + * Check if query has a custom date range + * + * @param WorkingTimeActQuery $query + * @return bool + */ + private function hasCustomDateRange(WorkingTimeActQuery $query): bool + { + return $query->getDateRange() + && $query->getDateRange()->getBegin() + && $query->getDateRange()->getEnd(); + } + + /** + * Process compliance for custom date range + * + * @param User $user + * @param WorkingTimeActQuery $query + * @return array + */ + private function processCustomDateRangeCompliance(User $user, WorkingTimeActQuery $query): array + { + $periodStart = $query->getDateRange()->getBegin(); + $periodEnd = $query->getDateRange()->getEnd(); + + $timesheets = $this->getTimesheets($user, $periodStart, $periodEnd); + $publicHolidayGroupId = $this->getPublicHolidayGroupId($user); + + $complianceResult = $this->workingTimeActToolGER->checkWorkingTimeActToolGERCompliance( + $timesheets, + $publicHolidayGroupId, + $periodStart, + $periodEnd + ); + + return [ + 'user' => $user->getDisplayName(), + 'average_daterange' => round($complianceResult['average'], 2), + ]; + } + + /** + * Process compliance for default periods (6 months and 24 weeks) + * + * @param User $user + * @return array + */ + private function processDefaultPeriodsCompliance(User $user): array + { + $periodEnd = new DateTime(); + $periodStart6Months = new DateTime('-6 month'); + $periodStart24Weeks = new DateTime('-24 week'); + + $publicHolidayGroupId = $this->getPublicHolidayGroupId($user); + + // Get timesheets for 6 months (covers both periods) + $timesheets6Months = $this->getTimesheets($user, $periodStart6Months, $periodEnd); + + $complianceResult6Months = $this->workingTimeActToolGER->checkWorkingTimeActToolGERCompliance( + $timesheets6Months, + $publicHolidayGroupId, + $periodStart6Months, + $periodEnd + ); + + // Filter timesheets for 24 weeks period + $timesheets24Weeks = $this->filterTimesheetsByDate($timesheets6Months, $periodStart24Weeks); + + $complianceResult24Weeks = $this->workingTimeActToolGER->checkWorkingTimeActToolGERCompliance( + $timesheets24Weeks, + $publicHolidayGroupId, + $periodStart24Weeks, + $periodEnd + ); + + return [ + 'user' => $user->getDisplayName(), + 'average_6months' => round($complianceResult6Months['average'], 2), + 'average_24weeks' => round($complianceResult24Weeks['average'], 2), + 'compliance' => $complianceResult6Months['compliance'] && $complianceResult24Weeks['compliance'], + ]; + } + + /** + * Get public holiday group ID for a user + * + * @param User $user + * @return int|null + */ + private function getPublicHolidayGroupId(User $user): ?int + { + return $user->getPublicHolidayGroup() ? (int) $user->getPublicHolidayGroup() : null; + } + + /** + * Filter timesheets by minimum date + * + * @param array $timesheets + * @param DateTime $minDate + * @return array + */ + private function filterTimesheetsByDate(array $timesheets, DateTime $minDate): array + { + return array_filter($timesheets, function ($timesheet) use ($minDate) { + return $timesheet->getBegin() >= $minDate; + }); + } + + private function getTimesheets(?User $selectedUser, DateTime $start, DateTime $end): array + { + $timesheetQuery = new TimesheetQuery(); + $timesheetQuery->setUser($selectedUser); + $dateRange = new DateRange(); + $dateRange->setBegin($start); + $dateRange->setEnd($end); + $timesheetQuery->setDateRange($dateRange); + $timesheetQuery->setOrderBy('date'); + $timesheetQuery->setOrderBy('begin'); + $timesheetQuery->setOrder(BaseQuery::ORDER_ASC); + + return $this->timesheetRepository->getTimesheetsForQuery($timesheetQuery); + + } + + /** + * Check ArbZG compliance for timesheets + */ + /** + * Build DataTable for Working Time Act compliance results + */ + private function buildDataTable(WorkingTimeActQuery $query, array $rows, FormInterface $form): DataTable + { + $dataTable = new DataTable('working_time_act_check', $query); + $dataTable->deactivateConfiguration(); + $dataTable->addColumn("user", ['label' => 'label.user']); + + if ($query->getDateRange() && $query->getDateRange()->getBegin() && $query->getDateRange()->getEnd()) { + $dataTable->addColumn("label.average_daterange", ['label' => 'label.average_daterange']); + } else { + $dataTable->addColumn("label.average_6months", ['label' => 'label.average_6months']); + $dataTable->addColumn("label.average_24weeks", ['label' => 'label.average_24']); + $dataTable->addColumn("label.compliance", ['label' => 'label.compliance', 'type' => 'boolean']); + } + + $adapter = new ArrayAdapter($rows); + $pagination = new Pagination($adapter); + $dataTable->setPagination($pagination); + $dataTable->setSearchForm($form); + + return $dataTable; + } + + protected function getToolbarForm(WorkingTimeActQuery $query): FormInterface + { + return $this->createSearchForm(WorkingTimeActToolbarForm::class, $query, [ + 'action' => $this->generateUrl('approval_bundle_working_time_act_ger', [ + 'page' => $query->getPage(), + ]), + 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), + ]); + } + + private function filterBySearchTerm(array $rows, ?SearchTerm $searchTerm): array + { + if ($searchTerm === null || empty($searchTerm->getSearchTerm())) { + return $rows; + } + + $searchParts = $searchTerm->getParts(); + + return array_filter($rows, function ($row) use ($searchParts) { + foreach ($searchParts as $part) { + if (!$this->rowMatchesSearchTerm($row, mb_strtolower($part->getTerm()))) { + return false; + } + } + return true; + }); + } + + private function rowMatchesSearchTerm(array $row, string $term): bool + { + $searchableFields = [ + mb_strtolower($row['user'] ?? ''), + (string) ($row['average_6months'] ?? ''), + (string) ($row['average_24weeks'] ?? ''), + ]; + + foreach ($searchableFields as $field) { + if (str_contains($field, $term)) { + return true; + } + } + + return false; + } + + private function sortArrayByQuery(array $rows, WorkingTimeActQuery $query): array + { + $orderBy = $query->getOrderBy(); + $order = $query->getOrder(); + + if (!$orderBy || !in_array($orderBy, WorkingTimeActQuery::ORDER_ALLOWED, true)) { + return $rows; + } + + usort($rows, function ($a, $b) use ($orderBy, $order) { + $valueA = $a[$orderBy] ?? null; + $valueB = $b[$orderBy] ?? null; + + if (is_string($valueA) || is_string($valueB)) { + $valueA = mb_strtolower((string) $valueA); + $valueB = mb_strtolower((string) $valueB); + } + + if ($valueA == $valueB) { + return 0; + } + + $result = ($valueA < $valueB) ? -1 : 1; + return $order === BaseQuery::ORDER_DESC ? -$result : $result; + }); + + return $rows; + } + +} \ No newline at end of file diff --git a/Form/Toolbar/WorkingTimeActToolbarForm.php b/Form/Toolbar/WorkingTimeActToolbarForm.php new file mode 100644 index 0000000..dedec96 --- /dev/null +++ b/Form/Toolbar/WorkingTimeActToolbarForm.php @@ -0,0 +1,41 @@ +addSearchTermInputField($builder); + + $this->addDateRange($builder, ['timezone' => $options['timezone']]); + + $this->addUsersChoice($builder); + + $this->addHiddenPagination($builder); + + $query = $options['data']; + + if ($query instanceof WorkingTimeActQuery) { + $this->addOrder($builder); + $this->addOrderBy($builder, $query->getAllowedOrderColumns()); + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => WorkingTimeActQuery::class, + 'csrf_protection' => false, + 'timezone' => date_default_timezone_get(), + ]); + } +} diff --git a/Repository/ApprovalRepository.php b/Repository/ApprovalRepository.php index 5e8b9cf..a1aaeb0 100644 --- a/Repository/ApprovalRepository.php +++ b/Repository/ApprovalRepository.php @@ -19,6 +19,7 @@ use KimaiPlugin\ApprovalBundle\Entity\ApprovalHistory; use KimaiPlugin\ApprovalBundle\Entity\ApprovalStatus; use KimaiPlugin\ApprovalBundle\Enumeration\ConfigEnum; +use KimaiPlugin\ApprovalBundle\Repository\Query\ApprovalQuery; use KimaiPlugin\ApprovalBundle\Settings\ApprovalSettingsInterface; use KimaiPlugin\ApprovalBundle\Toolbox\Formatting; use KimaiPlugin\ApprovalBundle\Toolbox\SettingsTool; @@ -782,6 +783,109 @@ public function getUserApprovals(?array $users, $startDate = null) return $parseToViewArray; } + public function getUserApprovalsFiltered(?array $users, ApprovalQuery $query, $startDate = null) + { + if (count($users) == 0) { + return []; + } + + $usersId = array_map(function ($user) { + return $user->getId(); + }, $users); + + $approval_workflow_start = $this->settingsTool->getConfiguration(ConfigEnum::APPROVAL_WORKFLOW_START); + if ($approval_workflow_start == '') { + $approval_workflow_start = '0000-01-01'; + } else { + $approval_workflow_start = (new DateTime($approval_workflow_start))->modify('-7 day')->format('Y-m-d'); + } + + if ($startDate !== null) { + $start = $startDate; + } else { + $start = $approval_workflow_start; + } + + $dateRangeStart = $query->getBegin() ?? null; + $dateRangeEnd = $query->getEnd() ?? null; + $searchTerm = $query->getSearchTerm(); + + + $em = $this->getEntityManager(); + $qb = $em->createQueryBuilder() + ->select('ap') + ->from(Approval::class, 'ap') + ->join('ap.user', 'u') + ->join('ap.history', 'ah') + ->join('ah.status', 'ast') + ->andWhere($em->getExpressionBuilder()->in('u.id', $usersId)) + ->andWhere("ast.name = '" . ApprovalStatus::SUBMITTED . "'") + ->andWhere('ap.startDate >= :begin') + ->setParameter('begin', $start); + + if ($dateRangeStart !== null) { + $qb->andWhere('ap.startDate >= :dateRangeStart') + ->setParameter('dateRangeStart', $dateRangeStart); + } + + if ($dateRangeEnd !== null) { + $qb->andWhere('ap.endDate <= :dateRangeEnd') + ->setParameter('dateRangeEnd', $dateRangeEnd); + } + + $approvedListFiltered = $qb + ->getQuery() + ->getResult(); + + $this->parseHistoryToOneElement($approvedListFiltered); + + if ($searchTerm !== null && !empty($searchTerm->getSearchTerm())) { + $searchParts = $searchTerm->getParts(); + $approvedListFiltered = array_filter( + $approvedListFiltered, + function ($item) use ($searchParts) { + foreach ($searchParts as $part) { + $term = mb_strtolower($part->getTerm()); + $matchedInItem = false; + + $userName = mb_strtolower($item->getUser()->getUsername() ?? ''); + if (str_contains($userName, $term)) { + $matchedInItem = true; + } + + $userAlias = mb_strtolower($item->getUser()->getAlias() ?? ''); + if (str_contains($userAlias, $term)) { + $matchedInItem = true; + } + + $weekLabel = mb_strtolower($this->formatting->parseDate($item->getStartDate()) ?? ''); + if (str_contains($weekLabel, $term)) { + $matchedInItem = true; + } + + if (!$matchedInItem) { + return false; + } + + } + return true; + } + ); + } + $approvedListFiltered = array_filter( + $approvedListFiltered, + function ($item) { + $status = mb_strtolower($item->getHistory()[0]->getStatus()->getName() ?? ''); + if ($status === mb_strtolower(ApprovalStatus::SUBMITTED)) { + return true; + } + return false; + } + ); + + return $approvedListFiltered; + } + private function getLastHistory(Approval $item) { $history = $item->getHistory(); @@ -821,7 +925,6 @@ public function getNextApproveWeek(User $user): ?string if (empty($allRows)) { return null; } - // Otherwise, when there are $allRows, get the one which would be next (located in the future) $prevWeekDay = end($allRows)['startDate']; $prev = strtotime($prevWeekDay . ' + 7 days'); @@ -832,6 +935,36 @@ public function getNextApproveWeek(User $user): ?string return date('Y-m-d', $prev); } + /** + * @param array $userIds + * @return Approval[] + */ + public function findApprovedForUsersAndDateRange(array $userIds, \DateTimeInterface $begin, \DateTimeInterface $end): array + { + if ($userIds === []) { + return []; + } + + $em = $this->getEntityManager(); + $qb = $em->createQueryBuilder() + ->select('ap') + ->from(Approval::class, 'ap') + ->join('ap.user', 'u') + ->join('ap.history', 'ah') + ->join('ah.status', 'ast') + ->andWhere($em->getExpressionBuilder()->in('u.id', ':userIds')) + ->andWhere('ap.startDate <= :end') + ->andWhere('ap.endDate >= :begin') + ->andWhere('ah.date = (SELECT MAX(ah2.date) FROM ' . ApprovalHistory::class . ' ah2 WHERE ah2.approval = ap)') + ->andWhere('ast.name = :status') + ->setParameter('userIds', $userIds) + ->setParameter('begin', $begin->format('Y-m-d')) + ->setParameter('end', $end->format('Y-m-d')) + ->setParameter('status', ApprovalStatus::APPROVED); + + return $qb->getQuery()->getResult(); + } + public function updateExpectedActualDurationForUser(User $user) { $approvals = $this->findBy(['user' => $user]); diff --git a/Repository/Query/ApprovalQuery.php b/Repository/Query/ApprovalQuery.php index d594a22..edb2536 100644 --- a/Repository/Query/ApprovalQuery.php +++ b/Repository/Query/ApprovalQuery.php @@ -2,6 +2,7 @@ namespace KimaiPlugin\ApprovalBundle\Repository\Query; use App\Entity\User; +use App\Form\Model\DateRange; use App\Repository\Query\BaseQuery; use App\Repository\Query\DateRangeTrait; use App\Repository\Query\DateRangeInterface; @@ -14,7 +15,7 @@ class ApprovalQuery extends BaseQuery implements DateRangeInterface /** * @var array */ - public array $users = []; + private array $users = []; private array $weeks = []; private array $statuses = []; @@ -26,6 +27,8 @@ public function __construct() ]); $this->setAllowedOrderColumns(self::APPROVAL_ORDER_ALLOWED); + + $this->dateRange = new DateRange(); } public function getStatus(): array @@ -45,4 +48,9 @@ public function getUsers(): array { return array_values($this->users); } + + public function setUsers(array $users): void + { + $this->users = $users; + } } \ No newline at end of file diff --git a/Repository/Query/WorkingTimeActQuery.php b/Repository/Query/WorkingTimeActQuery.php new file mode 100644 index 0000000..1da1c47 --- /dev/null +++ b/Repository/Query/WorkingTimeActQuery.php @@ -0,0 +1,42 @@ + + */ + private array $users = []; + + public function __construct() + { + $this->setDefaults([ + 'order' => self::ORDER_ASC, + 'orderBy' => 'user', + ]); + + $this->setAllowedOrderColumns(self::ORDER_ALLOWED); + } + + /** + * @return User[] + */ + public function getUsers(): array + { + return array_values($this->users); + } + + public function setUsers(array $users): void + { + $this->users = $users; + } +} diff --git a/Resources/translations/messages.de.xlf b/Resources/translations/messages.de.xlf index 9558846..5168773 100644 --- a/Resources/translations/messages.de.xlf +++ b/Resources/translations/messages.de.xlf @@ -126,6 +126,18 @@ stats.duration_actual Erfasste Dauer + + table.auto_approve_confirm + Möchten Sie %count% Wochen automatisch genehmigen? + + + table.auto_approve_result + %success% Wochen automatisch genehmigt, %fail% fehlgeschlagen. + + + table.auto_approve + Automatische Genehmigung + table.past_weeks Vergangene Wochen @@ -138,6 +150,30 @@ table.future_week Kommende Wochen + + table.has_errors_tooltip + Hat Errors + + + menu.working_time_act_ger_check + Arbeitszeitgesetz §3 Check + + + label.compliance + Einhaltung + + + label.average_6months + Durchschnitt 6 Monate + + + label.average_24weeks + Durchschnitt 24 Wochen + + + label.average_daterange + Durchschnitt Zeitraum + table.approve_user Benutzer diff --git a/Resources/translations/messages.en.xlf b/Resources/translations/messages.en.xlf index 9b84154..498972d 100644 --- a/Resources/translations/messages.en.xlf +++ b/Resources/translations/messages.en.xlf @@ -126,6 +126,18 @@ stats.duration_actual Actual duration + + table.auto_approve + Auto approval + + + table.auto_approve_confirm + Do you want to automatically approve %count% weeks? + + + table.auto_approve_result + %success% weeks automatically approved, %fail% failed. + table.past_weeks Past weeks @@ -138,6 +150,30 @@ table.future_week Upcoming weeks + + table.has_errors_tooltip + Has errors + + + menu.working_time_act_ger_check + Working time act §3 Check + + + label.compliance + Compliance + + + label.average_6months + Average 6 months + + + label.average_24weeks + Average 24 weeks + + + label.average_daterange + Average Daterange + table.approve_user User diff --git a/Resources/translations/messages.hr.xlf b/Resources/translations/messages.hr.xlf index 216bf17..77d977e 100644 --- a/Resources/translations/messages.hr.xlf +++ b/Resources/translations/messages.hr.xlf @@ -122,6 +122,18 @@ stats.duration_actual Stvarno trajanje + + table.auto_approve + Automatsko odobrenje + + + table.auto_approve_confirm + Želite li automatski odobriti %count% tjedana? + + + table.auto_approve_result + %success% tjedana automatski odobreno, %fail% neuspješno. + table.past_weeks Prošli tjedan @@ -134,6 +146,30 @@ table.future_week Nadolazećih tjedana + + table.has_errors_tooltip + Ima greške + + + menu.working_time_act_ger_check + Provjera članka 3 Zakona o radnom vremenu + + + label.compliance + Usklađenost + + + label.average_6months + Prosječno 6 mjeseci + + + label.average_24weeks + Prosječno 24 tjedna + + + label.average_daterange + Prosječni raspon datuma + table.approve_user Korisnik diff --git a/Resources/views/navigation.html.twig b/Resources/views/navigation.html.twig index 1aa3002..b1ebba6 100644 --- a/Resources/views/navigation.html.twig +++ b/Resources/views/navigation.html.twig @@ -37,4 +37,11 @@ {% endif %} + {% if showWorkingTimeActCheckGER %} + + {% endif %} diff --git a/Resources/views/report_by_user.html.twig b/Resources/views/report_by_user.html.twig index d7726b3..d295729 100644 --- a/Resources/views/report_by_user.html.twig +++ b/Resources/views/report_by_user.html.twig @@ -4,6 +4,10 @@ {% block report_title %}{{ report_title|trans }}{% endblock %} +{% set lastStatus = approve|last %} +{% set penultimateStatus = approve|length > 1 ? approve[approve|length - 2] : null %} + + {% block report %} {% if approvePreviousWeeksMessage and approvePreviousWeeksMessage < (current | date_format('Y-m-d')) %} {{ widgets.callout('danger', 'warning.add_to_approve_previous_weeks'|trans) }} @@ -18,7 +22,11 @@ {% endif %}
-
+