From 08add27484a468b99bd4fd785865b4d627e02229 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:37:34 +0200 Subject: [PATCH 01/42] Added data model structure for forecast report --- src/Model/Reports/ForecastReportData.php | 18 ++++++++++++++++ src/Model/Reports/ForecastReportIssueData.php | 21 +++++++++++++++++++ .../ForecastReportIssueVersionData.php | 19 +++++++++++++++++ .../Reports/ForecastReportProjectData.php | 19 +++++++++++++++++ .../Reports/ForecastReportWorklogData.php | 19 +++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 src/Model/Reports/ForecastReportData.php create mode 100644 src/Model/Reports/ForecastReportIssueData.php create mode 100644 src/Model/Reports/ForecastReportIssueVersionData.php create mode 100644 src/Model/Reports/ForecastReportProjectData.php create mode 100644 src/Model/Reports/ForecastReportWorklogData.php diff --git a/src/Model/Reports/ForecastReportData.php b/src/Model/Reports/ForecastReportData.php new file mode 100644 index 00000000..a13aa9d2 --- /dev/null +++ b/src/Model/Reports/ForecastReportData.php @@ -0,0 +1,18 @@ + */ + public ArrayCollection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } +} diff --git a/src/Model/Reports/ForecastReportIssueData.php b/src/Model/Reports/ForecastReportIssueData.php new file mode 100644 index 00000000..b2ed267e --- /dev/null +++ b/src/Model/Reports/ForecastReportIssueData.php @@ -0,0 +1,21 @@ + */ + public array $versions = []; + + public function __construct(string $issueTag) + { + $this->issueTag = $issueTag; + } +} diff --git a/src/Model/Reports/ForecastReportIssueVersionData.php b/src/Model/Reports/ForecastReportIssueVersionData.php new file mode 100644 index 00000000..553a6688 --- /dev/null +++ b/src/Model/Reports/ForecastReportIssueVersionData.php @@ -0,0 +1,19 @@ + */ + public array $worklogs = []; + + public function __construct(string $issueVersion) + { + $this->issueVersion = $issueVersion; + } +} diff --git a/src/Model/Reports/ForecastReportProjectData.php b/src/Model/Reports/ForecastReportProjectData.php new file mode 100644 index 00000000..60eda6b7 --- /dev/null +++ b/src/Model/Reports/ForecastReportProjectData.php @@ -0,0 +1,19 @@ + */ + public array $issues = []; + + public function __construct(string $projectId) + { + $this->projectId = $projectId; + } +} diff --git a/src/Model/Reports/ForecastReportWorklogData.php b/src/Model/Reports/ForecastReportWorklogData.php new file mode 100644 index 00000000..7ad5e69d --- /dev/null +++ b/src/Model/Reports/ForecastReportWorklogData.php @@ -0,0 +1,19 @@ + Date: Wed, 9 Oct 2024 10:38:45 +0200 Subject: [PATCH 02/42] Forecast report form data model --- src/Model/Reports/ForecastReportFormData.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Model/Reports/ForecastReportFormData.php diff --git a/src/Model/Reports/ForecastReportFormData.php b/src/Model/Reports/ForecastReportFormData.php new file mode 100644 index 00000000..9d79fdf2 --- /dev/null +++ b/src/Model/Reports/ForecastReportFormData.php @@ -0,0 +1,12 @@ + Date: Wed, 9 Oct 2024 10:38:59 +0200 Subject: [PATCH 03/42] Forecast report form type --- src/Form/ForecastReportType.php | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/Form/ForecastReportType.php diff --git a/src/Form/ForecastReportType.php b/src/Form/ForecastReportType.php new file mode 100644 index 00000000..7d7f22ea --- /dev/null +++ b/src/Form/ForecastReportType.php @@ -0,0 +1,87 @@ +dataProviderRepository->findAll(); + $defaultProvider = $this->dataProviderRepository->find($this->defaultDataProvider); + + if (null === $defaultProvider && count($dataProviders) > 0) { + $defaultProvider = $dataProviders[0]; + } + $builder + ->add('dataProvider', EntityType::class, [ + 'class' => DataProvider::class, + 'required' => false, + 'label' => 'workload_report.select_data_provider', + 'label_attr' => ['class' => 'label'], + 'attr' => [ + 'class' => 'form-element', + ], + 'data' => $defaultProvider, + 'choices' => $dataProviders, + ]) + ->add('dateFrom', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'hour_report.from_date', + 'label_attr' => ['class' => 'label'], + 'by_reference' => true, + 'data' => new \DateTime(), + 'attr' => [ + 'class' => 'form-element', + 'onchange' => 'this.form.submit()', + ], + ]) + ->add('dateTo', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'hour_report.to_date', + 'label_attr' => ['class' => 'label'], + 'data' => new \DateTime(), + 'by_reference' => true, + 'attr' => [ + 'class' => 'form-element', + 'onchange' => 'this.form.submit()', + ], + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'workload_report.submit', + 'attr' => [ + 'class' => 'hour-report-submit button', + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ForecastReportFormData::class, + 'attr' => [ + 'data-sprint-report-target' => 'form', + ], + ]); + } +} From ddb2354b515f64a807f1a052eb0bb9d4207c5664 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:39:25 +0200 Subject: [PATCH 04/42] Forecast report service --- src/Service/ForecastReportService.php | 116 ++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/Service/ForecastReportService.php diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php new file mode 100644 index 00000000..dd96da38 --- /dev/null +++ b/src/Service/ForecastReportService.php @@ -0,0 +1,116 @@ +worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate); + $forecastReportData = new ForecastReportData(); + + foreach ($invoiceAttachedWorklogs as $worklog) { + $projectId = $worklog->getProject()->getId(); + + // Add project entry + if (!isset($forecastReportData->projects[$projectId])) { + $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); + $forecastReportData->projects[$projectId]->projectName = $worklog->getProject()->getName(); + } + + $currentProject = $forecastReportData->projects[$projectId]; + $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); + $isWorklogBilled = $worklog->isBilled(); + + // Count up total project hours + $currentProject->invoiced += $worklogTime; + if ($isWorklogBilled) { + $currentProject->invoicedAndRecorded += $worklogTime; + } + + $issueId = $worklog->getIssue()->getProjectTrackerKey(); + $issueLink = $worklog->getIssue()->getLinkToIssue(); + $issueTag = $worklog->getIssue()->getEpicName() ?: '[no tag]'; + + // Add issue entry + if (!isset($currentProject->issues[$issueTag])) { + $currentProject->issues[$issueTag] = new ForecastReportIssueData($issueTag); + $currentProject->issues[$issueTag]->issueId = $issueId; + $currentProject->issues[$issueTag]->issueLink = $issueLink; + } + + $currentIssue = $currentProject->issues[$issueTag]; + + $currentIssue->invoiced += $worklogTime; + if ($isWorklogBilled) { + $currentIssue->invoicedAndRecorded += $worklogTime; + } + + $issueVersions = $worklog->getIssue()->getVersions(); + $issueVersion = count($issueVersions) > 0 ? implode('', array_map(function($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; + + $issueVersionIdentifier = $issueTag.$issueVersion; + + if (!isset($currentIssue->versions[$issueVersion])) { + $currentIssue->versions[$issueVersion] = new ForecastReportIssueVersionData($issueVersion); + $currentIssue->versions[$issueVersion]->issueVersionIdentifier = $issueVersionIdentifier; + } + + $currentVersion = $currentIssue->versions[$issueVersion]; + + $currentVersion->invoiced += $worklogTime; + + if ($isWorklogBilled) { + $currentVersion->invoicedAndRecorded += $worklogTime; + } + + $worklogId = $worklog->getId(); + $workerEmail = $worklog->getWorker(); + $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); + $workerName = $worker->getName() ?? '[no worker]'; + $description = $worklog->getDescription(); + + if (!isset($currentVersion->worklogs[$worklogId])) { + $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData($worklogId, $description); + $currentVersion->worklogs[$worklogId]->worker = $workerName; + $currentVersion->worklogs[$worklogId]->description = $description; + } + + $currentWorklog = $currentVersion->worklogs[$worklogId]; + + $currentWorklog->invoiced += $worklogTime; + + if ($isWorklogBilled) { + $currentWorklog->invoicedAndRecorded += $worklogTime; + } + + // Add up grand totals + $forecastReportData->totalInvoiced += $worklogTime; + if ($isWorklogBilled) { + $forecastReportData->totalInvoicedAndRecorded += $worklogTime; + } + } + + return $forecastReportData; + } +} From 9df37f2541682773a0ab778dca36690255c0218a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:39:35 +0200 Subject: [PATCH 05/42] Forecast report template --- templates/reports/forecast_report.html.twig | 129 ++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 templates/reports/forecast_report.html.twig diff --git a/templates/reports/forecast_report.html.twig b/templates/reports/forecast_report.html.twig new file mode 100644 index 00000000..ffce3cbf --- /dev/null +++ b/templates/reports/forecast_report.html.twig @@ -0,0 +1,129 @@ +
+ + + + + + + + + + + {% for projectId, project in data.projects %} + + + + + + + + {% for issueId, issue in project.issues %} + + + + + + + {% for versionName, version in issue.versions %} + + + + + + + {% for worklogId, worklog in version.worklogs %} + + + + + + + {% endfor %} + {% endfor %} + {% endfor %} + {% endfor %} + + + + + + + +
{{ 'forecast_report.projects'|trans }} + {{ 'forecast_report.invoiced_hours'|trans }} + + {{ 'forecast_report.invoiced_recorded_hours'|trans }} + + {{ 'forecast_report.missing_hours'|trans }} +
+
+ + {{ project.projectName }} + + +
+
+ {{ project.invoiced }} + + {{ project.invoicedAndRecorded }} + + {{ project.invoiced - project.invoicedAndRecorded }} +
+
+ +   {{ issueId }} + + +
+
+ {{ issue.invoiced }} + + {{ issue.invoicedAndRecorded }} + + {{ issue.invoiced - issue.invoicedAndRecorded }} +
+
+ +   {{ versionName }} + + +
+
+ {{ version.invoiced }} + + {{ version.invoicedAndRecorded }} + + {{ version.invoiced - version.invoicedAndRecorded }} +
+
+ + {{ worklog.worker }} [{{ issue.issueId }}]
{{ worklog.description }} +
+
+
+ {{ worklog.invoiced }} + + {{ worklog.invoicedAndRecorded }} + + {{ worklog.invoiced - worklog.invoicedAndRecorded }} +
{{ 'forecast_report.total'|trans }}{{ data.totalInvoiced }}{{ data.totalInvoicedAndRecorded }}{{ data.totalInvoiced - data.totalInvoicedAndRecorded }}
+
From 14d521a9621611124720025486991070693b214a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:41:44 +0200 Subject: [PATCH 06/42] Added whitespace formatting in IssueRepository --- src/Repository/IssueRepository.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 72a0705f..f0ed294a 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -118,4 +118,5 @@ public function findIssuesInDateRange(string $startDate, string $endDate) ->getQuery() ->getResult(); } + } From e074da889456d8ee14b99aa89f6b161d5304cab7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:41:44 +0200 Subject: [PATCH 07/42] Added getWorklogsAttachedToInvoiceInDateRange method to WorklogRepository --- src/Repository/WorklogRepository.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index 0a8187bd..d418a515 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -133,4 +133,24 @@ public function findBillableWorklogsByWorkerAndDateRange(string $workerIdentifie ]) ->getQuery()->getResult(); } + + /** + * @throws \Exception + */ + public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $periodStart, \DateTimeInterface $periodEnd) + { + $from = new \DateTimeImmutable($periodStart->format('Y-m-d') . ' 00:00:00'); + $to = new \DateTimeImmutable($periodEnd->format('Y-m-d') . ' 23:59:59'); + + return $this->createQueryBuilder('worklog') + ->leftJoin('App\Entity\Issue', 'issue', 'WITH', 'worklog.issue = issue.id') + ->leftJoin('App\Entity\Project', 'project', 'WITH', 'issue.project = project.id') + ->where('worklog.invoiceEntry IS NOT NULL') + ->andWhere('worklog.started BETWEEN :from AND :to') + ->setParameter('from', $from) + ->setParameter('to', $to) + ->getQuery() + ->getResult(); + + } } From 61e42739b6ddcc04b284f8c88334f63c29a84600 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:41:44 +0200 Subject: [PATCH 08/42] Added navigation item for forecast report in navigation.html.twig --- templates/components/navigation.html.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/components/navigation.html.twig b/templates/components/navigation.html.twig index 7341a2f7..076e9c8c 100644 --- a/templates/components/navigation.html.twig +++ b/templates/components/navigation.html.twig @@ -31,6 +31,7 @@ From 158d5cab880164c4869be1bf5ff24d07ed660b8f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:41:44 +0200 Subject: [PATCH 09/42] Added loading class to planning-wrapper in reports.html.twig --- templates/reports/reports.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index cb92b7a0..dc59fc85 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -26,7 +26,7 @@ {{ form_end(form) }} {% if data is not empty %} -
+
{% if mode is defined %} {{ include('reports/' ~ mode ~ '.html.twig') }} {% endif %} From 0953d9ec617d837f64bcc14562f44bf169c90e7e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:41:45 +0200 Subject: [PATCH 10/42] Added translations for forecast report in messages.da.yaml --- translations/messages.da.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index e06bc996..ecae0c7b 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -56,6 +56,7 @@ navigation: management_report: "Ledelsesrapport" planning_users: 'Brugere' planning_projects: 'Projekter' + forecast_report: "Forecast" hour_report: "Timerapport" workload_report: "Normtidsrapport" worker: "Medarbejdere" @@ -776,3 +777,10 @@ subscription: edit: 'Rediger' monthly: 'Månedligt' quarterly: 'Kvartalsvist' + +forecast_report: + projects: 'Projekter' + invoiced_hours: 'Fakturerede timer' + invoiced_recorded_hours: 'Bogførte timer' + missing_hours: 'Rest timer' + total: 'Total' From bdae0a5356990efdfedec4d9b0907575bd38ab59 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 10:47:37 +0200 Subject: [PATCH 11/42] Added forecast report controller --- src/Controller/ForecastReportController.php | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/Controller/ForecastReportController.php diff --git a/src/Controller/ForecastReportController.php b/src/Controller/ForecastReportController.php new file mode 100644 index 00000000..dc8f7fb6 --- /dev/null +++ b/src/Controller/ForecastReportController.php @@ -0,0 +1,59 @@ +createForm(ForecastReportType::class, $reportFormData, [ + 'action' => $this->generateUrl('app_forecast_report'), + 'method' => 'GET', + 'attr' => [ + 'id' => 'sprint_report', + ], + 'csrf_protection' => false, + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $fromDate = $form->get('dateFrom')->getData() ?? null; + $toDate = $form->get('dateTo')->getData() ?? null; + + $reportData = $this->forecastReportService->getForecastReport($fromDate, $toDate); + } + + return $this->render('reports/reports.html.twig', [ + 'controller_name' => 'ForecastReportController', + 'form' => $form, + 'error' => $error, + 'data' => $reportData, + 'mode' => $mode, + ]); + } +} From 6a8520f644d88c8ea4ecb9abf520c396dbef5c15 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 11:04:34 +0200 Subject: [PATCH 12/42] Refactor code and improve comments for readability --- src/Repository/WorklogRepository.php | 9 ++++---- src/Service/ForecastReportService.php | 32 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index d418a515..942e8087 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -139,18 +139,17 @@ public function findBillableWorklogsByWorkerAndDateRange(string $workerIdentifie */ public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $periodStart, \DateTimeInterface $periodEnd) { - $from = new \DateTimeImmutable($periodStart->format('Y-m-d') . ' 00:00:00'); - $to = new \DateTimeImmutable($periodEnd->format('Y-m-d') . ' 23:59:59'); + $from = new \DateTimeImmutable($periodStart->format('Y-m-d').' 00:00:00'); + $to = new \DateTimeImmutable($periodEnd->format('Y-m-d').' 23:59:59'); return $this->createQueryBuilder('worklog') - ->leftJoin('App\Entity\Issue', 'issue', 'WITH', 'worklog.issue = issue.id') - ->leftJoin('App\Entity\Project', 'project', 'WITH', 'issue.project = project.id') + ->leftJoin(Issue::class, 'issue', 'WITH', 'worklog.issue = issue.id') + ->leftJoin(Project::class, 'project', 'WITH', 'issue.project = project.id') ->where('worklog.invoiceEntry IS NOT NULL') ->andWhere('worklog.started BETWEEN :from AND :to') ->setParameter('from', $from) ->setParameter('to', $to) ->getQuery() ->getResult(); - } } diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index dd96da38..749831ed 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -27,90 +27,112 @@ public function getForecastReport(?\DateTimeInterface $fromDate, ?\DateTimeInter { // Get all worklogs attached to an invoice for the period $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate); + + // Create an new instance of ForecastReportData $forecastReportData = new ForecastReportData(); + // Loop through each worklog foreach ($invoiceAttachedWorklogs as $worklog) { $projectId = $worklog->getProject()->getId(); - // Add project entry + // If the project isn't t already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); $forecastReportData->projects[$projectId]->projectName = $worklog->getProject()->getName(); } + // Get current project from forecast $currentProject = $forecastReportData->projects[$projectId]; + + // Calculate worklog time in hours $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); + + // Check if worklog is billed $isWorklogBilled = $worklog->isBilled(); - // Count up total project hours + // Tally up total project hours based on whether the worklog is billed $currentProject->invoiced += $worklogTime; if ($isWorklogBilled) { $currentProject->invoicedAndRecorded += $worklogTime; } + // Get issue details from worklog $issueId = $worklog->getIssue()->getProjectTrackerKey(); $issueLink = $worklog->getIssue()->getLinkToIssue(); $issueTag = $worklog->getIssue()->getEpicName() ?: '[no tag]'; - // Add issue entry + // Add issue in the project if it does not exist if (!isset($currentProject->issues[$issueTag])) { $currentProject->issues[$issueTag] = new ForecastReportIssueData($issueTag); $currentProject->issues[$issueTag]->issueId = $issueId; $currentProject->issues[$issueTag]->issueLink = $issueLink; } + // Get current issue from project $currentIssue = $currentProject->issues[$issueTag]; + // Add up the invoiced hours to the current issue $currentIssue->invoiced += $worklogTime; if ($isWorklogBilled) { $currentIssue->invoicedAndRecorded += $worklogTime; } + // Get version details from issue $issueVersions = $worklog->getIssue()->getVersions(); - $issueVersion = count($issueVersions) > 0 ? implode('', array_map(function($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; + $issueVersion = count($issueVersions) > 0 ? implode('', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; $issueVersionIdentifier = $issueTag.$issueVersion; + // Add version entry in the issue if it does not exist if (!isset($currentIssue->versions[$issueVersion])) { $currentIssue->versions[$issueVersion] = new ForecastReportIssueVersionData($issueVersion); $currentIssue->versions[$issueVersion]->issueVersionIdentifier = $issueVersionIdentifier; } + // Get the current version from issue $currentVersion = $currentIssue->versions[$issueVersion]; + // Add up invoiced hours in current version $currentVersion->invoiced += $worklogTime; + // If worklog is billed, add it to the recorded hours as well if ($isWorklogBilled) { $currentVersion->invoicedAndRecorded += $worklogTime; } + // Get worklog details $worklogId = $worklog->getId(); $workerEmail = $worklog->getWorker(); $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); $workerName = $worker->getName() ?? '[no worker]'; $description = $worklog->getDescription(); + // Add worklog entry in the version if it does not exist if (!isset($currentVersion->worklogs[$worklogId])) { $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData($worklogId, $description); $currentVersion->worklogs[$worklogId]->worker = $workerName; $currentVersion->worklogs[$worklogId]->description = $description; } + // Get the current worklog from the version $currentWorklog = $currentVersion->worklogs[$worklogId]; + // Add up invoiced hours in the current worklog $currentWorklog->invoiced += $worklogTime; + // If worklog is billed, add it to the recorded hours as well if ($isWorklogBilled) { $currentWorklog->invoicedAndRecorded += $worklogTime; } - // Add up grand totals + // Add up grand totals for the entire forecast $forecastReportData->totalInvoiced += $worklogTime; if ($isWorklogBilled) { $forecastReportData->totalInvoicedAndRecorded += $worklogTime; } } + // Return populated forecast report data return $forecastReportData; } } From f48ebcf5690fa2c9f1bbda17220532a0bc7d3ff5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:57 +0200 Subject: [PATCH 13/42] Added exception handling and date form changes in ForecastReportController --- src/Controller/ForecastReportController.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Controller/ForecastReportController.php b/src/Controller/ForecastReportController.php index dc8f7fb6..122a24a1 100644 --- a/src/Controller/ForecastReportController.php +++ b/src/Controller/ForecastReportController.php @@ -22,6 +22,9 @@ public function __construct( ) { } + /** + * @throws \Exception + */ #[Route('/', name: 'app_forecast_report')] public function index(Request $request): Response { @@ -42,8 +45,8 @@ public function index(Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $fromDate = $form->get('dateFrom')->getData() ?? null; - $toDate = $form->get('dateTo')->getData() ?? null; + $fromDate = $form->get('dateFrom')->getData(); + $toDate = $form->get('dateTo')->getData(); $reportData = $this->forecastReportService->getForecastReport($fromDate, $toDate); } From 74bac34f4708ef70cee46c5af75cf9f52c9a0f01 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:57 +0200 Subject: [PATCH 14/42] Modified data value providers for date fields in ForecastReportType form --- src/Form/ForecastReportType.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Form/ForecastReportType.php b/src/Form/ForecastReportType.php index 7d7f22ea..0c81d2aa 100644 --- a/src/Form/ForecastReportType.php +++ b/src/Form/ForecastReportType.php @@ -5,6 +5,7 @@ use App\Entity\DataProvider; use App\Model\Reports\ForecastReportFormData; use App\Repository\DataProviderRepository; +use App\Service\ForecastReportService; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -17,6 +18,7 @@ class ForecastReportType extends AbstractType public function __construct( private readonly DataProviderRepository $dataProviderRepository, private readonly ?string $defaultDataProvider, + private readonly ForecastReportService $forecastReportService, ) { } @@ -47,10 +49,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'hour_report.from_date', 'label_attr' => ['class' => 'label'], 'by_reference' => true, - 'data' => new \DateTime(), + 'data' => $options['fromDate'] ?? $this->forecastReportService->getDefaultFromDate(), 'attr' => [ 'class' => 'form-element', - 'onchange' => 'this.form.submit()', ], ]) ->add('dateTo', DateType::class, [ @@ -59,11 +60,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => 'hour_report.to_date', 'label_attr' => ['class' => 'label'], - 'data' => new \DateTime(), + 'data' => $options['fromDate'] ?? $this->forecastReportService->getDefaultToDate(), 'by_reference' => true, 'attr' => [ 'class' => 'form-element', - 'onchange' => 'this.form.submit()', ], ]) ->add('submit', SubmitType::class, [ From 07e417e78a29f9de3f44cf347909dfbe7cebc168 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:57 +0200 Subject: [PATCH 15/42] Removed unused ArrayCollection import in ForecastReportIssueData --- src/Model/Reports/ForecastReportIssueData.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Model/Reports/ForecastReportIssueData.php b/src/Model/Reports/ForecastReportIssueData.php index b2ed267e..5cb43b80 100644 --- a/src/Model/Reports/ForecastReportIssueData.php +++ b/src/Model/Reports/ForecastReportIssueData.php @@ -2,8 +2,6 @@ namespace App\Model\Reports; -use Doctrine\Common\Collections\ArrayCollection; - class ForecastReportIssueData { public string $issueId; From db4c494d045a8eb42d46f2dd50be8385306592de Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 16/42] Removed extra space in ForecastReportIssueVersionData --- src/Model/Reports/ForecastReportIssueVersionData.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/Reports/ForecastReportIssueVersionData.php b/src/Model/Reports/ForecastReportIssueVersionData.php index 553a6688..60bf7910 100644 --- a/src/Model/Reports/ForecastReportIssueVersionData.php +++ b/src/Model/Reports/ForecastReportIssueVersionData.php @@ -2,7 +2,6 @@ namespace App\Model\Reports; - class ForecastReportIssueVersionData { public string $issueVersion; From ef89016a3aa1b89f1bb2e98e0f116f41623fa6e9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 17/42] Removed extra space in ForecastReportProjectData --- src/Model/Reports/ForecastReportProjectData.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/Reports/ForecastReportProjectData.php b/src/Model/Reports/ForecastReportProjectData.php index 60eda6b7..eb0650bc 100644 --- a/src/Model/Reports/ForecastReportProjectData.php +++ b/src/Model/Reports/ForecastReportProjectData.php @@ -2,7 +2,6 @@ namespace App\Model\Reports; - class ForecastReportProjectData { public string $projectId; From c4bc99063712fcc1e90d2fce461b11acf53a3f80 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 18/42] Restructured properties and added comments in ForecastReportWorklogData --- src/Model/Reports/ForecastReportWorklogData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Reports/ForecastReportWorklogData.php b/src/Model/Reports/ForecastReportWorklogData.php index 7ad5e69d..8ca4252f 100644 --- a/src/Model/Reports/ForecastReportWorklogData.php +++ b/src/Model/Reports/ForecastReportWorklogData.php @@ -4,11 +4,11 @@ class ForecastReportWorklogData { - public string $description; public string $worker; public float $invoiced = 0.0; public float $invoicedAndRecorded = 0.0; + /** * @param $worklogId * @param $description From 100a92a941dc1546acf0f5019dd8a7b2df0adde5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 19/42] Removed trailing space in IssueRepository --- src/Repository/IssueRepository.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index f0ed294a..72a0705f 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -118,5 +118,4 @@ public function findIssuesInDateRange(string $startDate, string $endDate) ->getQuery() ->getResult(); } - } From 084dcdc8ff2f4af69cab075e9f0a9138af4bc88a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 20/42] Refactored getForecastReport function and added date helper functions in ForecastReportService --- src/Service/ForecastReportService.php | 58 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 749831ed..fb2c8891 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -2,7 +2,6 @@ namespace App\Service; -use App\Exception\EconomicsException; use App\Model\Reports\ForecastReportData; use App\Model\Reports\ForecastReportIssueData; use App\Model\Reports\ForecastReportIssueVersionData; @@ -20,10 +19,16 @@ public function __construct( } /** - * @throws EconomicsException + * Get forecast report data based on given date range. + * + * @param \DateTimeInterface $fromDate The start date of the period + * @param \DateTimeInterface $toDate The end date of the period + * + * @return ForecastReportData The forecast report data + * * @throws \Exception */ - public function getForecastReport(?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate): ForecastReportData + public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterface $toDate): ForecastReportData { // Get all worklogs attached to an invoice for the period $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate); @@ -35,15 +40,27 @@ public function getForecastReport(?\DateTimeInterface $fromDate, ?\DateTimeInter foreach ($invoiceAttachedWorklogs as $worklog) { $projectId = $worklog->getProject()->getId(); - // If the project isn't t already in the forecast, add it + if (!$projectId) { + throw new \Exception('Project id is null'); + } + $projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + + // If the project isn't already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); - $forecastReportData->projects[$projectId]->projectName = $worklog->getProject()->getName(); + } + + if (is_object($forecastReportData->projects[$projectId])) { + $forecastReportData->projects[$projectId]->projectName = $projectName; } // Get current project from forecast $currentProject = $forecastReportData->projects[$projectId]; + if (!$currentProject) { + throw new \Exception('Project instance was not found'); + } + // Calculate worklog time in hours $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); @@ -104,7 +121,10 @@ public function getForecastReport(?\DateTimeInterface $fromDate, ?\DateTimeInter $worklogId = $worklog->getId(); $workerEmail = $worklog->getWorker(); $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); - $workerName = $worker->getName() ?? '[no worker]'; + $workerName = $worker ? $worker->getName() : '[no worker]'; + if ($workerName === null) { + $workerName = '[no worker]'; + } $description = $worklog->getDescription(); // Add worklog entry in the version if it does not exist @@ -135,4 +155,30 @@ public function getForecastReport(?\DateTimeInterface $fromDate, ?\DateTimeInter // Return populated forecast report data return $forecastReportData; } + + /** + * Gets the first day of the last month. + * + * @return \DateTime The default from date + */ + public function getDefaultFromDate(): \DateTime + { + $fromDate = new \DateTime(); + $fromDate->modify('first day of last month'); + + return $fromDate; + } + + /** + * Gets the last day of the last month. + * + * @return \DateTime The default "to" date + */ + public function getDefaultToDate(): \DateTime + { + $fromDate = new \DateTime(); + $fromDate->modify('last day of last month'); + + return $fromDate; + } } From cc2042e6f26da66d368e5b31c5a51595d68659d9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:13:58 +0200 Subject: [PATCH 21/42] Added a new translation entry in messages.da.yaml --- translations/messages.da.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index ecae0c7b..1066f15b 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -779,6 +779,7 @@ subscription: quarterly: 'Kvartalsvist' forecast_report: + title: 'Forecast rapport' projects: 'Projekter' invoiced_hours: 'Fakturerede timer' invoiced_recorded_hours: 'Bogførte timer' From 456808338941d9dbd813e05571f970a1c91e9818 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:21:43 +0200 Subject: [PATCH 22/42] Refactored null check for workerName in ForecastReportService --- src/Service/ForecastReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index fb2c8891..ceed7cf2 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -122,7 +122,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $workerEmail = $worklog->getWorker(); $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); $workerName = $worker ? $worker->getName() : '[no worker]'; - if ($workerName === null) { + if (null === $workerName) { $workerName = '[no worker]'; } $description = $worklog->getDescription(); From bc5230ffffafafce2f12c44b19580c955da9f38f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 9 Oct 2024 16:22:47 +0200 Subject: [PATCH 23/42] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2567a38b..ce46feb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-175](https://github.com/itk-dev/economics/pull/175) + 2617: Added forecast report. * [PR-167](https://github.com/itk-dev/economics/pull/167) 2499: Added worker name in workload report. * [PR-166](https://github.com/itk-dev/economics/pull/166) From 4f4db4e77917b95127ee24287506cf745872e9cf Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 16 Oct 2024 12:45:41 +0200 Subject: [PATCH 24/42] Minor adjustments --- src/Service/ForecastReportService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index ceed7cf2..9694dc32 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -50,7 +50,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); } - if (is_object($forecastReportData->projects[$projectId])) { + if ($forecastReportData->projects[$projectId] instanceof ForecastReportProjectData) { $forecastReportData->projects[$projectId]->projectName = $projectName; } @@ -96,7 +96,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // Get version details from issue $issueVersions = $worklog->getIssue()->getVersions(); - $issueVersion = count($issueVersions) > 0 ? implode('', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; + $issueVersion = count($issueVersions) > 0 ? implode(', ', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; $issueVersionIdentifier = $issueTag.$issueVersion; From a1f856ea0a22cd30ab02e1bdb03aae59279cab33 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 16 Oct 2024 15:33:49 +0200 Subject: [PATCH 25/42] Added reports description --- reports.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 reports.md diff --git a/reports.md b/reports.md new file mode 100644 index 00000000..0a50dea8 --- /dev/null +++ b/reports.md @@ -0,0 +1,25 @@ +# Reports in Economics + +Simple overview of the various reports found in Economics. + +## Reports + +## Sprint rapport + +This report gives an overview of a project, detailing its name, version, and its work hours, both spent and remaining. It includes project budget details, if applicable. Tables illustrate the breakdown of work hours for each task within current and future sprints, giving a comprehensive view of project progress. + +## Ledelsesrapport + +This is a financial report providing an itemized overview of an organization's invoices by year and quarter. It offers yearly totals and individual quarter breakdowns. There's functionality to export data to excel. The report can be adjusted for specific timeframes, supporting comprehensive financial analysis. + +## Forecast rapport + +This report shows invoiced and recorded work hours for each project and issues within it. Hierarchical view allows expanding/collapsing for clarity. It indicates unbilled hours (difference between invoiced and recorded hours) and provides the total for all projects. + +## Timerapport + +This report provides an overview of estimated and logged work hours, grouped by project tags. Each tag corresponds to a project, with detailed hours per ticket. The report concludes with totals for each category over all project tags. + +## Normtidsrapport + +This is a workforce report providing a detailed breakdown of individual workloads over different periods. For each worker, their total workload and specific contribution percentages for each period are shown. The report also includes an average workload percentage. From 2b577812868317f73d2b1775ee0663af837110e3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 17 Oct 2024 08:59:01 +0200 Subject: [PATCH 26/42] Coding standards --- reports.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/reports.md b/reports.md index 0a50dea8..35b3f1ab 100644 --- a/reports.md +++ b/reports.md @@ -1,25 +1,33 @@ # Reports in Economics -Simple overview of the various reports found in Economics. +Overview of the various reports found in Economics. ## Reports -## Sprint rapport +### Sprint rapport -This report gives an overview of a project, detailing its name, version, and its work hours, both spent and remaining. It includes project budget details, if applicable. Tables illustrate the breakdown of work hours for each task within current and future sprints, giving a comprehensive view of project progress. +This report offers an overview of a project with its name, version, and work hours. It details hours +spent and remaining. If applicable, project budget details are included. The report shows tables with +work hours breakdown for each task in current and future sprints. Thus, giving a view of project progress. -## Ledelsesrapport +### Ledelsesrapport -This is a financial report providing an itemized overview of an organization's invoices by year and quarter. It offers yearly totals and individual quarter breakdowns. There's functionality to export data to excel. The report can be adjusted for specific timeframes, supporting comprehensive financial analysis. +This financial report provides an itemized overview of an organization's invoices by year and +quarter. It offers yearly totals and individual quarter breakdowns. There's functionality to export +data. The report timeframe can be adjusted, aiding comprehensive financial analysis. -## Forecast rapport +### Forecast rapport -This report shows invoiced and recorded work hours for each project and issues within it. Hierarchical view allows expanding/collapsing for clarity. It indicates unbilled hours (difference between invoiced and recorded hours) and provides the total for all projects. +This report presents invoiced and recorded work hours for each project and its issues. A +hierarchical view for clarity is available. It indicates unbilled hours and provides an overall total. -## Timerapport +### Timerapport -This report provides an overview of estimated and logged work hours, grouped by project tags. Each tag corresponds to a project, with detailed hours per ticket. The report concludes with totals for each category over all project tags. +This report offers an overview of estimated and logged work hours grouped by project tags. Each tag +represents a project and the detailed hours per ticket. The report concludes with totals. -## Normtidsrapport +### Normtidsrapport -This is a workforce report providing a detailed breakdown of individual workloads over different periods. For each worker, their total workload and specific contribution percentages for each period are shown. The report also includes an average workload percentage. +This workforce report provides a detailed breakdown of individual workloads over different +periods. It presents each worker's total workload and specific contribution percentages each period. +The report also includes an average workload percentage. From e66b4031aa6364ed1c64b33c6818fa887882d516 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 12:47:23 +0200 Subject: [PATCH 27/42] Removed unused DataProviderRepository from ForecastReportController --- src/Controller/ForecastReportController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Controller/ForecastReportController.php b/src/Controller/ForecastReportController.php index 122a24a1..bd721639 100644 --- a/src/Controller/ForecastReportController.php +++ b/src/Controller/ForecastReportController.php @@ -4,7 +4,6 @@ use App\Form\ForecastReportType; use App\Model\Reports\ForecastReportFormData; -use App\Repository\DataProviderRepository; use App\Service\ForecastReportService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -17,7 +16,6 @@ class ForecastReportController extends AbstractController { public function __construct( - private readonly DataProviderRepository $dataProviderRepository, private readonly ForecastReportService $forecastReportService, ) { } From 33c3703cdf1d1817aadd2d970018f75174dd0224 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 12:47:23 +0200 Subject: [PATCH 28/42] Removed DataProvider related fields and methods from ForecastReportType --- src/Form/ForecastReportType.php | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/Form/ForecastReportType.php b/src/Form/ForecastReportType.php index 0c81d2aa..165be48b 100644 --- a/src/Form/ForecastReportType.php +++ b/src/Form/ForecastReportType.php @@ -2,11 +2,8 @@ namespace App\Form; -use App\Entity\DataProvider; use App\Model\Reports\ForecastReportFormData; -use App\Repository\DataProviderRepository; use App\Service\ForecastReportService; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -16,32 +13,13 @@ class ForecastReportType extends AbstractType { public function __construct( - private readonly DataProviderRepository $dataProviderRepository, - private readonly ?string $defaultDataProvider, private readonly ForecastReportService $forecastReportService, ) { } public function buildForm(FormBuilderInterface $builder, array $options): void { - $dataProviders = $this->dataProviderRepository->findAll(); - $defaultProvider = $this->dataProviderRepository->find($this->defaultDataProvider); - - if (null === $defaultProvider && count($dataProviders) > 0) { - $defaultProvider = $dataProviders[0]; - } $builder - ->add('dataProvider', EntityType::class, [ - 'class' => DataProvider::class, - 'required' => false, - 'label' => 'workload_report.select_data_provider', - 'label_attr' => ['class' => 'label'], - 'attr' => [ - 'class' => 'form-element', - ], - 'data' => $defaultProvider, - 'choices' => $dataProviders, - ]) ->add('dateFrom', DateType::class, [ 'widget' => 'single_text', 'input' => 'datetime', From 89cbbd857f7401cc56d7385c733b656c4447f142 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 12:47:23 +0200 Subject: [PATCH 29/42] Removed DataProvider property from ForecastReportFormData --- src/Model/Reports/ForecastReportFormData.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/Reports/ForecastReportFormData.php b/src/Model/Reports/ForecastReportFormData.php index 9d79fdf2..a6889f5d 100644 --- a/src/Model/Reports/ForecastReportFormData.php +++ b/src/Model/Reports/ForecastReportFormData.php @@ -6,7 +6,6 @@ class ForecastReportFormData { - public DataProvider $dataProvider; public \DateTimeInterface $dateFrom; public \DateTimeInterface $dateTo; } From 0842ff2713aced3cd727c794f59f951fad8fcffc Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 12:47:23 +0200 Subject: [PATCH 30/42] Added condition for rendering form.dataProvider in reports.html.twig --- templates/reports/reports.html.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index 6bf62a78..af383810 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -22,7 +22,9 @@ {% endif %} {{ form_start(form) }} - {{ form_row(form.dataProvider) }} + {% if form.dataProvider is defined %} + {{ form_row(form.dataProvider) }} + {% endif %} {{ form_end(form) }} {% if data is not empty %} From 416ec3f6aa84f110ba80baa4cf51e77204241002 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 13:11:07 +0200 Subject: [PATCH 31/42] Added pagination to the method getWorklogsAttachedToInvoiceInDateRange in WorklogRepository --- src/Repository/WorklogRepository.php | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index 942e8087..c52a5616 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -10,6 +10,8 @@ use App\Model\Invoices\InvoiceEntryWorklogsFilterData; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\ORM\Tools\Pagination\Paginator; + /** * @extends ServiceEntityRepository @@ -134,15 +136,16 @@ public function findBillableWorklogsByWorkerAndDateRange(string $workerIdentifie ->getQuery()->getResult(); } + /** * @throws \Exception */ - public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $periodStart, \DateTimeInterface $periodEnd) + public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $periodStart, \DateTimeInterface $periodEnd, int $page = 1, int $pageSize = 50): array { $from = new \DateTimeImmutable($periodStart->format('Y-m-d').' 00:00:00'); $to = new \DateTimeImmutable($periodEnd->format('Y-m-d').' 23:59:59'); - return $this->createQueryBuilder('worklog') + $query = $this->createQueryBuilder('worklog') ->leftJoin(Issue::class, 'issue', 'WITH', 'worklog.issue = issue.id') ->leftJoin(Project::class, 'project', 'WITH', 'issue.project = project.id') ->where('worklog.invoiceEntry IS NOT NULL') @@ -150,6 +153,25 @@ public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $peri ->setParameter('from', $from) ->setParameter('to', $to) ->getQuery() - ->getResult(); + ->setFirstResult(($page - 1) * $pageSize) + ->setMaxResults($pageSize); + + $paginator = new Paginator($query, true); + + $totalItemCount = count($paginator); + $pagesCount = ceil($totalItemCount / $pageSize); + + $items = []; + foreach ($paginator as $post) { + $items[] = $post; + } + + return [ + 'total_count' => $totalItemCount, + 'pages_count' => $pagesCount, + 'current_page' => $page, + 'page_size' => $pageSize, + 'items' => $items, + ]; } } From a7e0f5afe70a585178ec6cfbd548f0928fe1ee4e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Wed, 23 Oct 2024 13:11:07 +0200 Subject: [PATCH 32/42] Modified getForecastReport method to use the paginated version of getWorklogsAttachedToInvoiceInDateRange --- src/Service/ForecastReportService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 9694dc32..35db0147 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -31,13 +31,21 @@ public function __construct( public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterface $toDate): ForecastReportData { // Get all worklogs attached to an invoice for the period - $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate); + $page = 1; + $pageSize = 50; + $allInvoiceAttachedWorklogs = []; + + do { + $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, $pageSize); + $allInvoiceAttachedWorklogs = array_merge($allInvoiceAttachedWorklogs, $invoiceAttachedWorklogs['items']); + ++$page; + } while ($page <= $invoiceAttachedWorklogs['pages_count']); // Create an new instance of ForecastReportData $forecastReportData = new ForecastReportData(); // Loop through each worklog - foreach ($invoiceAttachedWorklogs as $worklog) { + foreach ($allInvoiceAttachedWorklogs as $worklog) { $projectId = $worklog->getProject()->getId(); if (!$projectId) { From d86720868eb4e83c65c3b62462e9bc5badc469ba Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 07:39:08 +0200 Subject: [PATCH 33/42] Coding standards --- src/Model/Reports/ForecastReportFormData.php | 2 -- src/Repository/WorklogRepository.php | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Model/Reports/ForecastReportFormData.php b/src/Model/Reports/ForecastReportFormData.php index a6889f5d..031806f1 100644 --- a/src/Model/Reports/ForecastReportFormData.php +++ b/src/Model/Reports/ForecastReportFormData.php @@ -2,8 +2,6 @@ namespace App\Model\Reports; -use App\Entity\DataProvider; - class ForecastReportFormData { public \DateTimeInterface $dateFrom; diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index c52a5616..2465226b 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -9,9 +9,8 @@ use App\Enum\BillableKindsEnum; use App\Model\Invoices\InvoiceEntryWorklogsFilterData; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; use Doctrine\ORM\Tools\Pagination\Paginator; - +use Doctrine\Persistence\ManagerRegistry; /** * @extends ServiceEntityRepository @@ -136,7 +135,6 @@ public function findBillableWorklogsByWorkerAndDateRange(string $workerIdentifie ->getQuery()->getResult(); } - /** * @throws \Exception */ From 294093a06b94c8ccc567389bfdd6b92b62584c19 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 10:42:20 +0200 Subject: [PATCH 34/42] Refactored ForecastReportService to optimize memory usage --- src/Service/ForecastReportService.php | 242 +++++++++++++------------- 1 file changed, 121 insertions(+), 121 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 35db0147..adc78d72 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -9,12 +9,14 @@ use App\Model\Reports\ForecastReportWorklogData; use App\Repository\WorkerRepository; use App\Repository\WorklogRepository; +use Doctrine\ORM\EntityManagerInterface; class ForecastReportService { public function __construct( private readonly WorklogRepository $worklogRepository, private readonly WorkerRepository $workerRepository, + private readonly EntityManagerInterface $entityManager, ) { } @@ -33,132 +35,130 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // Get all worklogs attached to an invoice for the period $page = 1; $pageSize = 50; - $allInvoiceAttachedWorklogs = []; - - do { - $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, $pageSize); - $allInvoiceAttachedWorklogs = array_merge($allInvoiceAttachedWorklogs, $invoiceAttachedWorklogs['items']); - ++$page; - } while ($page <= $invoiceAttachedWorklogs['pages_count']); - // Create an new instance of ForecastReportData $forecastReportData = new ForecastReportData(); - // Loop through each worklog - foreach ($allInvoiceAttachedWorklogs as $worklog) { - $projectId = $worklog->getProject()->getId(); - - if (!$projectId) { - throw new \Exception('Project id is null'); - } - $projectName = $worklog->getProject()?->getName() ?? '[no project name]'; - - // If the project isn't already in the forecast, add it - if (!isset($forecastReportData->projects[$projectId])) { - $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); - } - - if ($forecastReportData->projects[$projectId] instanceof ForecastReportProjectData) { - $forecastReportData->projects[$projectId]->projectName = $projectName; - } - - // Get current project from forecast - $currentProject = $forecastReportData->projects[$projectId]; - - if (!$currentProject) { - throw new \Exception('Project instance was not found'); - } - - // Calculate worklog time in hours - $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); - - // Check if worklog is billed - $isWorklogBilled = $worklog->isBilled(); - - // Tally up total project hours based on whether the worklog is billed - $currentProject->invoiced += $worklogTime; - if ($isWorklogBilled) { - $currentProject->invoicedAndRecorded += $worklogTime; - } - - // Get issue details from worklog - $issueId = $worklog->getIssue()->getProjectTrackerKey(); - $issueLink = $worklog->getIssue()->getLinkToIssue(); - $issueTag = $worklog->getIssue()->getEpicName() ?: '[no tag]'; - - // Add issue in the project if it does not exist - if (!isset($currentProject->issues[$issueTag])) { - $currentProject->issues[$issueTag] = new ForecastReportIssueData($issueTag); - $currentProject->issues[$issueTag]->issueId = $issueId; - $currentProject->issues[$issueTag]->issueLink = $issueLink; - } - - // Get current issue from project - $currentIssue = $currentProject->issues[$issueTag]; - - // Add up the invoiced hours to the current issue - $currentIssue->invoiced += $worklogTime; - if ($isWorklogBilled) { - $currentIssue->invoicedAndRecorded += $worklogTime; - } - - // Get version details from issue - $issueVersions = $worklog->getIssue()->getVersions(); - $issueVersion = count($issueVersions) > 0 ? implode(', ', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; - - $issueVersionIdentifier = $issueTag.$issueVersion; - - // Add version entry in the issue if it does not exist - if (!isset($currentIssue->versions[$issueVersion])) { - $currentIssue->versions[$issueVersion] = new ForecastReportIssueVersionData($issueVersion); - $currentIssue->versions[$issueVersion]->issueVersionIdentifier = $issueVersionIdentifier; - } - - // Get the current version from issue - $currentVersion = $currentIssue->versions[$issueVersion]; - - // Add up invoiced hours in current version - $currentVersion->invoiced += $worklogTime; - - // If worklog is billed, add it to the recorded hours as well - if ($isWorklogBilled) { - $currentVersion->invoicedAndRecorded += $worklogTime; - } - - // Get worklog details - $worklogId = $worklog->getId(); - $workerEmail = $worklog->getWorker(); - $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); - $workerName = $worker ? $worker->getName() : '[no worker]'; - if (null === $workerName) { - $workerName = '[no worker]'; - } - $description = $worklog->getDescription(); - - // Add worklog entry in the version if it does not exist - if (!isset($currentVersion->worklogs[$worklogId])) { - $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData($worklogId, $description); - $currentVersion->worklogs[$worklogId]->worker = $workerName; - $currentVersion->worklogs[$worklogId]->description = $description; - } - - // Get the current worklog from the version - $currentWorklog = $currentVersion->worklogs[$worklogId]; - - // Add up invoiced hours in the current worklog - $currentWorklog->invoiced += $worklogTime; - - // If worklog is billed, add it to the recorded hours as well - if ($isWorklogBilled) { - $currentWorklog->invoicedAndRecorded += $worklogTime; - } + do { + $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, $pageSize); - // Add up grand totals for the entire forecast - $forecastReportData->totalInvoiced += $worklogTime; - if ($isWorklogBilled) { - $forecastReportData->totalInvoicedAndRecorded += $worklogTime; + foreach ($invoiceAttachedWorklogs['items'] as $worklog) { + // Loop through each worklog + $projectId = $worklog->getProject()->getId(); + + if (!$projectId) { + throw new \Exception('Project id is null'); + } + $projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + + // If the project isn't already in the forecast, add it + if (!isset($forecastReportData->projects[$projectId])) { + $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); + } + + if ($forecastReportData->projects[$projectId] instanceof ForecastReportProjectData) { + $forecastReportData->projects[$projectId]->projectName = $projectName; + } + + // Get current project from forecast + $currentProject = $forecastReportData->projects[$projectId]; + + if (!$currentProject) { + throw new \Exception('Project instance was not found'); + } + + // Calculate worklog time in hours + $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); + + // Check if worklog is billed + $isWorklogBilled = $worklog->isBilled(); + + // Tally up total project hours based on whether the worklog is billed + $currentProject->invoiced += $worklogTime; + if ($isWorklogBilled) { + $currentProject->invoicedAndRecorded += $worklogTime; + } + + // Get issue details from worklog + $issueId = $worklog->getIssue()->getProjectTrackerKey(); + $issueLink = $worklog->getIssue()->getLinkToIssue(); + $issueTag = $worklog->getIssue()->getEpicName() ?: '[no tag]'; + + // Add issue in the project if it does not exist + if (!isset($currentProject->issues[$issueTag])) { + $currentProject->issues[$issueTag] = new ForecastReportIssueData($issueTag); + $currentProject->issues[$issueTag]->issueId = $issueId; + $currentProject->issues[$issueTag]->issueLink = $issueLink; + } + + // Get current issue from project + $currentIssue = $currentProject->issues[$issueTag]; + + // Add up the invoiced hours to the current issue + $currentIssue->invoiced += $worklogTime; + if ($isWorklogBilled) { + $currentIssue->invoicedAndRecorded += $worklogTime; + } + + // Get version details from issue + $issueVersions = $worklog->getIssue()->getVersions(); + $issueVersion = count($issueVersions) > 0 ? implode(', ', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; + + $issueVersionIdentifier = $issueTag.$issueVersion; + + // Add version entry in the issue if it does not exist + if (!isset($currentIssue->versions[$issueVersion])) { + $currentIssue->versions[$issueVersion] = new ForecastReportIssueVersionData($issueVersion); + $currentIssue->versions[$issueVersion]->issueVersionIdentifier = $issueVersionIdentifier; + } + + // Get the current version from issue + $currentVersion = $currentIssue->versions[$issueVersion]; + + // Add up invoiced hours in current version + $currentVersion->invoiced += $worklogTime; + + // If worklog is billed, add it to the recorded hours as well + if ($isWorklogBilled) { + $currentVersion->invoicedAndRecorded += $worklogTime; + } + + // Get worklog details + $worklogId = $worklog->getId(); + $workerEmail = $worklog->getWorker(); + $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); + $workerName = $worker ? $worker->getName() : '[no worker]'; + if (null === $workerName) { + $workerName = '[no worker]'; + } + $description = $worklog->getDescription(); + + // Add worklog entry in the version if it does not exist + if (!isset($currentVersion->worklogs[$worklogId])) { + $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData($worklogId, $description); + $currentVersion->worklogs[$worklogId]->worker = $workerName; + $currentVersion->worklogs[$worklogId]->description = $description; + } + + // Get the current worklog from the version + $currentWorklog = $currentVersion->worklogs[$worklogId]; + + // Add up invoiced hours in the current worklog + $currentWorklog->invoiced += $worklogTime; + + // If worklog is billed, add it to the recorded hours as well + if ($isWorklogBilled) { + $currentWorklog->invoicedAndRecorded += $worklogTime; + } + + // Add up grand totals for the entire forecast + $forecastReportData->totalInvoiced += $worklogTime; + if ($isWorklogBilled) { + $forecastReportData->totalInvoicedAndRecorded += $worklogTime; + } } - } + $this->entityManager->clear(); + ++$page; + } while ($page <= $invoiceAttachedWorklogs['pages_count']); // Return populated forecast report data return $forecastReportData; From 5c10b98d29fdc12b113ec410aacee1d3e800c77a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 11:23:04 +0200 Subject: [PATCH 35/42] Changed pagesize from 50 to 200 --- src/Service/ForecastReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index adc78d72..69ee8cf6 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -34,7 +34,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa { // Get all worklogs attached to an invoice for the period $page = 1; - $pageSize = 50; + $pageSize = 200; // Create an new instance of ForecastReportData $forecastReportData = new ForecastReportData(); From d0fe61bbb18e637eecc776bf2208e813c0a4e584 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 12:07:41 +0200 Subject: [PATCH 36/42] Optimized worker name retrieval by moving operations outside of the loop. --- src/Service/ForecastReportService.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 69ee8cf6..b64189e4 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -32,12 +32,15 @@ public function __construct( */ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterface $toDate): ForecastReportData { - // Get all worklogs attached to an invoice for the period $page = 1; $pageSize = 200; - // Create an new instance of ForecastReportData $forecastReportData = new ForecastReportData(); + $allWorkers = $this->workerRepository->findAll(); + $workerNameMapping = array_reduce($this->workerRepository->findAll(), function ($carry, $worker) { + $carry[$worker->getEmail()] = $worker->getName() ?? '[no worker]'; + return $carry; + }, []); do { $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, $pageSize); @@ -125,11 +128,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // Get worklog details $worklogId = $worklog->getId(); $workerEmail = $worklog->getWorker(); - $worker = $this->workerRepository->findOneBy(['email' => $workerEmail]); - $workerName = $worker ? $worker->getName() : '[no worker]'; - if (null === $workerName) { - $workerName = '[no worker]'; - } + $workerName = $workerNameMapping[$workerEmail] ?? '[no worker]'; $description = $worklog->getDescription(); // Add worklog entry in the version if it does not exist From 2d87008e5002b610fb8b180c63e39ca66ee80754 Mon Sep 17 00:00:00 2001 From: Jeppe Julius Krogh <106669866+jeppekroghitk@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:08:23 +0200 Subject: [PATCH 37/42] Update src/Service/ForecastReportService.php Co-authored-by: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> --- src/Service/ForecastReportService.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index b64189e4..96d8a585 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -54,6 +54,11 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $projectName = $worklog->getProject()?->getName() ?? '[no project name]'; // If the project isn't already in the forecast, add it + if (!isset($forecastReportData->projects[$projectId])) { + $newForecastReportProjectData = new ForecastReportProjectData($projectId); + $newForecastReportProjectData.projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + $forecastReportData->projects[$projectId] = $newForecastReportProjectData; + } if (!isset($forecastReportData->projects[$projectId])) { $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); } From 9cddd131c193a5f1004bf89b38d453a93aa40d02 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 12:19:54 +0200 Subject: [PATCH 38/42] Refactor project name assignment in Forecast Report service --- src/Service/ForecastReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 96d8a585..0aedf907 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -56,7 +56,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // If the project isn't already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { $newForecastReportProjectData = new ForecastReportProjectData($projectId); - $newForecastReportProjectData.projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + $newForecastReportProjectData->projectName = $projectName; $forecastReportData->projects[$projectId] = $newForecastReportProjectData; } if (!isset($forecastReportData->projects[$projectId])) { From 50d9976dafefc4e4514f945d953a72c52eb35175 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 12:23:39 +0200 Subject: [PATCH 39/42] Clean up --- src/Service/ForecastReportService.php | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 0aedf907..87c876d6 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -39,6 +39,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $workerNameMapping = array_reduce($this->workerRepository->findAll(), function ($carry, $worker) { $carry[$worker->getEmail()] = $worker->getName() ?? '[no worker]'; + return $carry; }, []); do { @@ -51,22 +52,12 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa if (!$projectId) { throw new \Exception('Project id is null'); } - $projectName = $worklog->getProject()?->getName() ?? '[no project name]'; - // If the project isn't already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { - $newForecastReportProjectData = new ForecastReportProjectData($projectId); - $newForecastReportProjectData->projectName = $projectName; - $forecastReportData->projects[$projectId] = $newForecastReportProjectData; - } - if (!isset($forecastReportData->projects[$projectId])) { - $forecastReportData->projects[$projectId] = new ForecastReportProjectData($projectId); + $newForecastReportProjectData = new ForecastReportProjectData($projectId); + $newForecastReportProjectData->projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + $forecastReportData->projects[$projectId] = $newForecastReportProjectData; } - - if ($forecastReportData->projects[$projectId] instanceof ForecastReportProjectData) { - $forecastReportData->projects[$projectId]->projectName = $projectName; - } - // Get current project from forecast $currentProject = $forecastReportData->projects[$projectId]; From 79c474588157d58a175d1b4fcefa05b918c025e9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 24 Oct 2024 12:26:38 +0200 Subject: [PATCH 40/42] Removed unused variable --- src/Service/ForecastReportService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 87c876d6..3f538de4 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -35,8 +35,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $page = 1; $pageSize = 200; $forecastReportData = new ForecastReportData(); - $allWorkers = $this->workerRepository->findAll(); - + $workerNameMapping = array_reduce($this->workerRepository->findAll(), function ($carry, $worker) { $carry[$worker->getEmail()] = $worker->getName() ?? '[no worker]'; From 7414bc8e3960fc6113180718d4cc9b745626ec86 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 25 Oct 2024 08:38:45 +0200 Subject: [PATCH 41/42] Changed return value to be a direct Paginator object instead of an items array --- src/Repository/WorklogRepository.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index 2465226b..d108ddaf 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -159,17 +159,12 @@ public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $peri $totalItemCount = count($paginator); $pagesCount = ceil($totalItemCount / $pageSize); - $items = []; - foreach ($paginator as $post) { - $items[] = $post; - } - return [ 'total_count' => $totalItemCount, 'pages_count' => $pagesCount, 'current_page' => $page, 'page_size' => $pageSize, - 'items' => $items, + 'paginator' => $paginator, ]; } } From 9da95faf53f0a1c489021d82283834570cd78ab5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 25 Oct 2024 08:38:49 +0200 Subject: [PATCH 42/42] Refactored to use PAGE_SIZE constant and rearranged code for clarity --- src/Service/ForecastReportService.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 3f538de4..f67128d6 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -13,6 +13,8 @@ class ForecastReportService { + private const PAGE_SIZE = 200; + public function __construct( private readonly WorklogRepository $worklogRepository, private readonly WorkerRepository $workerRepository, @@ -33,18 +35,18 @@ public function __construct( public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterface $toDate): ForecastReportData { $page = 1; - $pageSize = 200; $forecastReportData = new ForecastReportData(); - + $workerNameMapping = array_reduce($this->workerRepository->findAll(), function ($carry, $worker) { $carry[$worker->getEmail()] = $worker->getName() ?? '[no worker]'; return $carry; }, []); + do { - $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, $pageSize); + $invoiceAttachedWorklogs = $this->worklogRepository->getWorklogsAttachedToInvoiceInDateRange($fromDate, $toDate, $page, self::PAGE_SIZE); - foreach ($invoiceAttachedWorklogs['items'] as $worklog) { + foreach ($invoiceAttachedWorklogs['paginator'] as $worklog) { // Loop through each worklog $projectId = $worklog->getProject()->getId();