From 1e136e8817182ca7ca2a5bbe5377ede0227086a3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 6 May 2024 13:28:26 +0200 Subject: [PATCH 001/134] set up new twig template for hour report. Modified reports controller --- src/Controller/ReportsController.php | 109 ++++++++++++++++++++++++-- templates/reports/project.html.twig | 112 +++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 templates/reports/project.html.twig diff --git a/src/Controller/ReportsController.php b/src/Controller/ReportsController.php index 7dbf9e9f..c481897c 100644 --- a/src/Controller/ReportsController.php +++ b/src/Controller/ReportsController.php @@ -1,26 +1,121 @@ render( +// 'reports/index.html.twig', +// $this->viewService->addView([]) +// ); +// } +// } + + namespace App\Controller; +use App\Entity\DataProvider; +use App\Exception\EconomicsException; +use App\Exception\UnsupportedDataProviderException; +use App\Form\PlanningType; +use App\Model\Planning\PlanningFormData; +use App\Repository\DataProviderRepository; +use App\Service\DataProviderService; use App\Service\ViewService; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; #[Route('/admin/reports')] class ReportsController extends AbstractController { public function __construct( + private readonly DataProviderService $dataProviderService, + private readonly DataProviderRepository $dataProviderRepository, private readonly ViewService $viewService, + private readonly ?string $planningDefaultDataProvider, ) { } - #[Route('', name: 'app_reports_index')] - public function display(): Response + #[Route('/', name: 'app_reports')] + public function index(Request $request): Response { - return $this->render( - 'reports/index.html.twig', - $this->viewService->addView([]) - ); + return $this->createResponse($request, 'project'); + } + + /** + * @throws UnsupportedDataProviderException + * @throws EconomicsException + */ + private function createResponse(Request $request, string $mode): Response + { + + // $planningFormData = new PlanningFormData(); + // $form = $this->createForm(PlanningType::class, $planningFormData); + + // $dataProviders = $this->dataProviderRepository->findAll(); + // $defaultProvider = $this->dataProviderRepository->find($this->planningDefaultDataProvider); + + // if (null === $defaultProvider && count($dataProviders) > 0) { + // $defaultProvider = $dataProviders[0]; + // } + + // $form->add('dataProvider', EntityType::class, [ + // 'class' => DataProvider::class, + // 'required' => true, + // 'label' => 'planning.data_provider', + // 'label_attr' => ['class' => 'label'], + // 'attr' => [ + // 'class' => 'form-element', + // ], + // 'help' => 'planning.data_provider_helptext', + // 'data' => $this->dataProviderRepository->find($this->planningDefaultDataProvider), + // 'choices' => $dataProviders, + // ]); + + // $form->handleRequest($request); + + // $service = null; + + // if ($form->isSubmitted() && $form->isValid()) { + // $service = $this->dataProviderService->getService($planningFormData->dataProvider); + // } elseif (null !== $defaultProvider) { + // $service = $this->dataProviderService->getService($defaultProvider); + // } + + // try { + // $planningData = $service?->getPlanningDataWeeks(); + // } catch (\Exception $e) { + // $error = $e->getMessage(); + // $planningData = null; + // } + + // return $this->render('planning/planning.html.twig', $this->viewService->addView([ + // 'controller_name' => 'ReportController', + // 'planningData' => $planningData, + // 'error' => $error ?? null, + // 'form' => $form, + // 'mode' => $mode, + // ])); + return $this->render('reports/project.html.twig', $this->viewService->addView([ + 'controller_name' => 'ReportController', + 'mode' => $mode, + ])); } } diff --git a/templates/reports/project.html.twig b/templates/reports/project.html.twig new file mode 100644 index 00000000..af1e0b58 --- /dev/null +++ b/templates/reports/project.html.twig @@ -0,0 +1,112 @@ +
+ + Hello World! :D + {# + + + {% for week in weeks %} + + {% endfor %} + + + {% for project in projects %} + + + + {% for week in weeks %} + {% set res = null %} + {% for week_number in week.weekCollection %} + {% if project.sprintSums.containsKey(week_number) %} + {% set res = res + project.sprintSums.get(week_number).sumHours %} + {% endif %} + {% endfor %} + + {% endfor %} + + {% for assignee in project.assignees %} + + + {% for week in weeks %} + + {% endfor %} + + {% for issue in assignee.issues %} + + + {% for week in weeks %} + + {% endfor %} + + {% endfor %} + {% endfor %} + + {% endfor %} #} +
{{ 'planning.projects'|trans }} + {{ week.displayName }} +
+
+ + {{ project.displayName }} + + + +
+
+ {{ res }} +
+
+ + {{ assignee.displayName }} + + +
+
+ {% set res = null %} + {% for week_number in week.weekCollection %} + {% if assignee.sprintSums.containsKey(week_number) %} + {% set res = res + assignee.sprintSums.get(week_number).sumHours %} + {% endif %} + {% endfor %} + {{ res }} +
+
+ + {{ issue.displayName }} + + #{{ issue.key }} +
+
+ {% for week_number in week.weekCollection %} + {% if issue.sprintId == week_number %} + {{ issue.remainingHours ?? 'UE' }} + {% endif %} + {% endfor %} +
+
From 8bc8f1b6a273cd42f9c3300f8f1ad2894c12ef35 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 7 May 2024 13:22:32 +0200 Subject: [PATCH 002/134] The variable name for default data provider was updated for more universal use. --- config/services.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index c393dd81..ecb8d0ab 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,7 +23,7 @@ services: $invoiceSupplierAccount: '%env(string:APP_INVOICE_SUPPLIER_ACCOUNT)%' $invoiceDefaultReceiverAccount: '%env(string:APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT)%' $projectBillingDefaultDescription: '%env(string:APP_PROJECT_BILLING_DEFAULT_DESCRIPTION)%' - $planningDefaultDataProvider: '%env(string:APP_DEFAULT_PLANNING_DATA_PROVIDER)%' + $defaultDataProvider: '%env(string:APP_DEFAULT_DATA_PROVIDER)%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name @@ -48,4 +48,3 @@ services: arguments: $options: standard_price: '%env(float:CLIENT_STANDARD_PRICE)%' - From e1fd834eb50fe2d5b58716d657b0e7f44cf3f7cf Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:05:27 +0200 Subject: [PATCH 003/134] added data models for hour report --- src/Model/Reports/HourReportData.php | 21 +++++++++++++++++ src/Model/Reports/HourReportProjectTag.php | 22 ++++++++++++++++++ src/Model/Reports/HourReportProjectTicket.php | 23 +++++++++++++++++++ src/Model/Reports/HourReportTimesheet.php | 19 +++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 src/Model/Reports/HourReportData.php create mode 100644 src/Model/Reports/HourReportProjectTag.php create mode 100644 src/Model/Reports/HourReportProjectTicket.php create mode 100644 src/Model/Reports/HourReportTimesheet.php diff --git a/src/Model/Reports/HourReportData.php b/src/Model/Reports/HourReportData.php new file mode 100644 index 00000000..8de90ac0 --- /dev/null +++ b/src/Model/Reports/HourReportData.php @@ -0,0 +1,21 @@ + */ + public ArrayCollection $projectTags; + + public function __construct(float $projectTotalSpent, float $projectTotalEstimated) + { + $this->projectTotalSpent = $projectTotalSpent; + $this->projectTotalEstimated = $projectTotalEstimated; + $this->projectTags = new ArrayCollection(); + } +} diff --git a/src/Model/Reports/HourReportProjectTag.php b/src/Model/Reports/HourReportProjectTag.php new file mode 100644 index 00000000..9eb76389 --- /dev/null +++ b/src/Model/Reports/HourReportProjectTag.php @@ -0,0 +1,22 @@ + */ + public ArrayCollection $projectTickets; + + public function __construct(float $totalEstimated, float $totalSpent, string $tag) + { + $this->totalEstimated = $totalEstimated; + $this->totalSpent = $totalSpent; + $this->tag = empty(trim($tag)) ? 'noTag' : $tag; + $this->projectTickets = new ArrayCollection(); + } +} diff --git a/src/Model/Reports/HourReportProjectTicket.php b/src/Model/Reports/HourReportProjectTicket.php new file mode 100644 index 00000000..395fa866 --- /dev/null +++ b/src/Model/Reports/HourReportProjectTicket.php @@ -0,0 +1,23 @@ +id = $id; + $this->headline = $headline; + $this->totalEstimated = $totalEstimated; + $this->totalSpent = $totalSpent; + $this->timesheets = new ArrayCollection(); + } +} diff --git a/src/Model/Reports/HourReportTimesheet.php b/src/Model/Reports/HourReportTimesheet.php new file mode 100644 index 00000000..dd2c83de --- /dev/null +++ b/src/Model/Reports/HourReportTimesheet.php @@ -0,0 +1,19 @@ +id = $id; + $this->hours = $hours; + $this->projectTicket = new ArrayCollection(); + } +} From 430e42cc9008779cbad9747e18219b230283e014 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:06:14 +0200 Subject: [PATCH 004/134] minor adjustments to navigation styling --- assets/styles/app.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/styles/app.css b/assets/styles/app.css index 49c96561..0bafa90d 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -371,7 +371,7 @@ */ .navigation-item { - @apply flex items-center p-2 text-base font-normal rounded-lg hover:bg-gray-100 hover:dark:bg-gray-500; + @apply flex items-center p-2 text-base font-normal rounded-lg hover:bg-gray-100 hover:dark:bg-gray-500 cursor-pointer; } .navigation-item.current { @@ -383,7 +383,7 @@ } .navigation-item-submenu.shown { - @apply flex flex-col visible pt-1 gap-y-1 px-10; + @apply flex flex-col visible pt-1 gap-y-1 pl-5; } .collapsible:has(+ .navigation-item-submenu):after { From 6092c1fb1605e92f6a77f60a5966e76ae020b710 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:07:31 +0200 Subject: [PATCH 005/134] modified reports menu point to be collapsible --- templates/components/navigation.html.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/components/navigation.html.twig b/templates/components/navigation.html.twig index 4b9393f4..34c3adea 100644 --- a/templates/components/navigation.html.twig +++ b/templates/components/navigation.html.twig @@ -26,7 +26,7 @@ {% if is_granted('ROLE_REPORT') %}
  • - + {% include 'components/icons.html.twig' with {icon: 'report', class: "w-6 h-6"} %} {{ 'navigation.reporting'|trans }} @@ -34,6 +34,7 @@ {% include 'components/navigation-item.html.twig' with {title: 'navigation.sprint_report'|trans, role: 'ROLE_REPORT', route: path('app_sprint_report', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.management_report'|trans, role: 'ROLE_REPORT', route: path('app_management_reports_create', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.team_report'|trans, role: 'ROLE_REPORT', route: path('app_team_reports_create', {}|merge(view is defined ? {view: view} : {}))} %} + {% include 'components/navigation-item.html.twig' with {title: 'navigation.hour_report'|trans, role: 'ROLE_REPORT', route: path('app_hour_report', {}|merge(view is defined ? {view: view} : {}))} %}
  • {% endif %} From 52e2892fe163ed35424778e1eb95695375bb1cec Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:15:34 +0200 Subject: [PATCH 006/134] reworked reports controller --- templates/reports/reports.html.twig | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 templates/reports/reports.html.twig diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig new file mode 100644 index 00000000..cc40ba89 --- /dev/null +++ b/templates/reports/reports.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'reports.hour.title'|trans }}{% endblock %} + +{% block content %} +

    {{ 'reports.hour.title'|trans }}

    + + {{ form_start(form) }} + {{ form_row(form.dataProvider) }} + {{ form_end(form) }} + + {% if error is not null %} + {% include 'components/alert.html.twig' with {level: "danger", text: error} %} + {% endif %} + {% if data is not empty %} +
    + {% if mode is defined %} + {% include 'reports/' ~ mode ~ '.html.twig' %} + {% endif %} +
    + {% endif %} +{% endblock %} From 191177529bb9adb5aa408c4a315b458960213b25 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:15:46 +0200 Subject: [PATCH 007/134] created new controller for hour report --- templates/reports/hourReport.html.twig | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 templates/reports/hourReport.html.twig diff --git a/templates/reports/hourReport.html.twig b/templates/reports/hourReport.html.twig new file mode 100644 index 00000000..fc68cf0b --- /dev/null +++ b/templates/reports/hourReport.html.twig @@ -0,0 +1,73 @@ +
    + + + + + + + + + + + {% for projectTag in data.projectTags %} + + + + + + + + + {% for projectTicket in projectTag.projectTickets %} + + + + + + {% endfor %} + + + {% endfor %} + + + + + + + +
    {{ 'planning.projects'|trans }} + Estimeret + + Logget +
    +
    + + {{ projectTag.tag }} + + + +
    +
    + {{ projectTag.totalEstimated }} + + {{ projectTag.totalSpent }} +
    +
    + + {{ projectTicket.headline }} + +
    +
    {{ projectTicket.totalEstimated }}{{ projectTicket.totalSpent }}
    Total{{ data.projectTotalEstimated }}{{ data.projectTotalSpent }}
    +
    From 18c69f35fed53df3116e8911ebfc2e828b2b8ceb Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 21 May 2024 15:16:21 +0200 Subject: [PATCH 008/134] created new controller for hour report --- src/Controller/HourReportController.php | 174 ++++++++++++++++++++++++ src/Form/ReportsType.php | 67 +++++++++ 2 files changed, 241 insertions(+) create mode 100644 src/Controller/HourReportController.php create mode 100644 src/Form/ReportsType.php diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php new file mode 100644 index 00000000..2a39cab4 --- /dev/null +++ b/src/Controller/HourReportController.php @@ -0,0 +1,174 @@ +createForm(ReportsFormData::class, $reportFormData, [ + 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), + 'method' => 'GET', + 'attr' => [ + 'id' => 'sprint_report', + ], + // Since this is only a filtering form, csrf is not needed. + 'csrf_protection' => false, + ]); + + $form->add('dataProvider', EntityType::class, [ + 'class' => DataProvider::class, + 'required' => false, + 'label' => 'reports.hour.select_data_provider', + 'label_attr' => ['class' => 'label'], + 'attr' => [ + 'onchange' => 'this.form.submit()', + 'class' => 'form-element', + ], + 'help' => 'sprint_report.data_provider_helptext', + 'choices' => $this->dataProviderRepository->findAll(), + ]); + $form->add('projectId', ChoiceType::class, [ + 'placeholder' => 'reports.hour.select_option', + 'choices' => [], + 'required' => false, + 'label' => 'reports.hour.select_project', + 'label_attr' => ['class' => 'label'], + 'attr' => [ + 'disabled' => true, + 'class' => 'form-element', + ], + ]); + $form->add('milestoneId', ChoiceType::class, [ + 'placeholder' => 'reports.hour.select_option', + 'choices' => [], + 'required' => false, + 'label' => 'reports.hour.select_milestone', + 'label_attr' => ['class' => 'label'], + 'attr' => [ + 'disabled' => true, + 'class' => 'form-element', + ], + ]); + + $requestData = $request->query->all('reports_form_data'); + + if (isset($requestData['sprint_report'])) { + $requestData = $requestData['sprint_report']; + } + if (!empty($requestData['dataProvider'])) { + $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); + + if (null != $dataProvider) { + $service = $this->dataProviderService->getService($dataProvider); + + $projectCollection = $service->getSprintReportProjects(); + + $projectChoices = []; + + foreach ($projectCollection->projects as $project) { + $projectChoices[$project->name] = $project->id; + } + + // Override projectId with element with choices. + $form->add('projectId', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'choices' => $projectChoices, + 'required' => false, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'row_attr' => ['class' => 'form-choices'], + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormProjectId', + 'data-choices-target' => 'choices', + 'onchange' => 'this.form.submit()', + ], + ]); + } + } + if (!empty($requestData['dataProvider']) && !empty($requestData['projectId'])) { + $service = $this->dataProviderService->getService($dataProvider); + $projectId = $requestData['projectId']; + + $milestoneCollection = $service->getSprintReportVersions($projectId); + $milestoneChoices = []; + $milestoneChoices['All milestones'] = 0; + foreach ($milestoneCollection->versions as $milestone) { + $milestoneChoices[$milestone->name] = $milestone->id; + } + + // Override projectId with element with choices. + $form->add('milestoneId', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'choices' => $milestoneChoices, + 'required' => false, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'row_attr' => ['class' => 'form-choices'], + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormMilestoneId', + 'data-choices-target' => 'choices', + 'onchange' => 'this.form.submit()', + ], + ]); + } + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $projectId = $form->get('projectId')->getData(); + $milestoneId = $form->has('milestoneId') ? $form->get('milestoneId')->getData() : null; + $dataProvider = $form->get('dataProvider')->getData(); + + if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { + $service = $this->dataProviderService->getService($dataProvider); + $reportData = $service?->getHourReportData($projectId, $milestoneId); + $mode = 'hourReport'; + } + + if (!empty($projectId) && !empty($dataProvider) && 0 === $milestoneId) { + $service = $this->dataProviderService->getService($dataProvider); + $reportData = $service?->getHourReportData($projectId); + $mode = 'hourReport'; + } + } + + return $this->render('reports/reports.html.twig', $this->viewService->addView([ + 'controller_name' => 'HourReportController', + 'form' => $form, + 'error' => $error ?? null, + 'data' => $reportData, + 'mode' => $mode, + ])); + } +} diff --git a/src/Form/ReportsType.php b/src/Form/ReportsType.php new file mode 100644 index 00000000..ef8f9ded --- /dev/null +++ b/src/Form/ReportsType.php @@ -0,0 +1,67 @@ +add('dataProvider') + ->add('projectId', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'required' => true, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormProjectId', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ]); + /* ->add('dateFrom', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'required' => true, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormProjectId', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ]) + ->add('dateTo', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'required' => true, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormProjectId', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ]);*/ + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ReportsFormData::class, + 'attr' => [ + 'class' => 'form-default', + ], + ]); + } +} From 45d6cf9132a026a735df96d7cc04e0503289e7ab Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 22 May 2024 11:37:36 +0200 Subject: [PATCH 009/134] 1484: Cleaned up worklog cleanup --- CHANGELOG.md | 2 ++ src/Service/DataSynchronizationService.php | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 190d8abe..f986b964 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-120](https://github.com/itk-dev/economics/pull/120) + 1484: Cleaned up worklog cleanup * [PR-118](https://github.com/itk-dev/economics/pull/118) 1485: Made product quantity floatable diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index 55a824d0..3472b53e 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -21,8 +21,6 @@ use App\Repository\ProjectRepository; use App\Repository\VersionRepository; use App\Repository\WorklogRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -293,13 +291,19 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac // * invoiced // // as candidates for deletion. - /** @var Collection $worklogsToDelete */ - $worklogsToDelete = new ArrayCollection( + // + // Due to flushing below, we store only this worklogs IDs + // (if we load worklog entities, they will become detached when flushing). + /** @var array $worklogsToDeleteIds */ + $worklogsToDeleteIds = array_map( + static fn (Worklog $worklog) => $worklog->getId(), array_filter( - $this->entityManager->getRepository(Worklog::class)->findBy(['project' => $project]), + $this->worklogRepository->findBy(['project' => $project]), static fn (Worklog $worklog): bool => !$worklog->isBilled() || null === $worklog->getInvoiceEntry() ) ); + // Index by ID + $worklogsToDeleteIds = array_combine($worklogsToDeleteIds, $worklogsToDeleteIds); $worklogData = $service->getWorklogDataCollection($projectTrackerId); $worklogsAdded = 0; @@ -340,7 +344,10 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $project->addWorklog($worklog); // Keep the worklog. - $worklogsToDelete->removeElement($worklog); + $worklogId = $worklog->getId(); + if (null !== $worklogId) { + unset($worklogsToDeleteIds[$worklogId]); + } if (null !== $progressCallback) { $progressCallback($worklogsAdded, count($worklogData->worklogData)); @@ -356,6 +363,7 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac } // Remove leftover worklogs from project and remove the worklogs. + $worklogsToDelete = $this->worklogRepository->findBy(['id' => $worklogsToDeleteIds]); foreach ($worklogsToDelete as $worklog) { $project->removeWorklog($worklog); $this->entityManager->remove($worklog); From b3bee99abb31c6f691018c5d3f3662ea7dfdfc10 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 22 May 2024 13:32:20 +0200 Subject: [PATCH 010/134] 1485: Updated numbers --- .env | 4 ++++ CHANGELOG.md | 2 ++ config/services.yaml | 7 +++++++ src/Controller/IssueController.php | 6 ++++-- src/Form/IssueProductType.php | 14 ++++++++++---- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8446720f..5ea67b5a 100644 --- a/.env +++ b/.env @@ -56,3 +56,7 @@ OIDC_CLI_LOGIN_ROUTE=index CLIENT_STANDARD_PRICE=705.00 DEFAULT_URI=https://economics.local.itkdev.dk/ + +PRODUCT_QUANTITY_MIN=0.25 +PRODUCT_QUANTITY_STEP=0.25 +PRODUCT_QUANTITY_SCALE=2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f986b964..6d4e5608 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-121](https://github.com/itk-dev/economics/pull/121) + 1485: Fixed floating number issues * [PR-120](https://github.com/itk-dev/economics/pull/120) 1484: Cleaned up worklog cleanup * [PR-118](https://github.com/itk-dev/economics/pull/118) diff --git a/config/services.yaml b/config/services.yaml index c393dd81..39aeb4b5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -49,3 +49,10 @@ services: $options: standard_price: '%env(float:CLIENT_STANDARD_PRICE)%' + App\Controller\IssueController: + arguments: + $options: + issue_product_type_options: + quantity_min: '%env(float:PRODUCT_QUANTITY_MIN)%' + quantity_step: '%env(float:PRODUCT_QUANTITY_STEP)%' + quantity_scale: '%env(int:PRODUCT_QUANTITY_SCALE)%' diff --git a/src/Controller/IssueController.php b/src/Controller/IssueController.php index c6fc9f6d..cf98e3c5 100644 --- a/src/Controller/IssueController.php +++ b/src/Controller/IssueController.php @@ -24,6 +24,7 @@ class IssueController extends AbstractController { public function __construct( private readonly ViewService $viewService, + private readonly array $options, ) { } @@ -47,6 +48,7 @@ public function index(Request $request, Project $project, IssueRepository $issue #[Route('/{id}', name: 'show', methods: [Request::METHOD_GET])] public function show(Project $project, Issue $issue): Response { + $issueProductTypeOptions = $this->options['issue_product_type_options'] ?? []; $product = (new IssueProduct()) ->setIssue($issue); $addProductForm = $project->getProducts()->isEmpty() @@ -58,7 +60,7 @@ public function show(Project $project, Issue $issue): Response 'id' => $issue->getId(), ]), 'method' => Request::METHOD_POST, - ]); + ] + $issueProductTypeOptions); // Index issue products by id. $products = []; @@ -77,7 +79,7 @@ public function show(Project $project, Issue $issue): Response 'product' => $product->getId(), ]), 'method' => Request::METHOD_PUT, - ]) + ] + $issueProductTypeOptions) ->createView(), $products ); diff --git a/src/Form/IssueProductType.php b/src/Form/IssueProductType.php index 1245edfd..e7aa300d 100644 --- a/src/Form/IssueProductType.php +++ b/src/Form/IssueProductType.php @@ -21,9 +21,9 @@ class IssueProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { - $quantityMin = $options['quantity_min'] ?? 0.1; - $quantityStep = $options['quantity_step'] ?? $quantityMin; - $quantityScale = (int) ceil(-log10($quantityStep)); + $quantityMin = $options['quantity_min']; + $quantityStep = $options['quantity_step']; + $quantityScale = $options['quantity_scale']; $builder ->add('product', EntityType::class, [ @@ -71,8 +71,14 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setDefaults([ 'data_class' => IssueProduct::class, + 'quantity_min' => 0.10, + 'quantity_step' => 0.05, + 'quantity_scale' => 2, ]) ->setRequired('project') - ->setAllowedTypes('project', Project::class); + ->setAllowedTypes('project', Project::class) + ->setAllowedTypes('quantity_min', 'float') + ->setAllowedTypes('quantity_step', 'float') + ->setAllowedTypes('quantity_scale', 'int'); } } From 5d3a826cad28b6bec8123ba3f36b96babe073e45 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 22 May 2024 13:58:38 +0200 Subject: [PATCH 011/134] 1485: Removed default value --- src/Entity/IssueProduct.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/IssueProduct.php b/src/Entity/IssueProduct.php index 4d618183..49360b7c 100644 --- a/src/Entity/IssueProduct.php +++ b/src/Entity/IssueProduct.php @@ -18,7 +18,7 @@ class IssueProduct extends AbstractBaseEntity private ?Product $product = null; #[ORM\Column] - private float $quantity = 1.0; + private ?float $quantity; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $description = null; From f0e62b80a6ed133a2754a94ab049a8005adde2ea Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 22 May 2024 14:37:40 +0200 Subject: [PATCH 012/134] 1485: Evicted HTML5 number field --- .env | 2 -- config/services.yaml | 2 -- src/Form/IssueProductType.php | 9 --------- 3 files changed, 13 deletions(-) diff --git a/.env b/.env index 5ea67b5a..3a5e6c54 100644 --- a/.env +++ b/.env @@ -57,6 +57,4 @@ CLIENT_STANDARD_PRICE=705.00 DEFAULT_URI=https://economics.local.itkdev.dk/ -PRODUCT_QUANTITY_MIN=0.25 -PRODUCT_QUANTITY_STEP=0.25 PRODUCT_QUANTITY_SCALE=2 diff --git a/config/services.yaml b/config/services.yaml index 39aeb4b5..71a2e3a4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -53,6 +53,4 @@ services: arguments: $options: issue_product_type_options: - quantity_min: '%env(float:PRODUCT_QUANTITY_MIN)%' - quantity_step: '%env(float:PRODUCT_QUANTITY_STEP)%' quantity_scale: '%env(int:PRODUCT_QUANTITY_SCALE)%' diff --git a/src/Form/IssueProductType.php b/src/Form/IssueProductType.php index e7aa300d..289b0914 100644 --- a/src/Form/IssueProductType.php +++ b/src/Form/IssueProductType.php @@ -21,8 +21,6 @@ class IssueProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { - $quantityMin = $options['quantity_min']; - $quantityStep = $options['quantity_step']; $quantityScale = $options['quantity_scale']; $builder @@ -43,11 +41,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], ]) ->add('quantity', NumberType::class, [ - 'html5' => true, 'scale' => $quantityScale, 'attr' => [ - 'min' => $quantityMin, - 'step' => $quantityStep, 'class' => 'number text-right', 'size' => 4, ], @@ -71,14 +66,10 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setDefaults([ 'data_class' => IssueProduct::class, - 'quantity_min' => 0.10, - 'quantity_step' => 0.05, 'quantity_scale' => 2, ]) ->setRequired('project') ->setAllowedTypes('project', Project::class) - ->setAllowedTypes('quantity_min', 'float') - ->setAllowedTypes('quantity_step', 'float') ->setAllowedTypes('quantity_scale', 'int'); } } From 967809d26cec0e7a54aff9a55446ea90e8106ac0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 27 May 2024 09:01:22 +0200 Subject: [PATCH 013/134] hourreportcontroller using new hourreport type and hourreportformdata --- src/Controller/HourReportController.php | 24 +++++++++++++++--------- src/Form/HourReportType.php | 0 src/Model/Reports/HourReportFormData.php | 12 ++++++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 src/Form/HourReportType.php create mode 100644 src/Model/Reports/HourReportFormData.php diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index 2a39cab4..df18542d 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\DataProvider; -use App\Model\Reports\ReportsFormData; +use App\Exception\EconomicsException; +use App\Exception\UnsupportedDataProviderException; +use App\Form\HourReportType; +use App\Model\Reports\HourReportFormData; use App\Repository\DataProviderRepository; use App\Service\DataProviderService; use App\Service\ViewService; @@ -25,14 +28,17 @@ public function __construct( ) { } + /** + * @throws EconomicsException + * @throws UnsupportedDataProviderException + */ #[Route('/', name: 'app_hour_report')] public function index(Request $request): Response { $reportData = null; $mode = 'reports'; - $reportFormData = new ReportsFormData(); - - $form = $this->createForm(ReportsFormData::class, $reportFormData, [ + $reportFormData = new HourReportFormData(); + $form = $this->createForm(HourReportType::class, $reportFormData, [ 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), 'method' => 'GET', 'attr' => [ @@ -65,7 +71,7 @@ public function index(Request $request): Response 'class' => 'form-element', ], ]); - $form->add('milestoneId', ChoiceType::class, [ + $form->add('versionId', ChoiceType::class, [ 'placeholder' => 'reports.hour.select_option', 'choices' => [], 'required' => false, @@ -77,7 +83,7 @@ public function index(Request $request): Response ], ]); - $requestData = $request->query->all('reports_form_data'); + $requestData = $request->query->all('hour_report'); if (isset($requestData['sprint_report'])) { $requestData = $requestData['sprint_report']; @@ -126,7 +132,7 @@ public function index(Request $request): Response } // Override projectId with element with choices. - $form->add('milestoneId', ChoiceType::class, [ + $form->add('versionId', ChoiceType::class, [ 'placeholder' => 'sprint_report.select_an_option', 'choices' => $milestoneChoices, 'required' => false, @@ -136,7 +142,7 @@ public function index(Request $request): Response 'attr' => [ 'class' => 'form-element', 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormMilestoneId', + 'data-action' => 'sprint-report#submitFormVersionId', 'data-choices-target' => 'choices', 'onchange' => 'this.form.submit()', ], @@ -147,7 +153,7 @@ public function index(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $projectId = $form->get('projectId')->getData(); - $milestoneId = $form->has('milestoneId') ? $form->get('milestoneId')->getData() : null; + $milestoneId = $form->has('versionId') ? $form->get('versionId')->getData() : null; $dataProvider = $form->get('dataProvider')->getData(); if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php new file mode 100644 index 00000000..e69de29b diff --git a/src/Model/Reports/HourReportFormData.php b/src/Model/Reports/HourReportFormData.php new file mode 100644 index 00000000..ca6a8b41 --- /dev/null +++ b/src/Model/Reports/HourReportFormData.php @@ -0,0 +1,12 @@ + Date: Mon, 27 May 2024 09:02:45 +0200 Subject: [PATCH 014/134] planningcontroller using defaultDataProvider --- src/Controller/PlanningController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controller/PlanningController.php b/src/Controller/PlanningController.php index 362122d3..f40382ca 100644 --- a/src/Controller/PlanningController.php +++ b/src/Controller/PlanningController.php @@ -23,7 +23,7 @@ public function __construct( private readonly DataProviderService $dataProviderService, private readonly DataProviderRepository $dataProviderRepository, private readonly ViewService $viewService, - private readonly ?string $planningDefaultDataProvider, + private readonly ?string $defaultDataProvider, ) { } @@ -63,7 +63,7 @@ private function createResponse(Request $request, string $mode): Response $form = $this->createForm(PlanningType::class, $planningFormData); $dataProviders = $this->dataProviderRepository->findAll(); - $defaultProvider = $this->dataProviderRepository->find($this->planningDefaultDataProvider); + $defaultProvider = $this->dataProviderRepository->find($this->defaultDataProvider); if (null === $defaultProvider && count($dataProviders) > 0) { $defaultProvider = $dataProviders[0]; @@ -78,7 +78,7 @@ private function createResponse(Request $request, string $mode): Response 'class' => 'form-element', ], 'help' => 'planning.data_provider_helptext', - 'data' => $this->dataProviderRepository->find($this->planningDefaultDataProvider), + 'data' => $this->dataProviderRepository->find($this->defaultDataProvider), 'choices' => $dataProviders, ]); From e751a0a979e3552af7c9918613a1767a6cb96b4b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 27 May 2024 09:03:19 +0200 Subject: [PATCH 015/134] better index report page --- templates/reports/index.html.twig | 35 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/templates/reports/index.html.twig b/templates/reports/index.html.twig index ef7a4850..ba0bb208 100644 --- a/templates/reports/index.html.twig +++ b/templates/reports/index.html.twig @@ -1,19 +1,30 @@ {% extends 'base.html.twig' %} -{% block title %}{{ 'reports.title'|trans }}{% endblock %} +{% block title %}{{ 'planning.title'|trans }}{% endblock %} {% block content %} -

    {{ 'reports.title'|trans }}

    +

    {{ 'planning.title'|trans }}

    -
    -
    - {{ 'reports.description'|trans }} -
    - - + {{ form_start(form) }} + {{ form_row(form.dataProvider) }} +
    +
    + {{ form_end(form) }} + + {% if error is not null %} + {% include 'components/alert.html.twig' with {level: "danger", text: error} %} + {% endif %} + {% if planningData is not null %} +
    + + {% set weeks = planningData.weeks %} + {% set assignees = planningData.assignees %} + {% set projects = planningData.projects %} + + {% if mode is defined %} + {% include 'reports/' ~ mode ~ '.html.twig' %} + {% endif %} +
    + {% endif %} {% endblock %} From 5c84124d6e3a5c19db062678c49fca6c537827e5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 27 May 2024 09:03:47 +0200 Subject: [PATCH 016/134] new hour report type --- src/Form/HourReportType.php | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index e69de29b..662047b3 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -0,0 +1,55 @@ +add('dataProvider') + ->add('projectId', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'required' => true, + 'label' => 'sprint_report.select_project', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'project', + 'data-action' => 'sprint-report#submitFormProjectId', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ]) + ->add('versionId', ChoiceType::class, [ + 'placeholder' => 'sprint_report.select_an_option', + 'required' => false, + 'label' => 'sprint_report.select_version', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'version', + 'data-action' => 'sprint-report#submitForm', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => HourReportFormData::class, + 'attr' => [ + 'data-sprint-report-target' => 'form', + ], + ]); + } +} From 1f673417c9b4197628d30cba6c444c099cf51ef6 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 27 May 2024 09:08:36 +0200 Subject: [PATCH 017/134] added translations --- translations/messages.da.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 39beac91..44cf589f 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -57,6 +57,7 @@ navigation: team_report: "Hent teamrapport" planning_users: 'Brugere' planning_projects: 'Projekter' + hour_report: "Hent timerapport" planning: title: "Planlægning" @@ -542,11 +543,17 @@ reports: team-result: 'Viser: %items_per_page% af %items_total% elementer' team_project_name: 'Projektnavn' team_user: 'Bruger / medarbejder' - team_issue_id: 'Issue id' + team_issue_id: 'HourReportIssue id' team_date: 'Registreringsdato' team_time_spent: 'Timeforbrug' team_description: 'Beskrivelse' team_no_records_found: 'Ingen resultater' + hour: + title: 'Timerapport' + select_option: 'Vælg en mulighed' + select_data_provider: 'Vælg datakilde' + select_project: 'Vælg projekt' + select_milestone: 'Vælg milestone' view: list_title: 'Views' From 5c223119dad2c2cf53761697b7b600d0d344f417 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 27 May 2024 13:56:30 +0200 Subject: [PATCH 018/134] removed old project report template --- templates/reports/project.html.twig | 112 ---------------------------- 1 file changed, 112 deletions(-) delete mode 100644 templates/reports/project.html.twig diff --git a/templates/reports/project.html.twig b/templates/reports/project.html.twig deleted file mode 100644 index af1e0b58..00000000 --- a/templates/reports/project.html.twig +++ /dev/null @@ -1,112 +0,0 @@ -
    - - Hello World! :D - {# - - - {% for week in weeks %} - - {% endfor %} - - - {% for project in projects %} - - - - {% for week in weeks %} - {% set res = null %} - {% for week_number in week.weekCollection %} - {% if project.sprintSums.containsKey(week_number) %} - {% set res = res + project.sprintSums.get(week_number).sumHours %} - {% endif %} - {% endfor %} - - {% endfor %} - - {% for assignee in project.assignees %} - - - {% for week in weeks %} - - {% endfor %} - - {% for issue in assignee.issues %} - - - {% for week in weeks %} - - {% endfor %} - - {% endfor %} - {% endfor %} - - {% endfor %} #} -
    {{ 'planning.projects'|trans }} - {{ week.displayName }} -
    -
    - - {{ project.displayName }} - - - -
    -
    - {{ res }} -
    -
    - - {{ assignee.displayName }} - - -
    -
    - {% set res = null %} - {% for week_number in week.weekCollection %} - {% if assignee.sprintSums.containsKey(week_number) %} - {% set res = res + assignee.sprintSums.get(week_number).sumHours %} - {% endif %} - {% endfor %} - {{ res }} -
    -
    - - {{ issue.displayName }} - - #{{ issue.key }} -
    -
    - {% for week_number in week.weekCollection %} - {% if issue.sprintId == week_number %} - {{ issue.remainingHours ?? 'UE' }} - {% endif %} - {% endfor %} -
    -
    From e812e375953ac42aa28cb550cf1713525bc28c6e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 28 May 2024 14:58:31 +0200 Subject: [PATCH 019/134] adding default values and preselects to faster show data to user --- src/Controller/HourReportController.php | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index df18542d..207b06c7 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -9,6 +9,7 @@ use App\Model\Reports\HourReportFormData; use App\Repository\DataProviderRepository; use App\Service\DataProviderService; +use App\Service\HourReportService; use App\Service\ViewService; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -24,6 +25,7 @@ public function __construct( private readonly DataProviderService $dataProviderService, private readonly DataProviderRepository $dataProviderRepository, private readonly ViewService $viewService, + private readonly HourReportService $hourReportService, private readonly ?string $defaultDataProvider, ) { } @@ -36,8 +38,10 @@ public function __construct( public function index(Request $request): Response { $reportData = null; + $mode = 'reports'; $reportFormData = new HourReportFormData(); + $form = $this->createForm(HourReportType::class, $reportFormData, [ 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), 'method' => 'GET', @@ -56,6 +60,7 @@ public function index(Request $request): Response 'attr' => [ 'onchange' => 'this.form.submit()', 'class' => 'form-element', + 'data-preselect' => $this->defaultDataProvider ?? '', ], 'help' => 'sprint_report.data_provider_helptext', 'choices' => $this->dataProviderRepository->findAll(), @@ -88,14 +93,18 @@ public function index(Request $request): Response if (isset($requestData['sprint_report'])) { $requestData = $requestData['sprint_report']; } - if (!empty($requestData['dataProvider'])) { - $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); + + if (!empty($requestData['dataProvider']) || $this->defaultDataProvider) { + if (!empty($requestData['dataProvider'])) { + $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); + } else { + $dataProvider = $this->dataProviderRepository->find($this->defaultDataProvider); + } if (null != $dataProvider) { $service = $this->dataProviderService->getService($dataProvider); $projectCollection = $service->getSprintReportProjects(); - $projectChoices = []; foreach ($projectCollection->projects as $project) { @@ -120,7 +129,10 @@ public function index(Request $request): Response ]); } } - if (!empty($requestData['dataProvider']) && !empty($requestData['projectId'])) { + if ((!empty($requestData['dataProvider']) || $this->defaultDataProvider) && !empty($requestData['projectId'])) { + if (empty($dataProvider)) { + throw new EconomicsException('reports.hour.select_data_provider_empty'); + } $service = $this->dataProviderService->getService($dataProvider); $projectId = $requestData['projectId']; @@ -153,8 +165,8 @@ public function index(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $projectId = $form->get('projectId')->getData(); - $milestoneId = $form->has('versionId') ? $form->get('versionId')->getData() : null; - $dataProvider = $form->get('dataProvider')->getData(); + $milestoneId = $form->get('versionId')->getData() ?? 0; + $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider; if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { $service = $this->dataProviderService->getService($dataProvider); @@ -164,6 +176,8 @@ public function index(Request $request): Response if (!empty($projectId) && !empty($dataProvider) && 0 === $milestoneId) { $service = $this->dataProviderService->getService($dataProvider); + $repData = $this->hourReportService->getHourReport($projectId, $milestoneId); + die('
    ' . print_r($repData, true) . '
    '); $reportData = $service?->getHourReportData($projectId); $mode = 'hourReport'; } From 477d6e9aa5d804be897a4b53e45842b6bc80db70 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 28 May 2024 14:59:01 +0200 Subject: [PATCH 020/134] added some data to Issue. More might be needed --- src/Entity/Issue.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 581f7b5c..1f0013f5 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -55,6 +55,12 @@ class Issue extends AbstractBaseEntity #[ORM\OrderBy(['createdAt' => Criteria::ASC])] private Collection $products; + #[ORM\Column(length: 255, nullable: true)] + public string $tags; + + #[ORM\Column(length: 255, nullable: true)] + public float $planHours; + public function __construct() { $this->versions = new ArrayCollection(); From 8298f60813cc2b9fdbe28de05d39e032a06e2001 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 28 May 2024 15:03:17 +0200 Subject: [PATCH 021/134] added hour report service --- src/Service/HourReportService.php | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/Service/HourReportService.php diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php new file mode 100644 index 00000000..3cbf0a96 --- /dev/null +++ b/src/Service/HourReportService.php @@ -0,0 +1,80 @@ +issueRepository->findBy(['project_tracker_id' => $projectId]); + + foreach ($projectIssues as $issue) { + $totalTicketEstimated = (float) $issue->planHours; + $timesheetData = $this->worklogRepository->findBy(['issue_id' => $issue->getId()]); + + list($timesheets, $totalTicketSpent) = $this->processTimesheetsData($timesheetData); + + $projectTicket = new HourReportProjectTicket( + $issue->getId(), + $issue->getName(), + $totalTicketEstimated, + $totalTicketSpent + ); + + $projectTicket->timesheets->add($timesheets); + + if ($hourReportData->projectTags->containsKey($issue->tags)) { + $projectTag = $hourReportData->projectTags->get($issue->tags); + $projectTag->totalEstimated += $totalTicketEstimated; + $projectTag->totalSpent += $totalTicketSpent; + } else { + $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issue->tags); + } + $projectTag->projectTickets->add($projectTicket); + $hourReportData->projectTags->set($issue->tags, $projectTag); + $hourReportData->projectTotalEstimated += $totalTicketEstimated; + $hourReportData->projectTotalSpent += $totalTicketSpent; + } + + return $hourReportData; + } + + private function processTimesheetsData($timesheetsData): array + { + $timesheets = []; + $totalTicketSpent = 0; + + foreach ($timesheetsData as $timesheetDatum) { + $timesheet = new HourReportTimesheet($timesheetDatum->id, $timesheetDatum->hours); + $timesheets[] = $timesheet; + $totalTicketSpent += (float) $timesheetDatum->hours; + } + + return [$timesheets, $totalTicketSpent]; + } +} From 6f74842424029c0c06c89c085d09dd24e9776ccd Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 28 May 2024 15:02:34 +0200 Subject: [PATCH 022/134] 1547: Added invoice entry account selector --- .env | 11 +++++++++++ CHANGELOG.md | 2 ++ config/services.yaml | 5 +++++ src/Controller/InvoiceEntryController.php | 6 +++++- src/Form/InvoiceEntryWorklogType.php | 22 +++++++++++++++++++++- translations/messages.da.yaml | 1 + 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 3a5e6c54..b4ce0c52 100644 --- a/.env +++ b/.env @@ -58,3 +58,14 @@ CLIENT_STANDARD_PRICE=705.00 DEFAULT_URI=https://economics.local.itkdev.dk/ PRODUCT_QUANTITY_SCALE=2 + +# If not empty, the receiver account can be edited on an invoice entry. +# Must be a valid JSON object mapping labels to values (account IDs), e.g. +# +# INVOICE_ENTRY_ACCOUNTS='{ +# "test": "My-PSP-element", +# "prod": "The real PSP element", +# }' +# +# One of the values MUST match the value of APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT. +INVOICE_ENTRY_ACCOUNTS='null' diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4e5608..29a2eece 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-122](https://github.com/itk-dev/economics/pull/122) + 1547: Added invoice entry account selector * [PR-121](https://github.com/itk-dev/economics/pull/121) 1485: Fixed floating number issues * [PR-120](https://github.com/itk-dev/economics/pull/120) diff --git a/config/services.yaml b/config/services.yaml index 71a2e3a4..a60a77d0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -54,3 +54,8 @@ services: $options: issue_product_type_options: quantity_scale: '%env(int:PRODUCT_QUANTITY_SCALE)%' + + App\Controller\InvoiceEntryController: + arguments: + $options: + invoice_entry_accounts: '%env(json:INVOICE_ENTRY_ACCOUNTS)%' diff --git a/src/Controller/InvoiceEntryController.php b/src/Controller/InvoiceEntryController.php index eb6d1f80..9404c3b3 100644 --- a/src/Controller/InvoiceEntryController.php +++ b/src/Controller/InvoiceEntryController.php @@ -26,7 +26,8 @@ public function __construct( private readonly BillingService $billingService, private readonly TranslatorInterface $translator, private readonly ViewService $viewService, - private readonly ClientHelper $clientHelper + private readonly ClientHelper $clientHelper, + private readonly array $options, ) { } @@ -92,6 +93,9 @@ public function edit(Request $request, Invoice $invoice, InvoiceEntry $invoiceEn } if (InvoiceEntryTypeEnum::WORKLOG == $invoiceEntry->getEntryType()) { + if (!empty($this->options['invoice_entry_accounts'])) { + $options['invoice_entry_accounts'] = $this->options['invoice_entry_accounts']; + } $form = $this->createForm(InvoiceEntryWorklogType::class, $invoiceEntry, $options); } else { $form = $this->createForm(InvoiceEntryType::class, $invoiceEntry); diff --git a/src/Form/InvoiceEntryWorklogType.php b/src/Form/InvoiceEntryWorklogType.php index 3642c74d..7a78fe2b 100644 --- a/src/Form/InvoiceEntryWorklogType.php +++ b/src/Form/InvoiceEntryWorklogType.php @@ -4,6 +4,7 @@ use App\Entity\InvoiceEntry; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -12,6 +13,20 @@ class InvoiceEntryWorklogType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { + $accounts = $options['invoice_entry_accounts']; + if (!empty($accounts)) { + $builder + ->add('account', ChoiceType::class, [ + 'required' => true, + 'attr' => ['class' => 'form-element'], + 'label' => 'invoices.invoice_entry_receiver_acccount', + 'label_attr' => ['class' => 'label'], + 'row_attr' => ['class' => 'form-row'], + 'help' => 'invoices.invoice_entry_receiver_acccount_helptext', + 'choices' => $accounts, + ]); + } + $builder ->add('product', null, [ 'required' => true, @@ -36,6 +51,11 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => InvoiceEntry::class, - ]); + 'invoice_entry_accounts' => [], + ]) + ->setAllowedTypes('invoice_entry_accounts', ['array']) + ->setAllowedValues('invoice_entry_accounts', static fn ($value) => is_array($value) + && (empty($value) || !array_is_list($value))) + ; } } diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 39beac91..3478871f 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -196,6 +196,7 @@ invoices: recorded: "Bogført" created_by: "Opretter" invoice_entry_receiver_acccount: "Til konto" + invoice_entry_receiver_acccount_helptext: "Vælg konto der skal faktureres" invoice_entry_material_number: "Materialenummer" invoice_entry_product: "Vare" invoice_entry_amount: "Antal" From 647e903717571706afe20bd3f430c50c8e1af6d1 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 28 May 2024 15:06:57 +0200 Subject: [PATCH 023/134] 1547: Fixed typo --- src/Form/InvoiceEntryWorklogType.php | 4 ++-- templates/invoices/edit.html.twig | 2 +- translations/messages.da.yaml | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Form/InvoiceEntryWorklogType.php b/src/Form/InvoiceEntryWorklogType.php index 7a78fe2b..4c51cf45 100644 --- a/src/Form/InvoiceEntryWorklogType.php +++ b/src/Form/InvoiceEntryWorklogType.php @@ -19,10 +19,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('account', ChoiceType::class, [ 'required' => true, 'attr' => ['class' => 'form-element'], - 'label' => 'invoices.invoice_entry_receiver_acccount', + 'label' => 'invoices.invoice_entry_receiver_account', 'label_attr' => ['class' => 'label'], 'row_attr' => ['class' => 'form-row'], - 'help' => 'invoices.invoice_entry_receiver_acccount_helptext', + 'help' => 'invoices.invoice_entry_receiver_account_helptext', 'choices' => $accounts, ]); } diff --git a/templates/invoices/edit.html.twig b/templates/invoices/edit.html.twig index 6f3ee103..598c9978 100644 --- a/templates/invoices/edit.html.twig +++ b/templates/invoices/edit.html.twig @@ -95,7 +95,7 @@ - + diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 3478871f..250612cb 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -195,8 +195,8 @@ invoices: recorded_false: "Ikke bogført" recorded: "Bogført" created_by: "Opretter" - invoice_entry_receiver_acccount: "Til konto" - invoice_entry_receiver_acccount_helptext: "Vælg konto der skal faktureres" + invoice_entry_receiver_account: "Til konto" + invoice_entry_receiver_account_helptext: "Vælg konto der skal faktureres" invoice_entry_material_number: "Materialenummer" invoice_entry_product: "Vare" invoice_entry_amount: "Antal" @@ -238,7 +238,6 @@ invoices: action_delete_invoice_entry: "Slet fakturalinje" action_save_invoice_entry: "Gem fakturalinje" invoice_entry_material_number_helptext: "Denne værdi kan kun ændres på fakturaniveau." - invoice_entry_receiver_account_helptext: "Denne værdi kan kun ændres på fakturaniveau." invoice_entry_product_helptext: "Beskriv fakturalinjen." invoice_entry_price_helptext: "Vælg prisen pr. stk." invoice_entry_amount_helptext: "Vælg antal." From ece18a824230daea0b6afe2d99979b08c62476ce Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 28 May 2024 15:37:59 +0200 Subject: [PATCH 024/134] 1547: Refactored and added invoice entry helper --- config/packages/twig.yaml | 1 + config/services.yaml | 4 +-- src/Controller/InvoiceEntryController.php | 9 +++-- src/Service/InvoiceEntryHelper.php | 42 +++++++++++++++++++++++ templates/invoice_entry/edit.html.twig | 2 +- templates/invoices/edit.html.twig | 2 +- 6 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 src/Service/InvoiceEntryHelper.php diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 4e0c8229..3c9c24eb 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,6 +4,7 @@ twig: format: 'd.m.Y' globals: view_controller: '@App\Controller\ViewController' + invoice_entry_helper: '@App\Service\InvoiceEntryHelper' when@test: twig: diff --git a/config/services.yaml b/config/services.yaml index a60a77d0..aa0ea3bf 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -55,7 +55,7 @@ services: issue_product_type_options: quantity_scale: '%env(int:PRODUCT_QUANTITY_SCALE)%' - App\Controller\InvoiceEntryController: + App\Service\InvoiceEntryHelper: arguments: $options: - invoice_entry_accounts: '%env(json:INVOICE_ENTRY_ACCOUNTS)%' + accounts: '%env(json:INVOICE_ENTRY_ACCOUNTS)%' diff --git a/src/Controller/InvoiceEntryController.php b/src/Controller/InvoiceEntryController.php index 9404c3b3..aeeba272 100644 --- a/src/Controller/InvoiceEntryController.php +++ b/src/Controller/InvoiceEntryController.php @@ -11,6 +11,7 @@ use App\Repository\InvoiceEntryRepository; use App\Service\BillingService; use App\Service\ClientHelper; +use App\Service\InvoiceEntryHelper; use App\Service\ViewService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -27,7 +28,7 @@ public function __construct( private readonly TranslatorInterface $translator, private readonly ViewService $viewService, private readonly ClientHelper $clientHelper, - private readonly array $options, + private readonly InvoiceEntryHelper $invoiceEntryHelper, ) { } @@ -93,8 +94,10 @@ public function edit(Request $request, Invoice $invoice, InvoiceEntry $invoiceEn } if (InvoiceEntryTypeEnum::WORKLOG == $invoiceEntry->getEntryType()) { - if (!empty($this->options['invoice_entry_accounts'])) { - $options['invoice_entry_accounts'] = $this->options['invoice_entry_accounts']; + // @todo Should accounts also be handled for non-worklog entries? + $accounts = $this->invoiceEntryHelper->getAccounts(); + if (!empty($accounts)) { + $options['invoice_entry_accounts'] = $accounts; } $form = $this->createForm(InvoiceEntryWorklogType::class, $invoiceEntry, $options); } else { diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceEntryHelper.php new file mode 100644 index 00000000..d37316f8 --- /dev/null +++ b/src/Service/InvoiceEntryHelper.php @@ -0,0 +1,42 @@ + + */ + public function getAccounts(): array + { + return $this->options['accounts'] ?? []; + } + + /** + * Decide if an invoice entry is editable. + */ + public function isEditable(InvoiceEntry $entry): bool + { + return null === $entry->getInvoice()?->getProjectBilling() + || !empty($this->getAccounts()); + } + + /** + * Get account display name based on configured accounts. + */ + public function getAccountDisplayName(string $account): string + { + $labels = array_flip($this->getAccounts()); + + return $labels[$account] ?? $account; + } +} diff --git a/templates/invoice_entry/edit.html.twig b/templates/invoice_entry/edit.html.twig index 0c9fd634..c732c023 100644 --- a/templates/invoice_entry/edit.html.twig +++ b/templates/invoice_entry/edit.html.twig @@ -9,7 +9,7 @@
    {{ form_widget(form) }} - {% if not invoice.recorded and invoice.projectBilling is null %} + {% if not invoice.recorded and invoice_entry_helper.isEditable(invoice_entry) %}
    diff --git a/templates/invoices/edit.html.twig b/templates/invoices/edit.html.twig index 598c9978..2fff1d2c 100644 --- a/templates/invoices/edit.html.twig +++ b/templates/invoices/edit.html.twig @@ -113,7 +113,7 @@ } %} {% for index, invoice_entry in invoice.invoiceEntries %}
    - + From 3e6a06ebdcc5deca8132a05e5e081ac77f91d55c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 08:21:26 +0200 Subject: [PATCH 025/134] Changed APP_DEFAULT_PLANNING_DATA_PROVIDER to APP_DEFAULT_DATA_PROVIDER to serve a broader purpose --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 8446720f..22d8ae0c 100644 --- a/.env +++ b/.env @@ -27,7 +27,7 @@ APP_INVOICE_SUPPLIER_ACCOUNT=APP_INVOICE_SUPPLIER_ACCOUNT APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT=APP_INVOICE_DEFAULT_RECEIVER_ACCOUNT APP_INVOICE_DESCRIPTION_TEMPLATE="Spørgsmål vedrørende fakturaen rettes til %name%, %email%." APP_PROJECT_BILLING_DEFAULT_DESCRIPTION= -APP_DEFAULT_PLANNING_DATA_PROVIDER= +APP_DEFAULT_DATA_PROVIDER= ###< Planning ### ###> itk-dev/openid-connect-bundle ### From c298e5a7023c23d2fffdad31648beac8241d27fb Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 08:22:08 +0200 Subject: [PATCH 026/134] Added translations for hour report twig template --- templates/reports/hourReport.html.twig | 6 +++--- translations/messages.da.yaml | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/templates/reports/hourReport.html.twig b/templates/reports/hourReport.html.twig index fc68cf0b..a51b0afe 100644 --- a/templates/reports/hourReport.html.twig +++ b/templates/reports/hourReport.html.twig @@ -6,10 +6,10 @@ @@ -64,7 +64,7 @@ {% endfor %} - + diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 44cf589f..764702f3 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -548,12 +548,15 @@ reports: team_time_spent: 'Timeforbrug' team_description: 'Beskrivelse' team_no_records_found: 'Ingen resultater' - hour: + hour_report: title: 'Timerapport' select_option: 'Vælg en mulighed' select_data_provider: 'Vælg datakilde' select_project: 'Vælg projekt' select_milestone: 'Vælg milestone' + estimated_hours: 'Estimeret' + logged_hours: 'Logget' + total: 'Total' view: list_title: 'Views' From 190b3d2dedf109cc20ebf130a63e49f7e01de252 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 08:34:59 +0200 Subject: [PATCH 027/134] Undid unwanted change from earlier stage of development --- src/Controller/ReportsController.php | 109 ++------------------------- 1 file changed, 7 insertions(+), 102 deletions(-) diff --git a/src/Controller/ReportsController.php b/src/Controller/ReportsController.php index c481897c..7dbf9e9f 100644 --- a/src/Controller/ReportsController.php +++ b/src/Controller/ReportsController.php @@ -1,121 +1,26 @@ render( -// 'reports/index.html.twig', -// $this->viewService->addView([]) -// ); -// } -// } - - namespace App\Controller; -use App\Entity\DataProvider; -use App\Exception\EconomicsException; -use App\Exception\UnsupportedDataProviderException; -use App\Form\PlanningType; -use App\Model\Planning\PlanningFormData; -use App\Repository\DataProviderRepository; -use App\Service\DataProviderService; use App\Service\ViewService; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Annotation\Route; #[Route('/admin/reports')] class ReportsController extends AbstractController { public function __construct( - private readonly DataProviderService $dataProviderService, - private readonly DataProviderRepository $dataProviderRepository, private readonly ViewService $viewService, - private readonly ?string $planningDefaultDataProvider, ) { } - #[Route('/', name: 'app_reports')] - public function index(Request $request): Response + #[Route('', name: 'app_reports_index')] + public function display(): Response { - return $this->createResponse($request, 'project'); - } - - /** - * @throws UnsupportedDataProviderException - * @throws EconomicsException - */ - private function createResponse(Request $request, string $mode): Response - { - - // $planningFormData = new PlanningFormData(); - // $form = $this->createForm(PlanningType::class, $planningFormData); - - // $dataProviders = $this->dataProviderRepository->findAll(); - // $defaultProvider = $this->dataProviderRepository->find($this->planningDefaultDataProvider); - - // if (null === $defaultProvider && count($dataProviders) > 0) { - // $defaultProvider = $dataProviders[0]; - // } - - // $form->add('dataProvider', EntityType::class, [ - // 'class' => DataProvider::class, - // 'required' => true, - // 'label' => 'planning.data_provider', - // 'label_attr' => ['class' => 'label'], - // 'attr' => [ - // 'class' => 'form-element', - // ], - // 'help' => 'planning.data_provider_helptext', - // 'data' => $this->dataProviderRepository->find($this->planningDefaultDataProvider), - // 'choices' => $dataProviders, - // ]); - - // $form->handleRequest($request); - - // $service = null; - - // if ($form->isSubmitted() && $form->isValid()) { - // $service = $this->dataProviderService->getService($planningFormData->dataProvider); - // } elseif (null !== $defaultProvider) { - // $service = $this->dataProviderService->getService($defaultProvider); - // } - - // try { - // $planningData = $service?->getPlanningDataWeeks(); - // } catch (\Exception $e) { - // $error = $e->getMessage(); - // $planningData = null; - // } - - // return $this->render('planning/planning.html.twig', $this->viewService->addView([ - // 'controller_name' => 'ReportController', - // 'planningData' => $planningData, - // 'error' => $error ?? null, - // 'form' => $form, - // 'mode' => $mode, - // ])); - return $this->render('reports/project.html.twig', $this->viewService->addView([ - 'controller_name' => 'ReportController', - 'mode' => $mode, - ])); + return $this->render( + 'reports/index.html.twig', + $this->viewService->addView([]) + ); } } From d4a651152128cc2f5fcbf26e7656bb102d405bef Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 08:39:17 +0200 Subject: [PATCH 028/134] Corrected updated reference to translation in reports twig --- templates/reports/reports.html.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index cc40ba89..faa9b97c 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -1,9 +1,9 @@ {% extends 'base.html.twig' %} -{% block title %}{{ 'reports.hour.title'|trans }}{% endblock %} +{% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} {% block content %} -

    {{ 'reports.hour.title'|trans }}

    +

    {{ 'reports.hour_report.title'|trans }}

    {{ form_start(form) }} {{ form_row(form.dataProvider) }} From fad12fdbf413ffb470122b9bb2bfbc1c4a59bbf7 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 30 May 2024 12:29:12 +0200 Subject: [PATCH 029/134] 1547: Handled account on all invoice entries --- src/Controller/InvoiceEntryController.php | 13 ++++++------- src/Form/InvoiceEntryType.php | 22 +++++++++++++++++++++- src/Service/InvoiceEntryHelper.php | 18 ++++++++++++++---- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Controller/InvoiceEntryController.php b/src/Controller/InvoiceEntryController.php index aeeba272..f41394a0 100644 --- a/src/Controller/InvoiceEntryController.php +++ b/src/Controller/InvoiceEntryController.php @@ -93,15 +93,14 @@ public function edit(Request $request, Invoice $invoice, InvoiceEntry $invoiceEn $options['disabled'] = true; } - if (InvoiceEntryTypeEnum::WORKLOG == $invoiceEntry->getEntryType()) { - // @todo Should accounts also be handled for non-worklog entries? - $accounts = $this->invoiceEntryHelper->getAccounts(); - if (!empty($accounts)) { - $options['invoice_entry_accounts'] = $accounts; - } + $accounts = $this->invoiceEntryHelper->getAccounts($invoiceEntry->getAccount()); + if (!empty($accounts)) { + $options['invoice_entry_accounts'] = $accounts; + } + if (InvoiceEntryTypeEnum::WORKLOG === $invoiceEntry->getEntryType()) { $form = $this->createForm(InvoiceEntryWorklogType::class, $invoiceEntry, $options); } else { - $form = $this->createForm(InvoiceEntryType::class, $invoiceEntry); + $form = $this->createForm(InvoiceEntryType::class, $invoiceEntry, $options); } $form->handleRequest($request); diff --git a/src/Form/InvoiceEntryType.php b/src/Form/InvoiceEntryType.php index 5359f10d..9ba9f40c 100644 --- a/src/Form/InvoiceEntryType.php +++ b/src/Form/InvoiceEntryType.php @@ -4,6 +4,7 @@ use App\Entity\InvoiceEntry; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -12,6 +13,20 @@ class InvoiceEntryType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { + $accounts = $options['invoice_entry_accounts']; + if (!empty($accounts)) { + $builder + ->add('account', ChoiceType::class, [ + 'required' => true, + 'attr' => ['class' => 'form-element'], + 'label' => 'invoices.invoice_entry_receiver_account', + 'label_attr' => ['class' => 'label'], + 'row_attr' => ['class' => 'form-row'], + 'help' => 'invoices.invoice_entry_receiver_account_helptext', + 'choices' => $accounts, + ]); + } + $builder ->add('product', null, [ 'required' => true, @@ -46,6 +61,11 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => InvoiceEntry::class, - ]); + 'invoice_entry_accounts' => [], + ]) + ->setAllowedTypes('invoice_entry_accounts', ['array']) + ->setAllowedValues('invoice_entry_accounts', static fn ($value) => is_array($value) + && (empty($value) || !array_is_list($value))) + ; } } diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceEntryHelper.php index d37316f8..d8004585 100644 --- a/src/Service/InvoiceEntryHelper.php +++ b/src/Service/InvoiceEntryHelper.php @@ -14,11 +14,21 @@ public function __construct( /** * Get all configured accounts. * + * @param string|null $account + * An account that must exist in the result + * * @return array */ - public function getAccounts(): array + public function getAccounts(?string $account): array { - return $this->options['accounts'] ?? []; + $accounts = $this->options['accounts'] ?? []; + + // Make sure that the default account exists. + if (isset($account) && !in_array($account, $accounts, true)) { + $accounts[$account] = $account; + } + + return $accounts; } /** @@ -27,7 +37,7 @@ public function getAccounts(): array public function isEditable(InvoiceEntry $entry): bool { return null === $entry->getInvoice()?->getProjectBilling() - || !empty($this->getAccounts()); + || !empty($this->getAccounts(null)); } /** @@ -35,7 +45,7 @@ public function isEditable(InvoiceEntry $entry): bool */ public function getAccountDisplayName(string $account): string { - $labels = array_flip($this->getAccounts()); + $labels = array_flip($this->getAccounts($account)); return $labels[$account] ?? $account; } From 3aebfa319051c1f08ce4da72dc6f3fe50216561b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 12:50:55 +0200 Subject: [PATCH 030/134] Added planHours and hoursRemaining to Issue entity --- migrations/Version20240530104522.php | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 migrations/Version20240530104522.php diff --git a/migrations/Version20240530104522.php b/migrations/Version20240530104522.php new file mode 100644 index 00000000..7c57ba1a --- /dev/null +++ b/migrations/Version20240530104522.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE issue ADD plan_hours DOUBLE PRECISION DEFAULT NULL, ADD hours_remaining DOUBLE PRECISION DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE issue DROP plan_hours, DROP hours_remaining'); + } +} From e8136427a026180350af3331539a231d5265efbb Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 12:56:36 +0200 Subject: [PATCH 031/134] added planHours and hoursRemaining to issue entity --- src/Entity/Issue.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 1f0013f5..7f47a5d7 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -56,10 +56,10 @@ class Issue extends AbstractBaseEntity private Collection $products; #[ORM\Column(length: 255, nullable: true)] - public string $tags; + public ?float $planHours; #[ORM\Column(length: 255, nullable: true)] - public float $planHours; + public ?float $hoursRemaining; public function __construct() { @@ -269,6 +269,26 @@ public function removeProduct(IssueProduct $issueProduct): static } } + return $this; + } + public function getPlanHours(): ?float + { + return $this->planHours; + } + public function setPlanHours(?float $planHours): self + { + $this->planHours = $planHours; + + return $this; + } + public function getHoursRemaining(): ?float + { + return $this->planHours; + } + public function setHoursRemaining(?float $hoursRemaining): self + { + $this->hoursRemaining = $hoursRemaining; + return $this; } } From c4e8cb938ba68deffc127487fd7a15ecb7f5fe96 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 12:56:59 +0200 Subject: [PATCH 032/134] added planHours and hoursRemaining data fixtures --- src/DataFixtures/AppFixtures.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index ba6c898d..db34d0e9 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -102,7 +102,8 @@ public function load(ObjectManager $manager): void $issue->setDataProvider($dataProvider); $issue->addVersion($versions[$j % count($versions)]); $issue->setResolutionDate(new \DateTime()); - + $issue->setPlanHours($j); + $issue->setHoursRemaining($j); $manager->persist($issue); for ($k = 0; $k < 100; ++$k) { From 822e1072b15c1da761eea8c14fadc127c2bf39a7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 13:59:12 +0200 Subject: [PATCH 033/134] modifying hourreport controller to work with economics database --- src/Controller/HourReportController.php | 29 ++++++------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index 207b06c7..f5dff8f0 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -102,14 +102,7 @@ public function index(Request $request): Response } if (null != $dataProvider) { - $service = $this->dataProviderService->getService($dataProvider); - - $projectCollection = $service->getSprintReportProjects(); - $projectChoices = []; - - foreach ($projectCollection->projects as $project) { - $projectChoices[$project->name] = $project->id; - } + $projectChoices = $this->hourReportService->getProjects(); // Override projectId with element with choices. $form->add('projectId', ChoiceType::class, [ @@ -136,12 +129,7 @@ public function index(Request $request): Response $service = $this->dataProviderService->getService($dataProvider); $projectId = $requestData['projectId']; - $milestoneCollection = $service->getSprintReportVersions($projectId); - $milestoneChoices = []; - $milestoneChoices['All milestones'] = 0; - foreach ($milestoneCollection->versions as $milestone) { - $milestoneChoices[$milestone->name] = $milestone->id; - } + $milestoneChoices = $this->hourReportService->getMilestones($projectId, true); // Override projectId with element with choices. $form->add('versionId', ChoiceType::class, [ @@ -165,20 +153,17 @@ public function index(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $projectId = $form->get('projectId')->getData(); - $milestoneId = $form->get('versionId')->getData() ?? 0; + $milestoneId = $form->get('versionId')->getData() ?? '0'; $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider; + $reportData = $this->hourReportService->getHourReport($projectId, $milestoneId); if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { - $service = $this->dataProviderService->getService($dataProvider); - $reportData = $service?->getHourReportData($projectId, $milestoneId); + $reportData = $this->hourReportService->getHourReport($projectId, $milestoneId); $mode = 'hourReport'; } - if (!empty($projectId) && !empty($dataProvider) && 0 === $milestoneId) { - $service = $this->dataProviderService->getService($dataProvider); - $repData = $this->hourReportService->getHourReport($projectId, $milestoneId); - die('
    ' . print_r($repData, true) . '
    '); - $reportData = $service?->getHourReportData($projectId); + if (!empty($projectId) && !empty($selectedDataProvider) && '0' === $milestoneId) { + $reportData = $this->hourReportService->getHourReport($projectId); $mode = 'hourReport'; } } From c52edb7cea3a6601fa211d95cb18bc821a19967d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 13:59:37 +0200 Subject: [PATCH 034/134] crafted hourreport service to gather data from economics database --- src/Service/HourReportService.php | 76 +++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 3cbf0a96..f642d8fd 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -8,7 +8,9 @@ use App\Model\Reports\HourReportProjectTicket; use App\Model\Reports\HourReportTimesheet; use App\Repository\IssueRepository; +use App\Repository\ProjectRepository; use App\Repository\ProjectVersionBudgetRepository; +use App\Repository\VersionRepository; use App\Repository\WorklogRepository; use Doctrine\ORM\EntityManagerInterface; @@ -19,24 +21,64 @@ public function __construct( private readonly EntityManagerInterface $entityManager, private readonly IssueRepository $issueRepository, private readonly WorklogRepository $worklogRepository, + private readonly ProjectRepository $projectRepository, + private readonly VersionRepository $versionRepository, ) { } /** * @throws EconomicsException */ - public function getHourReport($projectId, $versionId): HourReportData + public function getProjects(): array + { + $projects = $this->projectRepository->findAll(); + $projectChoices = []; + foreach ($projects as $project) { + $projectChoices[$project->getName()] = $project->getId(); + } + + return $projectChoices; + } + + /** + * @throws EconomicsException + */ + public function getMilestones(string $projectId, bool $allAllOption = false): array + { + $milestones = $this->versionRepository->findBy(['project' => $projectId]); + $milestoneChoices = []; + if ($allAllOption) { + $milestoneChoices['All milestones'] = '0'; + } + foreach ($milestones as $milestone) { + $milestoneChoices[$milestone->getName()] = $milestone->getId(); + } + + return $milestoneChoices; + } + + /** + * @throws EconomicsException + */ + public function getHourReport(string $projectId, int $versionId = null): HourReportData { if (!$projectId) { throw new EconomicsException('No project id specified'); } $hourReportData = new HourReportData(0, 0); - $projectIssues = $this->issueRepository->findBy(['project_tracker_id' => $projectId]); + $projectIssues = $this->issueRepository->findBy(['project' => $projectId]); foreach ($projectIssues as $issue) { - $totalTicketEstimated = (float) $issue->planHours; - $timesheetData = $this->worklogRepository->findBy(['issue_id' => $issue->getId()]); + // If version is provided, we only want the issues containing the versionId + if ($versionId) { + $issueHasVersion = $this->checkIssueHasVersionId($issue, $versionId); + if (!$issueHasVersion) { + continue; + } + } + $totalTicketEstimated = (float) $issue->planHours; + $timesheetData = $this->worklogRepository->findBy(['issue' => $issue->getId()]); list($timesheets, $totalTicketSpent) = $this->processTimesheetsData($timesheetData); $projectTicket = new HourReportProjectTicket( @@ -48,15 +90,15 @@ public function getHourReport($projectId, $versionId): HourReportData $projectTicket->timesheets->add($timesheets); - if ($hourReportData->projectTags->containsKey($issue->tags)) { - $projectTag = $hourReportData->projectTags->get($issue->tags); + if ($hourReportData->projectTags->containsKey($issue->getEpicName())) { + $projectTag = $hourReportData->projectTags->get($issue->getEpicName()); $projectTag->totalEstimated += $totalTicketEstimated; $projectTag->totalSpent += $totalTicketSpent; } else { - $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issue->tags); + $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issue->getEpicName()); } $projectTag->projectTickets->add($projectTicket); - $hourReportData->projectTags->set($issue->tags, $projectTag); + $hourReportData->projectTags->set($issue->getEpicName(), $projectTag); $hourReportData->projectTotalEstimated += $totalTicketEstimated; $hourReportData->projectTotalSpent += $totalTicketSpent; } @@ -64,15 +106,29 @@ public function getHourReport($projectId, $versionId): HourReportData return $hourReportData; } + private function checkIssueHasVersionId($issue, int $versionId): bool + { + $issueVersions = $issue->getVersions(); + $issueHasVersion = false; + foreach ($issueVersions as $issueVersion) { + if ($issueVersion->getId() === $versionId) { + $issueHasVersion = true; + } + } + + return $issueHasVersion; + } + private function processTimesheetsData($timesheetsData): array { $timesheets = []; $totalTicketSpent = 0; foreach ($timesheetsData as $timesheetDatum) { - $timesheet = new HourReportTimesheet($timesheetDatum->id, $timesheetDatum->hours); + $hoursSpent = (float) ($timesheetDatum->getTimeSpentSeconds() / 3600); + $timesheet = new HourReportTimesheet($timesheetDatum->getId(), $hoursSpent); $timesheets[] = $timesheet; - $totalTicketSpent += (float) $timesheetDatum->hours; + $totalTicketSpent += $hoursSpent; } return [$timesheets, $totalTicketSpent]; From 73e37743be37634ca27e2d2cb3c3f5ff9fa66b77 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 30 May 2024 14:09:05 +0200 Subject: [PATCH 035/134] 1544: Allowed invoicing issues with products and no worklogs --- CHANGELOG.md | 2 ++ migrations/Version20240530115938.php | 35 +++++++++++++++++++++++++++ src/Entity/InvoiceEntry.php | 34 ++++++++++++++++++++++++++ src/Entity/IssueProduct.php | 31 ++++++++++++++++++++++++ src/Service/BillingService.php | 4 +++ src/Service/ProjectBillingService.php | 35 +++++++++++++++++++-------- 6 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 migrations/Version20240530115938.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a2eece..e37b1f9f 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-123](https://github.com/itk-dev/economics/pull/123) + 1544: Allowed invoicing issues with products and no worklogs * [PR-122](https://github.com/itk-dev/economics/pull/122) 1547: Added invoice entry account selector * [PR-121](https://github.com/itk-dev/economics/pull/121) diff --git a/migrations/Version20240530115938.php b/migrations/Version20240530115938.php new file mode 100644 index 00000000..ee702439 --- /dev/null +++ b/migrations/Version20240530115938.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE issue_product ADD invoice_entry_id INT DEFAULT NULL, ADD is_billed TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE issue_product ADD CONSTRAINT FK_76B2414CA51E131A FOREIGN KEY (invoice_entry_id) REFERENCES invoice_entry (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_76B2414CA51E131A ON issue_product (invoice_entry_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE issue_product DROP FOREIGN KEY FK_76B2414CA51E131A'); + $this->addSql('DROP INDEX IDX_76B2414CA51E131A ON issue_product'); + $this->addSql('ALTER TABLE issue_product DROP invoice_entry_id, DROP is_billed'); + } +} diff --git a/src/Entity/InvoiceEntry.php b/src/Entity/InvoiceEntry.php index 7ce98664..643ae128 100644 --- a/src/Entity/InvoiceEntry.php +++ b/src/Entity/InvoiceEntry.php @@ -51,9 +51,13 @@ class InvoiceEntry extends AbstractBaseEntity #[ORM\OneToMany(mappedBy: 'invoiceEntry', targetEntity: Worklog::class)] private Collection $worklogs; + #[ORM\OneToMany(mappedBy: 'invoiceEntry', targetEntity: IssueProduct::class)] + private Collection $issueProducts; + public function __construct() { $this->worklogs = new ArrayCollection(); + $this->issueProducts = new ArrayCollection(); } public function getInvoice(): ?Invoice @@ -182,6 +186,36 @@ public function removeWorklog(Worklog $worklog): self return $this; } + /** + * @return Collection + */ + public function getIssueProducts(): Collection + { + return $this->issueProducts; + } + + public function addIssueProduct(IssueProduct $issueProduct): self + { + if (!$this->issueProducts->contains($issueProduct)) { + $this->issueProducts->add($issueProduct); + $issueProduct->setInvoiceEntry($this); + } + + return $this; + } + + public function removeIssueProduct(IssueProduct $issueProduct): self + { + if ($this->issueProducts->removeElement($issueProduct)) { + // set the owning side to null (unless already changed) + if ($issueProduct->getInvoiceEntry() === $this) { + $issueProduct->setInvoiceEntry(null); + } + } + + return $this; + } + public function getEntryType(): InvoiceEntryTypeEnum { return $this->entryType; diff --git a/src/Entity/IssueProduct.php b/src/Entity/IssueProduct.php index 49360b7c..39e7f51f 100644 --- a/src/Entity/IssueProduct.php +++ b/src/Entity/IssueProduct.php @@ -23,6 +23,13 @@ class IssueProduct extends AbstractBaseEntity #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $description = null; + #[ORM\ManyToOne(inversedBy: 'issueProducts')] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + private ?InvoiceEntry $invoiceEntry = null; + + #[ORM\Column(nullable: true)] + private ?bool $isBilled = null; + public function getIssue(): ?Issue { return $this->issue; @@ -75,4 +82,28 @@ public function setDescription(string $description): static return $this; } + + public function getInvoiceEntry(): ?InvoiceEntry + { + return $this->invoiceEntry; + } + + public function setInvoiceEntry(?InvoiceEntry $invoiceEntry): self + { + $this->invoiceEntry = $invoiceEntry; + + return $this; + } + + public function isBilled(): ?bool + { + return $this->isBilled; + } + + public function setIsBilled(bool $isBilled): self + { + $this->isBilled = $isBilled; + + return $this; + } } diff --git a/src/Service/BillingService.php b/src/Service/BillingService.php index c2daa943..e0e5ac3e 100644 --- a/src/Service/BillingService.php +++ b/src/Service/BillingService.php @@ -115,6 +115,10 @@ public function recordInvoice(Invoice $invoice, string $confirmation = ConfirmDa $worklog->setIsBilled(true); $worklog->setBilledSeconds($worklog->getTimeSpentSeconds()); } + } elseif (InvoiceEntryTypeEnum::PRODUCT === $invoiceEntry->getEntryType()) { + foreach ($invoiceEntry->getIssueProducts() as $issueProduct) { + $issueProduct->setIsBilled(true); + } } } diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index 7800b8a0..a35e0eda 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -63,10 +63,20 @@ public function getIssuesNotIncludedInProjectBilling(ProjectBilling $projectBill /** @var Issue $issue */ foreach ($issues as $issue) { - foreach ($issue->getWorklogs() as $worklog) { - if (!$worklog->isBilled() && $worklog->getTimeSpentSeconds() > 0 && null === $worklog->getInvoiceEntry()) { - $filteredIssues[] = $issue; - break; + $worklogs = $issue->getWorklogs(); + if ($worklogs->isEmpty()) { + foreach ($issue->getProducts() as $product) { + if (!$product->isBilled() && null === $product->getInvoiceEntry()) { + $filteredIssues[] = $issue; + break; + } + } + } else { + foreach ($worklogs as $worklog) { + if (!$worklog->isBilled() && $worklog->getTimeSpentSeconds() > 0 && null === $worklog->getInvoiceEntry()) { + $filteredIssues[] = $issue; + break; + } } } } @@ -127,7 +137,7 @@ public function createProjectBilling(int $projectBillingId): void // Create invoice // Create an InvoiceEntry in the invoice // Connect worklogs from the database to the invoice entry. - $issues = $this->issueRepository->getClosedIssuesFromInterval($project, $periodStart, $periodEnd); + $issues = $this->getIssuesNotIncludedInProjectBilling($projectBilling); // TODO: Replace with Model. $invoices = []; @@ -215,12 +225,16 @@ public function createProjectBilling(int $projectBillingId): void } } - if (0 == count($invoiceEntry->getWorklogs())) { + // We only want to invoice entries with worklogs or products. + if ($invoiceEntry->getWorklogs()->isEmpty() + && $issue->getProducts()->isEmpty()) { continue; } - $invoice->addInvoiceEntry($invoiceEntry); - $this->entityManager->persist($invoiceEntry); + if (!$invoiceEntry->getWorklogs()->isEmpty()) { + $invoice->addInvoiceEntry($invoiceEntry); + $this->entityManager->persist($invoiceEntry); + } // Add invoice entries for each product. foreach ($issue->getProducts() as $productIssue) { @@ -237,7 +251,8 @@ public function createProjectBilling(int $projectBillingId): void ->setAmount($productIssue->getQuantity()) ->setTotalPrice($productIssue->getQuantity() * $product->getPriceAsFloat()) ->setMaterialNumber($invoice->getDefaultMaterialNumber()) - ->setAccount($invoice->getDefaultReceiverAccount()); + ->setAccount($invoice->getDefaultReceiverAccount()) + ->addIssueProduct($productIssue); // We don't add worklogs here, since they're already attached to the main invoice entry // (and only used to detect if an entry has been added to an invoice). @@ -246,7 +261,7 @@ public function createProjectBilling(int $projectBillingId): void } } - if (0 == count($invoice->getInvoiceEntries())) { + if ($invoice->getInvoiceEntries()->isEmpty()) { continue; } From ab7ec44614fd203e8721d6991c035774e2c2a335 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 14:58:58 +0200 Subject: [PATCH 036/134] Added repository method for finding issue versionId match --- src/Repository/IssueRepository.php | 15 +++++++++++++++ src/Service/HourReportService.php | 15 +-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index ddb2d9c1..016fb074 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -110,4 +110,19 @@ private function getClosedStatuses(Project $project): array '0', ]; } + + public function issueContainsVersion(int $issueId, int $versionId): bool + { + $qb = $this->createQueryBuilder('issue'); + + $qb->select('version.id') + ->leftJoin('issue.versions', 'version') + ->where('issue.id = :issueId') + ->andWhere('version.id = :versionId') + ->setParameters(['issueId' => $issueId, 'versionId' => $versionId]); + + // If the query returns any result, that means a versionId match has been found. + return (bool) $qb->getQuery()->getOneOrNullResult(); + } + } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index f642d8fd..676a0db9 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -71,7 +71,7 @@ public function getHourReport(string $projectId, int $versionId = null): HourRep foreach ($projectIssues as $issue) { // If version is provided, we only want the issues containing the versionId if ($versionId) { - $issueHasVersion = $this->checkIssueHasVersionId($issue, $versionId); + $issueHasVersion = $this->issueRepository->issueContainsVersion($issue->getId(), $versionId); if (!$issueHasVersion) { continue; @@ -106,19 +106,6 @@ public function getHourReport(string $projectId, int $versionId = null): HourRep return $hourReportData; } - private function checkIssueHasVersionId($issue, int $versionId): bool - { - $issueVersions = $issue->getVersions(); - $issueHasVersion = false; - foreach ($issueVersions as $issueVersion) { - if ($issueVersion->getId() === $versionId) { - $issueHasVersion = true; - } - } - - return $issueHasVersion; - } - private function processTimesheetsData($timesheetsData): array { $timesheets = []; From 6cfa631aae0cf48c7b057260a76ce2032b6a6d51 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 30 May 2024 15:17:09 +0200 Subject: [PATCH 037/134] passing whole entity to repo method instead of single value. Coding standards --- src/Controller/HourReportController.php | 2 -- src/Entity/Issue.php | 4 ++++ src/Repository/IssueRepository.php | 5 ++--- src/Service/HourReportService.php | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index f5dff8f0..20a87a05 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -126,7 +126,6 @@ public function index(Request $request): Response if (empty($dataProvider)) { throw new EconomicsException('reports.hour.select_data_provider_empty'); } - $service = $this->dataProviderService->getService($dataProvider); $projectId = $requestData['projectId']; $milestoneChoices = $this->hourReportService->getMilestones($projectId, true); @@ -156,7 +155,6 @@ public function index(Request $request): Response $milestoneId = $form->get('versionId')->getData() ?? '0'; $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider; - $reportData = $this->hourReportService->getHourReport($projectId, $milestoneId); if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { $reportData = $this->hourReportService->getHourReport($projectId, $milestoneId); $mode = 'hourReport'; diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 7f47a5d7..b9f45126 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -271,20 +271,24 @@ public function removeProduct(IssueProduct $issueProduct): static return $this; } + public function getPlanHours(): ?float { return $this->planHours; } + public function setPlanHours(?float $planHours): self { $this->planHours = $planHours; return $this; } + public function getHoursRemaining(): ?float { return $this->planHours; } + public function setHoursRemaining(?float $hoursRemaining): self { $this->hoursRemaining = $hoursRemaining; diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 016fb074..6ef2406e 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -111,7 +111,7 @@ private function getClosedStatuses(Project $project): array ]; } - public function issueContainsVersion(int $issueId, int $versionId): bool + public function issueContainsVersion(Issue $issue, int $versionId): bool { $qb = $this->createQueryBuilder('issue'); @@ -119,10 +119,9 @@ public function issueContainsVersion(int $issueId, int $versionId): bool ->leftJoin('issue.versions', 'version') ->where('issue.id = :issueId') ->andWhere('version.id = :versionId') - ->setParameters(['issueId' => $issueId, 'versionId' => $versionId]); + ->setParameters(['issueId' => $issue->getId(), 'versionId' => $versionId]); // If the query returns any result, that means a versionId match has been found. return (bool) $qb->getQuery()->getOneOrNullResult(); } - } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 676a0db9..d3c4bf3b 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -71,7 +71,7 @@ public function getHourReport(string $projectId, int $versionId = null): HourRep foreach ($projectIssues as $issue) { // If version is provided, we only want the issues containing the versionId if ($versionId) { - $issueHasVersion = $this->issueRepository->issueContainsVersion($issue->getId(), $versionId); + $issueHasVersion = $this->issueRepository->issueContainsVersion($issue, $versionId); if (!$issueHasVersion) { continue; From 2502413cf29ec64a03964eae0e4140870dc7ab2b Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 30 May 2024 16:32:06 +0200 Subject: [PATCH 038/134] 1547: Set account based on invoice entry type --- .env | 35 ++++- CHANGELOG.md | 2 + config/services.yaml | 1 - src/Controller/InvoiceController.php | 5 +- src/Controller/InvoiceEntryController.php | 2 +- src/Service/InvoiceEntryHelper.php | 150 ++++++++++++++++++++-- src/Service/ProjectBillingService.php | 6 +- templates/invoices/edit.html.twig | 2 +- 8 files changed, 177 insertions(+), 26 deletions(-) diff --git a/.env b/.env index b4ce0c52..260c909a 100644 --- a/.env +++ b/.env @@ -24,7 +24,6 @@ API_SERVICE_SPRINT_NAME_REGEX="/(?(?:-?\d+-?)*)\.(?\d+)$/" APP_WEEK_GOAL_LOW=25.0 APP_WEEK_GOAL_HIGH=34.5 APP_INVOICE_SUPPLIER_ACCOUNT=APP_INVOICE_SUPPLIER_ACCOUNT -APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT=APP_INVOICE_DEFAULT_RECEIVER_ACCOUNT APP_INVOICE_DESCRIPTION_TEMPLATE="Spørgsmål vedrørende fakturaen rettes til %name%, %email%." APP_PROJECT_BILLING_DEFAULT_DESCRIPTION= APP_DEFAULT_PLANNING_DATA_PROVIDER= @@ -59,13 +58,35 @@ DEFAULT_URI=https://economics.local.itkdev.dk/ PRODUCT_QUANTITY_SCALE=2 -# If not empty, the receiver account can be edited on an invoice entry. -# Must be a valid JSON object mapping labels to values (account IDs), e.g. +# Invoice entry accounts. +# Must be a valid JSON object mapping account IDs to a label and additional metadata. +# +# Requirements; +# +# * At least one account must be defined. +# +# * One and only one account must be "default" (a single account will be +# * "default") +# +# * If more than one account is defined, one and only one account must be +# * "product". # # INVOICE_ENTRY_ACCOUNTS='{ -# "test": "My-PSP-element", -# "prod": "The real PSP element", +# "test": { +# "label": "Test account" +# }, +# "account-87": { +# "label": "The default account", +# "default": true +# }, +# "product": { +# "label": "The real PSP element", +# "product": true +# } # }' # -# One of the values MUST match the value of APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT. -INVOICE_ENTRY_ACCOUNTS='null' +INVOICE_ENTRY_ACCOUNTS='{ +"test": { + "label": "test" +} +}' diff --git a/CHANGELOG.md b/CHANGELOG.md index e37b1f9f..918d2b7b 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-125](https://github.com/itk-dev/economics/pull/125) + 1547: Set account based on invoice entry type * [PR-123](https://github.com/itk-dev/economics/pull/123) 1544: Allowed invoicing issues with products and no worklogs * [PR-122](https://github.com/itk-dev/economics/pull/122) diff --git a/config/services.yaml b/config/services.yaml index aa0ea3bf..6bd7f2eb 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -21,7 +21,6 @@ services: bind: $defaultInvoiceDescriptionTemplate: '%env(APP_INVOICE_DESCRIPTION_TEMPLATE)%' $invoiceSupplierAccount: '%env(string:APP_INVOICE_SUPPLIER_ACCOUNT)%' - $invoiceDefaultReceiverAccount: '%env(string:APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT)%' $projectBillingDefaultDescription: '%env(string:APP_PROJECT_BILLING_DEFAULT_DESCRIPTION)%' $planningDefaultDataProvider: '%env(string:APP_DEFAULT_PLANNING_DATA_PROVIDER)%' diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index b3d0d72e..ad586aa7 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -18,6 +18,7 @@ use App\Repository\InvoiceRepository; use App\Service\BillingService; use App\Service\ClientHelper; +use App\Service\InvoiceEntryHelper; use App\Service\ViewService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -37,6 +38,7 @@ public function __construct( private readonly BillingService $billingService, private readonly TranslatorInterface $translator, private readonly ViewService $viewService, + private readonly InvoiceEntryHelper $invoiceEntryHelper, ) { } @@ -60,13 +62,14 @@ public function index(Request $request, InvoiceRepository $invoiceRepository): R #[Route('/new', name: 'app_invoices_new', methods: ['GET', 'POST'])] #[IsGranted('EDIT')] - public function new(Request $request, InvoiceRepository $invoiceRepository, string $invoiceDefaultReceiverAccount): Response + public function new(Request $request, InvoiceRepository $invoiceRepository): Response { $invoice = new Invoice(); $form = $this->createForm(InvoiceNewType::class, $invoice); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $invoiceDefaultReceiverAccount = $this->invoiceEntryHelper->getDefaultAccount(); if (!empty($invoiceDefaultReceiverAccount)) { $invoice->setDefaultReceiverAccount($invoiceDefaultReceiverAccount); } diff --git a/src/Controller/InvoiceEntryController.php b/src/Controller/InvoiceEntryController.php index f41394a0..5323bf96 100644 --- a/src/Controller/InvoiceEntryController.php +++ b/src/Controller/InvoiceEntryController.php @@ -93,7 +93,7 @@ public function edit(Request $request, Invoice $invoice, InvoiceEntry $invoiceEn $options['disabled'] = true; } - $accounts = $this->invoiceEntryHelper->getAccounts($invoiceEntry->getAccount()); + $accounts = $this->invoiceEntryHelper->getAccountOptions($invoiceEntry->getAccount()); if (!empty($accounts)) { $options['invoice_entry_accounts'] = $accounts; } diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceEntryHelper.php index d8004585..d00d28c5 100644 --- a/src/Service/InvoiceEntryHelper.php +++ b/src/Service/InvoiceEntryHelper.php @@ -3,12 +3,18 @@ namespace App\Service; use App\Entity\InvoiceEntry; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; class InvoiceEntryHelper { + private readonly array $options; + public function __construct( - private readonly array $options + array $options ) { + $this->options = $this->resolveOptions($options); } /** @@ -18,17 +24,66 @@ public function __construct( * An account that must exist in the result * * @return array + * label => id */ - public function getAccounts(?string $account): array + public function getAccountOptions(?string $account): array { - $accounts = $this->options['accounts'] ?? []; + $options = []; - // Make sure that the default account exists. - if (isset($account) && !in_array($account, $accounts, true)) { - $accounts[$account] = $account; + $accounts = $this->getAccounts($account); + foreach ($accounts as $id => $info) { + $options[$info['label'] ?? $id] = $id; } - return $accounts; + return $options; + } + + /** + * Get ID of default account. + * + * @return string|null + * The default account ID if any + */ + public function getDefaultAccount(): ?string + { + $accounts = $this->getAccounts(null); + + foreach ($accounts as $id => $info) { + if ((bool) ($info['default'] ?? false)) { + return $id; + } + } + + return null; + } + + /** + * Get ID of product account. + * + * @return string|null + * The product account ID if any + */ + public function getProductAccount(): ?string + { + $accounts = $this->getAccounts(null); + + foreach ($accounts as $id => $info) { + if ((bool) ($info['product'] ?? false)) { + return $id; + } + } + + return null; + } + + /** + * Get account label based on configured accounts. + */ + public function getAccountLabel(string $account): string + { + $accounts = $this->getAccounts(null); + + return $accounts[$account]['label'] ?? $account; } /** @@ -37,16 +92,87 @@ public function getAccounts(?string $account): array public function isEditable(InvoiceEntry $entry): bool { return null === $entry->getInvoice()?->getProjectBilling() - || !empty($this->getAccounts(null)); + || count($this->getAccountOptions(null)) > 1; } /** - * Get account display name based on configured accounts. + * @param string $account + * + * @return array */ - public function getAccountDisplayName(string $account): string + private function getAccounts(?string $account): array { - $labels = array_flip($this->getAccounts($account)); + $accounts = $this->options['accounts'] ?? []; + + // Make sure that the default account exists. + if (isset($account) && !isset($accounts[$account])) { + $accounts[$account] = [ + 'label' => $account, + ]; + } + + // Add default values to all accounts. + foreach ($accounts as &$account) { + $account += [ + 'default' => false, + 'product' => false, + ]; + } + + return $accounts; + } + + private function resolveOptions(array $options): array + { + return (new OptionsResolver()) + ->setRequired(['accounts']) + ->setAllowedTypes('accounts', 'array') + ->setDefault('accounts', static function (OptionsResolver $resolver, Options $parent): void { + $resolver + ->setPrototype(true) + ->setRequired('label') + ->setAllowedTypes('label', 'string') + ->setDefaults([ + 'default' => false, + 'product' => false, + ]) + ->setAllowedTypes('default', 'bool') + ->setAllowedTypes('product', 'bool'); + }) + ->setAllowedValues('accounts', function (array $values) { + if (empty($values)) { + throw new InvalidOptionsException('At least one invoice entry account must be defined.'); + } + + if (count($values) > 1) { + $formatLabels = static function (array $accounts): string { + $labels = array_map(static fn (array $spec) => $spec['label'], $accounts); + + return empty($labels) ? 'none' : join(', ', array_map('json_encode', $labels)); + }; + + $defaults = array_filter($values, static fn (array $spec) => $spec['default']); + if (1 !== count($defaults)) { + throw new InvalidOptionsException(sprintf('Exactly one invoice entry account must be "default"; %s found.', $formatLabels($defaults))); + } + + $products = array_filter($values, static fn (array $spec) => $spec['product']); + if (1 !== count($products)) { + throw new InvalidOptionsException(sprintf('Exactly one invoice entry account must be "product"; %s found.', $formatLabels($products))); + } + } + + return true; + }) + ->addNormalizer('accounts', function (Options $options, array $values) { + // Make sure that a single account is the default account. + if (1 === count($values)) { + $values[array_key_first($values)]['default'] = true; + } + + return $values; + }) - return $labels[$account] ?? $account; + ->resolve($options); } } diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index a35e0eda..72b176d1 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -32,7 +32,7 @@ public function __construct( private readonly ClientHelper $clientHelper, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, - private readonly string $invoiceDefaultReceiverAccount, + private readonly InvoiceEntryHelper $invoiceEntryHelper, ) { } @@ -198,7 +198,7 @@ public function createProjectBilling(int $projectBillingId): void // TODO: MaterialNumberEnum::EXTERNAL_WITH_MOMS or MaterialNumberEnum::EXTERNAL_WITHOUT_MOMS? $invoice->setDefaultMaterialNumber($internal ? MaterialNumberEnum::INTERNAL : MaterialNumberEnum::EXTERNAL_WITH_MOMS); - $invoice->setDefaultReceiverAccount($this->invoiceDefaultReceiverAccount); + $invoice->setDefaultReceiverAccount($this->invoiceEntryHelper->getDefaultAccount()); /** @var Issue $issue */ foreach ($invoiceArray['issues'] as $issue) { @@ -251,7 +251,7 @@ public function createProjectBilling(int $projectBillingId): void ->setAmount($productIssue->getQuantity()) ->setTotalPrice($productIssue->getQuantity() * $product->getPriceAsFloat()) ->setMaterialNumber($invoice->getDefaultMaterialNumber()) - ->setAccount($invoice->getDefaultReceiverAccount()) + ->setAccount($this->invoiceEntryHelper->getProductAccount()) ->addIssueProduct($productIssue); // We don't add worklogs here, since they're already attached to the main invoice entry // (and only used to detect if an entry has been added to an invoice). diff --git a/templates/invoices/edit.html.twig b/templates/invoices/edit.html.twig index 2fff1d2c..d2b4eec4 100644 --- a/templates/invoices/edit.html.twig +++ b/templates/invoices/edit.html.twig @@ -113,7 +113,7 @@ } %} {% for index, invoice_entry in invoice.invoiceEntries %}
    - + From 9999296ffafe9bb2bee5dc47cd7312d9bb43055d Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 31 May 2024 09:44:42 +0200 Subject: [PATCH 039/134] 1547: Cleaned up --- .env | 12 ++++++------ .env.test | 7 ++++++- src/Service/InvoiceEntryHelper.php | 12 ++++-------- src/Service/ProjectBillingService.php | 3 ++- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 260c909a..9d227682 100644 --- a/.env +++ b/.env @@ -59,17 +59,17 @@ DEFAULT_URI=https://economics.local.itkdev.dk/ PRODUCT_QUANTITY_SCALE=2 # Invoice entry accounts. -# Must be a valid JSON object mapping account IDs to a label and additional metadata. +# Must be a valid JSON object mapping account IDs to a account metadata. # # Requirements; # # * At least one account must be defined. -# -# * One and only one account must be "default" (a single account will be -# * "default") -# +# * "label" is required. +# * One and only one account must be "default" (a single account will be +# "default") # * If more than one account is defined, one and only one account must be -# * "product". +# "product". +# * The "product" account cannot be "default" # # INVOICE_ENTRY_ACCOUNTS='{ # "test": { diff --git a/.env.test b/.env.test index 9b427383..629acfec 100644 --- a/.env.test +++ b/.env.test @@ -8,5 +8,10 @@ PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots DATABASE_URL="mysql://root:password@mariadb:3306/db_test?serverVersion=10.9.3-MariaDB&charset=utf8mb4" APP_INVOICE_SUPPLIER_ACCOUNT=1111 -APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT="XG-0000000000-00000" APP_PROJECT_BILLING_DEFAULT_DESCRIPTION="Beskrivelse" + +INVOICE_ENTRY_ACCOUNTS='{ +"test": { + "label": "test" +} +}' diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceEntryHelper.php index d00d28c5..f7586130 100644 --- a/src/Service/InvoiceEntryHelper.php +++ b/src/Service/InvoiceEntryHelper.php @@ -111,14 +111,6 @@ private function getAccounts(?string $account): array ]; } - // Add default values to all accounts. - foreach ($accounts as &$account) { - $account += [ - 'default' => false, - 'product' => false, - ]; - } - return $accounts; } @@ -160,6 +152,10 @@ private function resolveOptions(array $options): array if (1 !== count($products)) { throw new InvalidOptionsException(sprintf('Exactly one invoice entry account must be "product"; %s found.', $formatLabels($products))); } + + if ($products === $defaults) { + throw new InvalidOptionsException(sprintf('The account %s cannot be both "default" and "product".', array_key_first($defaults))); + } } return true; diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index 72b176d1..cf70456d 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -251,7 +251,8 @@ public function createProjectBilling(int $projectBillingId): void ->setAmount($productIssue->getQuantity()) ->setTotalPrice($productIssue->getQuantity() * $product->getPriceAsFloat()) ->setMaterialNumber($invoice->getDefaultMaterialNumber()) - ->setAccount($this->invoiceEntryHelper->getProductAccount()) + ->setAccount($this->invoiceEntryHelper->getProductAccount() + ?? $this->invoiceEntryHelper->getDefaultAccount()) ->addIssueProduct($productIssue); // We don't add worklogs here, since they're already attached to the main invoice entry // (and only used to detect if an entry has been added to an invoice). From aeb473cbc8eecb9e72dafc9359ddfe426723ade1 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 31 May 2024 12:33:06 +0200 Subject: [PATCH 040/134] 1547: Cleaned up default account config --- .env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 9d227682..d0326a8f 100644 --- a/.env +++ b/.env @@ -86,7 +86,7 @@ PRODUCT_QUANTITY_SCALE=2 # }' # INVOICE_ENTRY_ACCOUNTS='{ -"test": { - "label": "test" +"Define INVOICE_ENTRY_ACCOUNTS in .env.local": { + "label": "Define INVOICE_ENTRY_ACCOUNTS in .env.local" } }' From ce8c5ec3eb4271e4f61b71c9b19837607a6158dd Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 13:26:29 +0200 Subject: [PATCH 041/134] removed old reportstype file --- src/Form/ReportsType.php | 67 ---------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 src/Form/ReportsType.php diff --git a/src/Form/ReportsType.php b/src/Form/ReportsType.php deleted file mode 100644 index ef8f9ded..00000000 --- a/src/Form/ReportsType.php +++ /dev/null @@ -1,67 +0,0 @@ -add('dataProvider') - ->add('projectId', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', - 'required' => true, - 'label' => 'sprint_report.select_project', - 'label_attr' => ['class' => 'label'], - 'disabled' => true, - 'attr' => [ - 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormProjectId', - ], - 'row_attr' => ['class' => 'form-row form-choices'], - ]); - /* ->add('dateFrom', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', - 'required' => true, - 'label' => 'sprint_report.select_project', - 'label_attr' => ['class' => 'label'], - 'disabled' => true, - 'attr' => [ - 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormProjectId', - ], - 'row_attr' => ['class' => 'form-row form-choices'], - ]) - ->add('dateTo', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', - 'required' => true, - 'label' => 'sprint_report.select_project', - 'label_attr' => ['class' => 'label'], - 'disabled' => true, - 'attr' => [ - 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormProjectId', - ], - 'row_attr' => ['class' => 'form-row form-choices'], - ]);*/ - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => ReportsFormData::class, - 'attr' => [ - 'class' => 'form-default', - ], - ]); - } -} From 7ef8781d005853e5a2a31f7fb1433aa088177c5e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 13:30:34 +0200 Subject: [PATCH 042/134] Added fromDate and toDate to hour report, fixed layout to fit more input fields --- assets/styles/app.css | 14 +++- src/Controller/HourReportController.php | 86 ++++++++++++++++-------- src/Form/HourReportType.php | 13 ++++ src/Model/Reports/HourReportFormData.php | 2 + src/Repository/IssueRepository.php | 12 ++-- src/Service/HourReportService.php | 33 +++++++-- translations/messages.da.yaml | 3 + 7 files changed, 121 insertions(+), 42 deletions(-) diff --git a/assets/styles/app.css b/assets/styles/app.css index 0bafa90d..b37dcc4b 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -144,6 +144,10 @@ @apply grid gap-3 mb-6 md:grid-cols-4; } + #hour_report { + @apply grid gap-3 mb-6 md:grid-cols-6; + } + .form-default { @apply grid gap-3 mb-6 md:grid-cols-6; } @@ -152,7 +156,7 @@ @apply flex flex-col justify-end; } - #sprint_report > div { + #sprint_report > div, #hour_report > div { @apply flex flex-col; } @@ -167,7 +171,13 @@ .subheading { @apply font-medium leading-tight text-2xl mt-5 mb-5; } - + .hour-report-submit { + @apply mt-6; + } + .label.required::after { + content: '*'; + @apply text-red-500 ml-1; + } /* ** Alerts */ diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index 20a87a05..7af28c64 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -13,12 +13,14 @@ use App\Service\ViewService; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\ButtonType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -#[Route('/admin/reports')] +#[Route('/admin/reports/hour_report')] class HourReportController extends AbstractController { public function __construct( @@ -33,6 +35,7 @@ public function __construct( /** * @throws EconomicsException * @throws UnsupportedDataProviderException + * @throws \Exception */ #[Route('/', name: 'app_hour_report')] public function index(Request $request): Response @@ -46,7 +49,7 @@ public function index(Request $request): Response 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), 'method' => 'GET', 'attr' => [ - 'id' => 'sprint_report', + 'id' => 'hour_report', ], // Since this is only a filtering form, csrf is not needed. 'csrf_protection' => false, @@ -54,8 +57,8 @@ public function index(Request $request): Response $form->add('dataProvider', EntityType::class, [ 'class' => DataProvider::class, - 'required' => false, - 'label' => 'reports.hour.select_data_provider', + 'required' => true, + 'label' => 'reports.hour_report.select_data_provider', 'label_attr' => ['class' => 'label'], 'attr' => [ 'onchange' => 'this.form.submit()', @@ -66,10 +69,10 @@ public function index(Request $request): Response 'choices' => $this->dataProviderRepository->findAll(), ]); $form->add('projectId', ChoiceType::class, [ - 'placeholder' => 'reports.hour.select_option', + 'placeholder' => 'reports.hour_report.select_option', 'choices' => [], - 'required' => false, - 'label' => 'reports.hour.select_project', + 'required' => true, + 'label' => 'reports.hour_report.select_project', 'label_attr' => ['class' => 'label'], 'attr' => [ 'disabled' => true, @@ -77,10 +80,10 @@ public function index(Request $request): Response ], ]); $form->add('versionId', ChoiceType::class, [ - 'placeholder' => 'reports.hour.select_option', + 'placeholder' => 'reports.hour_report.select_option', 'choices' => [], - 'required' => false, - 'label' => 'reports.hour.select_milestone', + 'required' => true, + 'label' => 'reports.hour_report.select_milestone', 'label_attr' => ['class' => 'label'], 'attr' => [ 'disabled' => true, @@ -88,6 +91,8 @@ public function index(Request $request): Response ], ]); + + $requestData = $request->query->all('hour_report'); if (isset($requestData['sprint_report'])) { @@ -108,15 +113,12 @@ public function index(Request $request): Response $form->add('projectId', ChoiceType::class, [ 'placeholder' => 'sprint_report.select_an_option', 'choices' => $projectChoices, - 'required' => false, + 'required' => true, 'label' => 'sprint_report.select_project', 'label_attr' => ['class' => 'label'], 'row_attr' => ['class' => 'form-choices'], 'attr' => [ 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormProjectId', - 'data-choices-target' => 'choices', 'onchange' => 'this.form.submit()', ], ]); @@ -124,7 +126,7 @@ public function index(Request $request): Response } if ((!empty($requestData['dataProvider']) || $this->defaultDataProvider) && !empty($requestData['projectId'])) { if (empty($dataProvider)) { - throw new EconomicsException('reports.hour.select_data_provider_empty'); + throw new EconomicsException('reports.hour_report.select_data_provider_empty'); } $projectId = $requestData['projectId']; @@ -132,18 +134,47 @@ public function index(Request $request): Response // Override projectId with element with choices. $form->add('versionId', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', + 'placeholder' => 'reports.hour_report.select_an_option', 'choices' => $milestoneChoices, - 'required' => false, - 'label' => 'sprint_report.select_project', + 'required' => true, + 'label' => 'reports.hour_report.select_milestone', 'label_attr' => ['class' => 'label'], 'row_attr' => ['class' => 'form-choices'], 'attr' => [ 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormVersionId', - 'data-choices-target' => 'choices', - 'onchange' => 'this.form.submit()', + ], + ]); + $form->add('fromDate', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'reports.hour_report.select_fromdate', + 'label_attr' => ['class' => 'label'], + 'empty_data' => '', + 'by_reference' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-preselect-date' => $this->hourReportService->getFromDate(), + ], + ]); + $form->add('toDate', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'reports.hour_report.select_todate', + 'label_attr' => ['class' => 'label'], + 'empty_data' => '', + 'by_reference' => true, + 'attr' => [ + 'class' => 'form-element disabled', + 'data-preselect-date' => $this->hourReportService->getToDate(), + ], + ]); + $form->add('submit', ButtonType::class, [ + 'block_name' => 'Submit', + 'attr' => [ + 'onclick' => 'this.form.submit()', + 'class' => 'hour-report-submit button', ], ]); } @@ -152,16 +183,18 @@ public function index(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $projectId = $form->get('projectId')->getData(); - $milestoneId = $form->get('versionId')->getData() ?? '0'; - $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider; + $milestoneId = $form->get('versionId')->getData(); + $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider ?? null; + $fromDate = $form->has('fromDate') ? $form->get('fromDate')->getData() : new \DateTime($this->hourReportService->getFromDate()); + $toDate = $form->has('toDate') ? $form->get('toDate')->getData() : new \DateTime($this->hourReportService->getToDate()); if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { - $reportData = $this->hourReportService->getHourReport($projectId, $milestoneId); + $reportData = $this->hourReportService->getHourReport($projectId, $fromDate, $toDate, $milestoneId); $mode = 'hourReport'; } if (!empty($projectId) && !empty($selectedDataProvider) && '0' === $milestoneId) { - $reportData = $this->hourReportService->getHourReport($projectId); + $reportData = $this->hourReportService->getHourReport($projectId, $fromDate, $toDate); $mode = 'hourReport'; } } @@ -169,7 +202,6 @@ public function index(Request $request): Response return $this->render('reports/reports.html.twig', $this->viewService->addView([ 'controller_name' => 'HourReportController', 'form' => $form, - 'error' => $error ?? null, 'data' => $reportData, 'mode' => $mode, ])); diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index 662047b3..b1191ce5 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -5,6 +5,7 @@ use App\Model\Reports\HourReportFormData; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -40,6 +41,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'row_attr' => ['class' => 'form-row form-choices'], ]) + /*->add('fromDate', DateType::class, [ + 'required' => false, + 'label' => 'sprint_report.select_version', + 'label_attr' => ['class' => 'label'], + 'disabled' => true, + 'attr' => [ + 'class' => 'form-element', + 'data-sprint-report-target' => 'version', + 'data-action' => 'sprint-report#submitForm', + ], + 'row_attr' => ['class' => 'form-row form-choices'], + ])*/ ; } diff --git a/src/Model/Reports/HourReportFormData.php b/src/Model/Reports/HourReportFormData.php index ca6a8b41..525bd1a5 100644 --- a/src/Model/Reports/HourReportFormData.php +++ b/src/Model/Reports/HourReportFormData.php @@ -9,4 +9,6 @@ class HourReportFormData public DataProvider $dataProvider; public string $projectId; public string $versionId; + public \DateTimeInterface $fromDate; + public \DateTimeInterface $toDate; } diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 6ef2406e..41e34c0a 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -111,15 +111,11 @@ private function getClosedStatuses(Project $project): array ]; } - public function issueContainsVersion(Issue $issue, int $versionId): bool + public function issueContainsVersion(int $versionId): bool { - $qb = $this->createQueryBuilder('issue'); - - $qb->select('version.id') - ->leftJoin('issue.versions', 'version') - ->where('issue.id = :issueId') - ->andWhere('version.id = :versionId') - ->setParameters(['issueId' => $issue->getId(), 'versionId' => $versionId]); + $qb = $this->createQueryBuilder('issue') + ->where(':version MEMBER OF issue.versions') + ->setParameter('version', $versionId); // If the query returns any result, that means a versionId match has been found. return (bool) $qb->getQuery()->getOneOrNullResult(); diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index d3c4bf3b..cc24be6b 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -60,7 +60,7 @@ public function getMilestones(string $projectId, bool $allAllOption = false): ar /** * @throws EconomicsException */ - public function getHourReport(string $projectId, int $versionId = null): HourReportData + public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate, int $versionId = null): HourReportData { if (!$projectId) { throw new EconomicsException('No project id specified'); @@ -71,15 +71,19 @@ public function getHourReport(string $projectId, int $versionId = null): HourRep foreach ($projectIssues as $issue) { // If version is provided, we only want the issues containing the versionId if ($versionId) { - $issueHasVersion = $this->issueRepository->issueContainsVersion($issue, $versionId); + $issueHasVersion = $issue->getVersions()->exists(function ($key, $value) use ($versionId) { + return $value->getId() === $versionId; + }); if (!$issueHasVersion) { continue; } } $totalTicketEstimated = (float) $issue->planHours; + $timesheetData = $this->worklogRepository->findBy(['issue' => $issue->getId()]); - list($timesheets, $totalTicketSpent) = $this->processTimesheetsData($timesheetData); + + list($timesheets, $totalTicketSpent) = $this->processTimesheetsData($timesheetData, $fromDate, $toDate); $projectTicket = new HourReportProjectTicket( $issue->getId(), @@ -106,18 +110,37 @@ public function getHourReport(string $projectId, int $versionId = null): HourRep return $hourReportData; } - private function processTimesheetsData($timesheetsData): array + private function processTimesheetsData($timesheetsData, $fromDate, $toDate): array { $timesheets = []; $totalTicketSpent = 0; foreach ($timesheetsData as $timesheetDatum) { + if ($fromDate && $toDate) { + $timesheetDate = $timesheetDatum->getStarted(); + if ($timesheetDate < $fromDate || $timesheetDate > $toDate) { + continue; + } + } + $hoursSpent = (float) ($timesheetDatum->getTimeSpentSeconds() / 3600); $timesheet = new HourReportTimesheet($timesheetDatum->getId(), $hoursSpent); $timesheets[] = $timesheet; $totalTicketSpent += $hoursSpent; } - return [$timesheets, $totalTicketSpent]; } + + public function getFromDate(): string + { + $fromDate = new \DateTime(); + $fromDate->modify('first day of this month'); + return $fromDate->format('Y-m-d'); + } + + public function getToDate(): string + { + $fromDate = new \DateTime(); + return $fromDate->format('Y-m-d'); + } } diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 764702f3..2aa7d06d 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -557,6 +557,9 @@ reports: estimated_hours: 'Estimeret' logged_hours: 'Logget' total: 'Total' + select_fromdate: 'Fra dato' + select_todate: 'Til dato' + select_an_option: 'Vælg en mulighed' view: list_title: 'Views' From 6d524a4999d3b11deac2718dd1811a8115d6906b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 14:23:57 +0200 Subject: [PATCH 043/134] coding standards --- src/Model/Reports/HourReportData.php | 2 +- src/Model/Reports/HourReportProjectTicket.php | 6 +++-- src/Service/HourReportService.php | 26 ++++++++++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Model/Reports/HourReportData.php b/src/Model/Reports/HourReportData.php index 8de90ac0..be68959b 100644 --- a/src/Model/Reports/HourReportData.php +++ b/src/Model/Reports/HourReportData.php @@ -9,7 +9,7 @@ class HourReportData public readonly string $id; public float $projectTotalSpent; public float $projectTotalEstimated; - /** @var ArrayCollection */ + /** @var ArrayCollection */ public ArrayCollection $projectTags; public function __construct(float $projectTotalSpent, float $projectTotalEstimated) diff --git a/src/Model/Reports/HourReportProjectTicket.php b/src/Model/Reports/HourReportProjectTicket.php index 395fa866..09ea333d 100644 --- a/src/Model/Reports/HourReportProjectTicket.php +++ b/src/Model/Reports/HourReportProjectTicket.php @@ -8,9 +8,10 @@ class HourReportProjectTicket { public readonly string $id; public readonly string $headline; - public string $totalEstimated; - public string $totalSpent; + public float $totalEstimated; + public float $totalSpent; public ArrayCollection $timesheets; + public ArrayCollection $projectTickets; public function __construct($id, $headline, $totalEstimated, $totalSpent) { @@ -19,5 +20,6 @@ public function __construct($id, $headline, $totalEstimated, $totalSpent) $this->totalEstimated = $totalEstimated; $this->totalSpent = $totalSpent; $this->timesheets = new ArrayCollection(); + $this->projectTickets = new ArrayCollection(); } } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index cc24be6b..b5029716 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -51,7 +51,8 @@ public function getMilestones(string $projectId, bool $allAllOption = false): ar $milestoneChoices['All milestones'] = '0'; } foreach ($milestones as $milestone) { - $milestoneChoices[$milestone->getName()] = $milestone->getId(); + $milestoneName = $milestone->getName() ?? ''; + $milestoneChoices[$milestoneName] = $milestone->getId(); } return $milestoneChoices; @@ -94,15 +95,23 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, $projectTicket->timesheets->add($timesheets); - if ($hourReportData->projectTags->containsKey($issue->getEpicName())) { - $projectTag = $hourReportData->projectTags->get($issue->getEpicName()); - $projectTag->totalEstimated += $totalTicketEstimated; - $projectTag->totalSpent += $totalTicketSpent; + $issueEpicName = $issue->getEpicName() ?? ''; + + if ($hourReportData->projectTags->containsKey($issueEpicName)) { + $projectTag = $hourReportData->projectTags->get($issueEpicName); + if ($projectTag) { + $projectTag->totalEstimated += $totalTicketEstimated; + $projectTag->totalSpent += $totalTicketSpent; + } } else { - $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issue->getEpicName()); + $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issueEpicName); + } + if (!$projectTag) { + throw new EconomicsException('Project tag not found'); } $projectTag->projectTickets->add($projectTicket); - $hourReportData->projectTags->set($issue->getEpicName(), $projectTag); + + $hourReportData->projectTags->set($issueEpicName, $projectTag); $hourReportData->projectTotalEstimated += $totalTicketEstimated; $hourReportData->projectTotalSpent += $totalTicketSpent; } @@ -128,6 +137,7 @@ private function processTimesheetsData($timesheetsData, $fromDate, $toDate): arr $timesheets[] = $timesheet; $totalTicketSpent += $hoursSpent; } + return [$timesheets, $totalTicketSpent]; } @@ -135,12 +145,14 @@ public function getFromDate(): string { $fromDate = new \DateTime(); $fromDate->modify('first day of this month'); + return $fromDate->format('Y-m-d'); } public function getToDate(): string { $fromDate = new \DateTime(); + return $fromDate->format('Y-m-d'); } } From 607a4fb60e856edc657d6edc97d3611af369f4b2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 14:29:51 +0200 Subject: [PATCH 044/134] coding standards --- src/Controller/HourReportController.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index 7af28c64..c3908cff 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -4,7 +4,6 @@ use App\Entity\DataProvider; use App\Exception\EconomicsException; -use App\Exception\UnsupportedDataProviderException; use App\Form\HourReportType; use App\Model\Reports\HourReportFormData; use App\Repository\DataProviderRepository; @@ -34,7 +33,6 @@ public function __construct( /** * @throws EconomicsException - * @throws UnsupportedDataProviderException * @throws \Exception */ #[Route('/', name: 'app_hour_report')] @@ -91,8 +89,6 @@ public function index(Request $request): Response ], ]); - - $requestData = $request->query->all('hour_report'); if (isset($requestData['sprint_report'])) { From e5cbd156c8e30b6c3e77431b4e8785f66807906c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 14:34:08 +0200 Subject: [PATCH 045/134] removed commented out code in hourreporttype.php --- src/Form/HourReportType.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index b1191ce5..daddde07 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -40,20 +40,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'data-action' => 'sprint-report#submitForm', ], 'row_attr' => ['class' => 'form-row form-choices'], - ]) - /*->add('fromDate', DateType::class, [ - 'required' => false, - 'label' => 'sprint_report.select_version', - 'label_attr' => ['class' => 'label'], - 'disabled' => true, - 'attr' => [ - 'class' => 'form-element', - 'data-sprint-report-target' => 'version', - 'data-action' => 'sprint-report#submitForm', - ], - 'row_attr' => ['class' => 'form-row form-choices'], - ])*/ - ; + ]); } public function configureOptions(OptionsResolver $resolver): void From 9c8faab9a5c0ba6ce25479a1fea1fa6be1af907c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 14:43:15 +0200 Subject: [PATCH 046/134] updated changelog + coding standards --- CHANGELOG.md | 3 +++ src/Form/HourReportType.php | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4e5608..5ef9ba7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* NOTE: APP_DEFAULT_PLANNING_DATA_PROVIDER has been changed to APP_DEFAULT_DATA_PROVIDER. This has to be changed when releasing. +* [PR-117](https://github.com/itk-dev/economics/pull/117) + 1211: Added hour report * [PR-121](https://github.com/itk-dev/economics/pull/121) 1485: Fixed floating number issues * [PR-120](https://github.com/itk-dev/economics/pull/120) diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index daddde07..c66ce0d3 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -5,7 +5,6 @@ use App\Model\Reports\HourReportFormData; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; From 5f067583faae229f86baf89cc1f11a93b0052348 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 31 May 2024 15:04:11 +0200 Subject: [PATCH 047/134] changelog coding standards --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce513665..e9019fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -<<<<<<< feature/1211-hour-report * NOTE: APP_DEFAULT_PLANNING_DATA_PROVIDER has been changed to APP_DEFAULT_DATA_PROVIDER. This has to be changed when releasing. * [PR-117](https://github.com/itk-dev/economics/pull/117) 1211: Added hour report -======= * [PR-125](https://github.com/itk-dev/economics/pull/125) 1547: Set account based on invoice entry type * [PR-123](https://github.com/itk-dev/economics/pull/123) 1544: Allowed invoicing issues with products and no worklogs * [PR-122](https://github.com/itk-dev/economics/pull/122) 1547: Added invoice entry account selector ->>>>>>> develop * [PR-121](https://github.com/itk-dev/economics/pull/121) 1485: Fixed floating number issues * [PR-120](https://github.com/itk-dev/economics/pull/120) From cbabae57706fe35edfacd704c5172c14c2774b63 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 31 May 2024 15:30:31 +0200 Subject: [PATCH 048/134] 1590: Added worklog product as prefix on product invoice entries --- CHANGELOG.md | 2 ++ migrations/Version20240531125801.php | 49 +++++++++++++++++++++++++++ src/Service/ProjectBillingService.php | 7 +++- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 migrations/Version20240531125801.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 918d2b7b..464879ad 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-126](https://github.com/itk-dev/economics/pull/126) + 1590: Added worklog product as prefix on product invoice entries * [PR-125](https://github.com/itk-dev/economics/pull/125) 1547: Set account based on invoice entry type * [PR-123](https://github.com/itk-dev/economics/pull/123) diff --git a/migrations/Version20240531125801.php b/migrations/Version20240531125801.php new file mode 100644 index 00000000..c7c504e8 --- /dev/null +++ b/migrations/Version20240531125801.php @@ -0,0 +1,49 @@ +addSql(<<<'SQL' +UPDATE + invoice_entry +SET + product = CONCAT( + IF( + ISNULL( + -- Get "product" from preceding worklog entry (this expression is repeated below). + (SELECT product FROM invoice_entry AS ie WHERE ie.invoice_id = invoice_entry.invoice_id AND ie.entry_type = 'worklog' AND ie.entry_index < invoice_entry.entry_index ORDER BY ie.entry_index DESC LIMIT 1) + ), + '', + CONCAT( + (SELECT product FROM invoice_entry AS ie WHERE ie.invoice_id = invoice_entry.invoice_id AND ie.entry_type = 'worklog' AND ie.entry_index < invoice_entry.entry_index ORDER BY ie.entry_index DESC LIMIT 1), + ': ' + ) + ), + product + ) +WHERE entry_type = 'product' +SQL); + } + + public function down(Schema $schema): void + { + // There is not going back (or down)! + } +} diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index cf70456d..cef1b1c3 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -236,6 +236,7 @@ public function createProjectBilling(int $projectBillingId): void $this->entityManager->persist($invoiceEntry); } + $invoiceEntryProductName = $invoiceEntry->getProduct(); // Add invoice entries for each product. foreach ($issue->getProducts() as $productIssue) { $product = $productIssue->getProduct(); @@ -243,10 +244,14 @@ public function createProjectBilling(int $projectBillingId): void continue; } + $productName = $product->getName() ?? ''; $productInvoiceEntry = (new InvoiceEntry()) ->setEntryType(InvoiceEntryTypeEnum::PRODUCT) ->setDescription($productIssue->getDescription()) - ->setProduct($product->getName()) + ->setProduct(null === $invoiceEntryProductName + ? $productName + : sprintf('%s: %s', $invoiceEntryProductName, $productName) + ) ->setPrice($product->getPriceAsFloat()) ->setAmount($productIssue->getQuantity()) ->setTotalPrice($productIssue->getQuantity() * $product->getPriceAsFloat()) From 241831dc191f3605657439ebfea07573f34e1c98 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 3 Jun 2024 13:06:01 +0200 Subject: [PATCH 049/134] Release 2.3.0 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464879ad..e889a0fd 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] +## [2.3.0] - 2024-06-03 + * [PR-126](https://github.com/itk-dev/economics/pull/126) 1590: Added worklog product as prefix on product invoice entries * [PR-125](https://github.com/itk-dev/economics/pull/125) @@ -257,7 +259,8 @@ complete process. * Updated to authorization code flow. * Changed worklog save button styling to be sticky. -[Unreleased]: https://github.com/itk-dev/economics/compare/2.2.0...HEAD +[Unreleased]: https://github.com/itk-dev/economics/compare/2.3.0...HEAD +[2.3.0]: https://github.com/itk-dev/economics/compare/2.2.0...2.3.0 [2.2.0]: https://github.com/itk-dev/economics/compare/2.1.2...2.2.0 [2.1.2]: https://github.com/itk-dev/economics/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/itk-dev/economics/compare/2.1.0...2.1.1 From b64773db181f657bef6a3bcfba2cf951ab2ab84f Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:09:13 +0200 Subject: [PATCH 050/134] 1595: Added retryable http client to handle rate limiting --- .env | 2 ++ config/services.yaml | 2 ++ src/Service/DataProviderService.php | 12 ++++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.env b/.env index d0326a8f..662406af 100644 --- a/.env +++ b/.env @@ -27,6 +27,8 @@ APP_INVOICE_SUPPLIER_ACCOUNT=APP_INVOICE_SUPPLIER_ACCOUNT APP_INVOICE_DESCRIPTION_TEMPLATE="Spørgsmål vedrørende fakturaen rettes til %name%, %email%." APP_PROJECT_BILLING_DEFAULT_DESCRIPTION= APP_DEFAULT_PLANNING_DATA_PROVIDER= +APP_HTTP_CLIENT_RETRY_DELAY_MS=1000 +APP_HTTP_CLIENT_MAX_RETRIES=3 ###< Planning ### ###> itk-dev/openid-connect-bundle ### diff --git a/config/services.yaml b/config/services.yaml index 6bd7f2eb..db332bfe 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -42,6 +42,8 @@ services: $weekGoalLow: '%env(float:APP_WEEK_GOAL_LOW)%' $weekGoalHigh: '%env(float:APP_WEEK_GOAL_HIGH)%' $sprintNameRegex: '%env(API_SERVICE_SPRINT_NAME_REGEX)%' + $httpClientRetryDelayMs: '%env(int:APP_HTTP_CLIENT_RETRY_DELAY_MS)%' + $httpClientMaxRetries: '%env(int:APP_HTTP_CLIENT_MAX_RETRIES)%' App\Service\ClientHelper: arguments: diff --git a/src/Service/DataProviderService.php b/src/Service/DataProviderService.php index 01a53691..2ba4f993 100644 --- a/src/Service/DataProviderService.php +++ b/src/Service/DataProviderService.php @@ -7,7 +7,9 @@ use App\Exception\UnsupportedDataProviderException; use App\Interface\DataProviderServiceInterface; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; class DataProviderService { @@ -24,6 +26,8 @@ public function __construct( protected readonly float $weekGoalLow, protected readonly float $weekGoalHigh, protected readonly string $sprintNameRegex, + protected readonly int $httpClientRetryDelayMs = 1000, + protected readonly int $httpClientMaxRetries = 3, ) { } @@ -49,8 +53,10 @@ public function getService(DataProvider $dataProvider): DataProviderServiceInter 'auth_basic' => $dataProvider->getSecret(), ]); + $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([429], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); + $service = new JiraApiService( - $client, + $retryableHttpClient, $this->customFieldMappings, $this->defaultBoard, $url, @@ -67,8 +73,10 @@ public function getService(DataProvider $dataProvider): DataProviderServiceInter ], ]); + $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([429], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); + $service = new LeantimeApiService( - $client, + $retryableHttpClient, $url, $this->weekGoalLow, $this->weekGoalHigh, From 0d21a094fa8933e54399fed66154b491d62fea24 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 10:41:28 +0200 Subject: [PATCH 051/134] re-added error controller variable --- src/Controller/HourReportController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index c3908cff..95e7eec5 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -41,6 +41,7 @@ public function index(Request $request): Response $reportData = null; $mode = 'reports'; + $error = null; $reportFormData = new HourReportFormData(); $form = $this->createForm(HourReportType::class, $reportFormData, [ @@ -200,6 +201,7 @@ public function index(Request $request): Response 'form' => $form, 'data' => $reportData, 'mode' => $mode, + 'error' => $error, ])); } } From 7ea187ef702c2c8e4494a373c6c258d850577d69 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:10:16 +0200 Subject: [PATCH 052/134] 1595: Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464879ad..5b0f496a 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-128](https://github.com/itk-dev/economics/pull/128) + 1595: Added retryable http client decorator for handling rate limiting. * [PR-126](https://github.com/itk-dev/economics/pull/126) 1590: Added worklog product as prefix on product invoice entries * [PR-125](https://github.com/itk-dev/economics/pull/125) From 829960c23d70cae537757cf74a3d790c16379501 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:16:40 +0200 Subject: [PATCH 053/134] 1595: Applied coding standards --- src/Service/DataProviderService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/DataProviderService.php b/src/Service/DataProviderService.php index 2ba4f993..2624e0b6 100644 --- a/src/Service/DataProviderService.php +++ b/src/Service/DataProviderService.php @@ -7,9 +7,9 @@ use App\Exception\UnsupportedDataProviderException; use App\Interface\DataProviderServiceInterface; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; class DataProviderService { From 03b2851cce433e696061a9935dbe39940b17207b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 10:53:20 +0200 Subject: [PATCH 054/134] minor corrections, coding standards --- src/Controller/HourReportController.php | 4 +--- src/Repository/IssueRepository.php | 5 ++--- templates/reports/hourReport.html.twig | 8 +++----- templates/reports/index.html.twig | 9 ++------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index 95e7eec5..f17948e1 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -122,9 +122,6 @@ public function index(Request $request): Response } } if ((!empty($requestData['dataProvider']) || $this->defaultDataProvider) && !empty($requestData['projectId'])) { - if (empty($dataProvider)) { - throw new EconomicsException('reports.hour_report.select_data_provider_empty'); - } $projectId = $requestData['projectId']; $milestoneChoices = $this->hourReportService->getMilestones($projectId, true); @@ -190,6 +187,7 @@ public function index(Request $request): Response $mode = 'hourReport'; } + // If milestone is '0', it will evaluate as empty above, but really we want to get the report for all milestones if (!empty($projectId) && !empty($selectedDataProvider) && '0' === $milestoneId) { $reportData = $this->hourReportService->getHourReport($projectId, $fromDate, $toDate); $mode = 'hourReport'; diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 41e34c0a..a9b7039f 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -111,13 +111,12 @@ private function getClosedStatuses(Project $project): array ]; } - public function issueContainsVersion(int $versionId): bool + public function issueContainingVersion(int $versionId): array { $qb = $this->createQueryBuilder('issue') ->where(':version MEMBER OF issue.versions') ->setParameter('version', $versionId); - // If the query returns any result, that means a versionId match has been found. - return (bool) $qb->getQuery()->getOneOrNullResult(); + return $qb->getQuery()->getResult(); } } diff --git a/templates/reports/hourReport.html.twig b/templates/reports/hourReport.html.twig index a51b0afe..2419e3cc 100644 --- a/templates/reports/hourReport.html.twig +++ b/templates/reports/hourReport.html.twig @@ -20,7 +20,7 @@ - @@ -59,12 +59,10 @@ {% endfor %} - {% endfor %} - - + diff --git a/templates/reports/index.html.twig b/templates/reports/index.html.twig index ba0bb208..69652726 100644 --- a/templates/reports/index.html.twig +++ b/templates/reports/index.html.twig @@ -1,15 +1,10 @@ -{% extends 'base.html.twig' %} - -{% block title %}{{ 'planning.title'|trans }}{% endblock %} +{% extends 'base.html.twig' {% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} {% block content %} -

    {{ 'planning.title'|trans }}

    +

    {{ 'reports.hour_report.title'|trans }}

    {{ form_start(form) }} {{ form_row(form.dataProvider) }} -
    - -
    {{ form_end(form) }} {% if error is not null %} From bc37fb4d805616dac74df911f84c1549ebdd94e0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 10:54:17 +0200 Subject: [PATCH 055/134] Optimized getting hour report issues in cases where milestone is provided --- src/Service/HourReportService.php | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index b5029716..09fe5f4d 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -9,16 +9,12 @@ use App\Model\Reports\HourReportTimesheet; use App\Repository\IssueRepository; use App\Repository\ProjectRepository; -use App\Repository\ProjectVersionBudgetRepository; use App\Repository\VersionRepository; use App\Repository\WorklogRepository; -use Doctrine\ORM\EntityManagerInterface; class HourReportService { public function __construct( - private readonly ProjectVersionBudgetRepository $budgetRepository, - private readonly EntityManagerInterface $entityManager, private readonly IssueRepository $issueRepository, private readonly WorklogRepository $worklogRepository, private readonly ProjectRepository $projectRepository, @@ -40,14 +36,11 @@ public function getProjects(): array return $projectChoices; } - /** - * @throws EconomicsException - */ - public function getMilestones(string $projectId, bool $allAllOption = false): array + public function getMilestones(string $projectId, bool $includeAllOption = false): array { $milestones = $this->versionRepository->findBy(['project' => $projectId]); $milestoneChoices = []; - if ($allAllOption) { + if ($includeAllOption) { $milestoneChoices['All milestones'] = '0'; } foreach ($milestones as $milestone) { @@ -67,19 +60,15 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, throw new EconomicsException('No project id specified'); } $hourReportData = new HourReportData(0, 0); - $projectIssues = $this->issueRepository->findBy(['project' => $projectId]); - foreach ($projectIssues as $issue) { - // If version is provided, we only want the issues containing the versionId - if ($versionId) { - $issueHasVersion = $issue->getVersions()->exists(function ($key, $value) use ($versionId) { - return $value->getId() === $versionId; - }); + // If version is provided, we only want the issues containing the versionId + if ($versionId) { + $projectIssues = $this->issueRepository->issueContainingVersion($versionId); + } else { + $projectIssues = $this->issueRepository->findBy(['project' => $projectId]); + } - if (!$issueHasVersion) { - continue; - } - } + foreach ($projectIssues as $issue) { $totalTicketEstimated = (float) $issue->planHours; $timesheetData = $this->worklogRepository->findBy(['issue' => $issue->getId()]); @@ -95,7 +84,7 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, $projectTicket->timesheets->add($timesheets); - $issueEpicName = $issue->getEpicName() ?? ''; + $issueEpicName = (string) $issue->getEpicName() ?? ''; if ($hourReportData->projectTags->containsKey($issueEpicName)) { $projectTag = $hourReportData->projectTags->get($issueEpicName); From 63c00b637bc1c4db8f432ca73e89ba1aaff60b67 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 10:56:07 +0200 Subject: [PATCH 056/134] coding standards --- src/Service/HourReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 09fe5f4d..24832ad9 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -84,7 +84,7 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, $projectTicket->timesheets->add($timesheets); - $issueEpicName = (string) $issue->getEpicName() ?? ''; + $issueEpicName = $issue->getEpicName() ?? ''; if ($hourReportData->projectTags->containsKey($issueEpicName)) { $projectTag = $hourReportData->projectTags->get($issueEpicName); From 8d648fb9cfbb011d739915bb10066bec389a3ac6 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:52:11 +0200 Subject: [PATCH 057/134] 1595: Changed to Response constant --- src/Service/DataProviderService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Service/DataProviderService.php b/src/Service/DataProviderService.php index 2624e0b6..738ac0a1 100644 --- a/src/Service/DataProviderService.php +++ b/src/Service/DataProviderService.php @@ -9,6 +9,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Component\HttpClient\RetryableHttpClient; +use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\HttpClient\HttpClientInterface; class DataProviderService @@ -53,7 +54,7 @@ public function getService(DataProvider $dataProvider): DataProviderServiceInter 'auth_basic' => $dataProvider->getSecret(), ]); - $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([429], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); + $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([Response::HTTP_TOO_MANY_REQUESTS], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); $service = new JiraApiService( $retryableHttpClient, @@ -73,7 +74,7 @@ public function getService(DataProvider $dataProvider): DataProviderServiceInter ], ]); - $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([429], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); + $retryableHttpClient = new RetryableHttpClient($client, new GenericRetryStrategy([Response::HTTP_TOO_MANY_REQUESTS], $this->httpClientRetryDelayMs, 1.0), $this->httpClientMaxRetries); $service = new LeantimeApiService( $retryableHttpClient, From 82bbb122f474c19cd8f2f78dedf50a08f32378a7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 12:15:24 +0200 Subject: [PATCH 058/134] coding standards --- src/Entity/Issue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index b9f45126..6887ab1a 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -152,7 +152,7 @@ public function setEpicKey(?string $epicKey): self return $this; } - public function getEpicName(): ?string + public function getEpicName(): string { return $this->epicName; } From 7dbf00c75d34b674dff1c1d158b068639fa9530b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 12:21:51 +0200 Subject: [PATCH 059/134] coding standards --- src/Entity/Issue.php | 2 +- src/Service/HourReportService.php | 2 +- templates/reports/index.html.twig | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 6887ab1a..b9f45126 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -152,7 +152,7 @@ public function setEpicKey(?string $epicKey): self return $this; } - public function getEpicName(): string + public function getEpicName(): ?string { return $this->epicName; } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 24832ad9..9febc348 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -87,7 +87,7 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, $issueEpicName = $issue->getEpicName() ?? ''; if ($hourReportData->projectTags->containsKey($issueEpicName)) { - $projectTag = $hourReportData->projectTags->get($issueEpicName); + $projectTag = $hourReportData->projectTags->get((string) $issueEpicName); if ($projectTag) { $projectTag->totalEstimated += $totalTicketEstimated; $projectTag->totalSpent += $totalTicketSpent; diff --git a/templates/reports/index.html.twig b/templates/reports/index.html.twig index 69652726..e5bd8d2b 100644 --- a/templates/reports/index.html.twig +++ b/templates/reports/index.html.twig @@ -1,4 +1,6 @@ -{% extends 'base.html.twig' {% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} +{% extends 'base.html.twig' %} + +{% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} {% block content %}

    {{ 'reports.hour_report.title'|trans }}

    From f0536a373b77a673bd40d42ecb22552d06c94ddd Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 12:23:14 +0200 Subject: [PATCH 060/134] coding standards --- src/Service/HourReportService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 9febc348..346d683b 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -93,14 +93,14 @@ public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, $projectTag->totalSpent += $totalTicketSpent; } } else { - $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, $issueEpicName); + $projectTag = new HourReportProjectTag($totalTicketEstimated, $totalTicketSpent, (string) $issueEpicName); } if (!$projectTag) { throw new EconomicsException('Project tag not found'); } $projectTag->projectTickets->add($projectTicket); - $hourReportData->projectTags->set($issueEpicName, $projectTag); + $hourReportData->projectTags->set((string) $issueEpicName, $projectTag); $hourReportData->projectTotalEstimated += $totalTicketEstimated; $hourReportData->projectTotalSpent += $totalTicketSpent; } From 7f6f23d9087fae53d13c6efde7c8856aeb415bc4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 12:28:34 +0200 Subject: [PATCH 061/134] removed hide option for milestones in hour report table view --- templates/reports/hourReport.html.twig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/templates/reports/hourReport.html.twig b/templates/reports/hourReport.html.twig index 2419e3cc..118fd36e 100644 --- a/templates/reports/hourReport.html.twig +++ b/templates/reports/hourReport.html.twig @@ -25,12 +25,6 @@ {{ projectTag.tag }} -
    {{ 'invoices.invoice_entry_receiver_acccount'|trans }}{{ 'invoices.invoice_entry_receiver_account'|trans }} {{ 'invoices.invoice_entry_material_number'|trans }} {{ 'invoices.invoice_entry_product'|trans }} {{ 'invoices.invoice_entry_amount'|trans }}
    {{ invoice_entry.account }}{{ invoice_entry_helper.getAccountDisplayName(invoice_entry.account) }} {{ invoice_entry.materialNumber.value ?? '' }} {{ invoice_entry.product }} {{ invoice_entry.amount }}
    {{ 'planning.projects'|trans }} - Estimeret + {{ 'reports.hour_report.estimated_hours'|trans }} - Logget + {{ 'reports.hour_report.logged_hours'|trans }}
    Total{{ 'reports.hour_report.total'|trans }} {{ data.projectTotalEstimated }} {{ data.projectTotalSpent }}
    {{ invoice_entry_helper.getAccountDisplayName(invoice_entry.account) }}{{ invoice_entry_helper.getAccountLabel(invoice_entry.account) }} {{ invoice_entry.materialNumber.value ?? '' }} {{ invoice_entry.product }} {{ invoice_entry.amount }}
    +
    {{ projectTag.tag }} @@ -38,7 +38,7 @@
    - +
    {{ projectTag.totalEstimated }} {{ projectTicket.totalSpent }}
    {{ 'reports.hour_report.total'|trans }}{{ 'reports.hour_report.total'|trans }} {{ data.projectTotalEstimated }} {{ data.projectTotalSpent }}
    + + + + + + + + + + {% for worker in data.workers %} + + + + + + + + + {# + {% for projectTicket in projectTag.projectTickets %} + + + + + + {% endfor %} + #} + + {% endfor %} +
    {{ 'planning.projects'|trans }} + {{ 'reports.workload_report.workload'|trans }} + + {{ 'reports.workload_report.logged_hours'|trans }} +
    +
    + + {{ worker.email }} + + + +
    +
    + {{ worker.workload }} + + {{ worker.hoursLogged }} +
    +
    + + {{ projectTicket.headline }} + +
    +
    {{ projectTicket.totalEstimated }}{{ projectTicket.totalSpent }}
    +
    From fac6de257b1c80364da86db87a1d9443e4d729d2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 11:12:59 +0200 Subject: [PATCH 073/134] Added CRUD for new worker entity --- src/Controller/WorkerController.php | 81 +++++++++++++++++++++++ src/Form/WorkerType.php | 26 ++++++++ templates/components/navigation.html.twig | 1 + templates/worker/edit.html.twig | 13 ++++ templates/worker/index.html.twig | 39 +++++++++++ 5 files changed, 160 insertions(+) create mode 100644 src/Controller/WorkerController.php create mode 100644 src/Form/WorkerType.php create mode 100644 templates/worker/edit.html.twig create mode 100644 templates/worker/index.html.twig diff --git a/src/Controller/WorkerController.php b/src/Controller/WorkerController.php new file mode 100644 index 00000000..404d5914 --- /dev/null +++ b/src/Controller/WorkerController.php @@ -0,0 +1,81 @@ +render('worker/index.html.twig', [ + 'workers' => $workerRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_worker_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $worker = new Worker(); + $form = $this->createForm(WorkerType::class, $worker); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($worker); + $entityManager->flush(); + + return $this->redirectToRoute('app_worker_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('worker/new.html.twig', [ + 'worker' => $worker, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_worker_show', methods: ['GET'])] + public function show(Worker $worker): Response + { + return $this->render('worker/show.html.twig', [ + 'worker' => $worker, + ]); + } + + #[Route('/{id}/edit', name: 'app_worker_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Worker $worker, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(WorkerType::class, $worker); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_worker_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('worker/edit.html.twig', [ + 'worker' => $worker, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_worker_delete', methods: ['POST'])] + public function delete(Request $request, Worker $worker, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$worker->getId(), $request->request->get('_token'))) { + $entityManager->remove($worker); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_worker_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Form/WorkerType.php b/src/Form/WorkerType.php new file mode 100644 index 00000000..f27a73c1 --- /dev/null +++ b/src/Form/WorkerType.php @@ -0,0 +1,26 @@ +add('email') + ->add('workload') + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Worker::class, + ]); + } +} diff --git a/templates/components/navigation.html.twig b/templates/components/navigation.html.twig index 8597f6be..5ce3e236 100644 --- a/templates/components/navigation.html.twig +++ b/templates/components/navigation.html.twig @@ -50,6 +50,7 @@ {% include 'components/navigation-item.html.twig' with {title: 'navigation.projects'|trans, role: 'ROLE_ADMIN', route: path('app_project_index', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.client'|trans, role: 'ROLE_ADMIN', route: path('app_client_index', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.account'|trans, role: 'ROLE_ADMIN', route: path('app_account_index', {}|merge(view is defined ? {view: view} : {}))} %} + {% include 'components/navigation-item.html.twig' with {title: 'navigation.worker'|trans, role: 'ROLE_ADMIN', route: path('app_worker_index', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.user'|trans, role: 'ROLE_ADMIN', route: path('app_user_index', {}|merge(view is defined ? {view: view} : {}))} %} {% include 'components/navigation-item.html.twig' with {title: 'navigation.views'|trans, role: 'ROLE_ADMIN', route: path('app_view_list', {}|merge(view is defined ? {view: view} : {}))} %} diff --git a/templates/worker/edit.html.twig b/templates/worker/edit.html.twig new file mode 100644 index 00000000..c9e80b43 --- /dev/null +++ b/templates/worker/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block title %}Edit Worker{% endblock %} + +{% block body %} +

    Edit Worker

    + + {{ include('worker/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('worker/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/worker/index.html.twig b/templates/worker/index.html.twig new file mode 100644 index 00000000..58722f80 --- /dev/null +++ b/templates/worker/index.html.twig @@ -0,0 +1,39 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'worker.title'|trans }}{% endblock %} + +{% block content %} + + {% include 'components/page-header.html.twig' with { + 'title': 'worker.title'|trans, + } %} + +

    {{ 'worker.index_description'|trans }}

    + + + + + + + + + + + + {% for index, worker in workers %} + + + + + + + {% else %} + + + + {% endfor %} + +
    {{ 'worker.id'|trans }}{{ 'worker.email'|trans }}{{ 'worker.workload'|trans }}{{ 'worker.actions'|trans }}
    {{ worker.id }}{{ worker.email }}{{ worker.workload }} + {{ 'worker.action_edit'|trans }} +
    {{ 'worker.no_records_found'|trans }}
    +{% endblock %} From 0763f580c053187301656cf2dc22ecacd6c4c09e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:28:50 +0200 Subject: [PATCH 074/134] renamed workload report template file and made template fit workload data --- templates/reports/reports.html.twig | 4 +++- ...port.html.twig => workload_report.html.twig} | 17 ++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) rename templates/reports/{workloadReport.html.twig => workload_report.html.twig} (86%) diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index faa9b97c..46fd7e90 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -1,9 +1,11 @@ {% extends 'base.html.twig' %} + + {% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} {% block content %} -

    {{ 'reports.hour_report.title'|trans }}

    +

    {{ ('reports.' ~ mode ~ '.title') | trans() }}

    {{ form_start(form) }} {{ form_row(form.dataProvider) }} diff --git a/templates/reports/workloadReport.html.twig b/templates/reports/workload_report.html.twig similarity index 86% rename from templates/reports/workloadReport.html.twig rename to templates/reports/workload_report.html.twig index 31bf669c..3db56b33 100644 --- a/templates/reports/workloadReport.html.twig +++ b/templates/reports/workload_report.html.twig @@ -8,10 +8,11 @@ {{ 'reports.workload_report.workload'|trans }} - - {{ 'reports.workload_report.logged_hours'|trans }} - - + {% for weeks in data.yearWeeks %} + + {{ "Uge " ~ weeks }} + + {% endfor %} @@ -42,9 +43,11 @@ {{ worker.workload }} - - {{ worker.hoursLogged }} - + {% for week in worker.hoursLogged %} + + {{ week ~ "%" }} + + {% endfor %} {# {% for projectTicket in projectTag.projectTickets %} From 9614a50e99faf11a45f1199140c63ba8abe5c247 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:29:22 +0200 Subject: [PATCH 075/134] renamed mode for workload controller to reflect translations --- src/Controller/WorkloadReportController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/WorkloadReportController.php b/src/Controller/WorkloadReportController.php index 56c60e6d..3dcc4530 100644 --- a/src/Controller/WorkloadReportController.php +++ b/src/Controller/WorkloadReportController.php @@ -38,7 +38,7 @@ public function index(Request $request): Response { $reportData = null; - $mode = 'workloadReport'; + $mode = 'workload_report'; $reportFormData = new WorkloadReportFormData(); $form = $this->createForm(WorkloadReportType::class, $reportFormData, [ From 5fd0ceb429d42c14cb74b3d277a08456f82b6c62 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:30:05 +0200 Subject: [PATCH 076/134] Modified workload entities to reflect report needs --- src/Model/Reports/WorkloadReportData.php | 3 +++ src/Model/Reports/WorkloadReportWorker.php | 13 ++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Model/Reports/WorkloadReportData.php b/src/Model/Reports/WorkloadReportData.php index e05274e6..bec0af53 100644 --- a/src/Model/Reports/WorkloadReportData.php +++ b/src/Model/Reports/WorkloadReportData.php @@ -7,11 +7,14 @@ class WorkloadReportData { public readonly string $id; + /** @var ArrayCollection */ + public ArrayCollection $yearWeeks; /** @var ArrayCollection */ public ArrayCollection $workers; public function __construct() { + $this->yearWeeks = new ArrayCollection(); $this->workers = new ArrayCollection(); } } diff --git a/src/Model/Reports/WorkloadReportWorker.php b/src/Model/Reports/WorkloadReportWorker.php index a6b2c2c4..2a54ddef 100644 --- a/src/Model/Reports/WorkloadReportWorker.php +++ b/src/Model/Reports/WorkloadReportWorker.php @@ -3,23 +3,22 @@ namespace App\Model\Reports; use App\Entity\Worker; +use Doctrine\Common\Collections\ArrayCollection; class WorkloadReportWorker extends Worker { - public float $hoursLogged; + /** @var ArrayCollection */ + public arrayCollection $hoursLogged; + public function __construct() { + $this->hoursLogged = new ArrayCollection(); } - public function getHoursLogged(): float + public function getHoursLogged(): ArrayCollection { return $this->hoursLogged; } - public function setHoursLogged(float $hoursLogged): void - { - $this->hoursLogged = $hoursLogged; - } - } From 4db8f09e025a04a223c486a570bf489b903df915 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:30:55 +0200 Subject: [PATCH 077/134] Added worklog repo method to select worklogs between dates on worker --- src/Repository/WorklogRepository.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index da29d658..5731e2f6 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -165,4 +165,19 @@ public function getDistinctWorklogUsers(): array return $workers; } + + public function findWorklogsByWorkerAndStartDateRange($worker, $date_from, $date_to) + { + $qb = $this->createQueryBuilder('wor'); + + return $qb + ->where($qb->expr()->between('wor.started', ':date_from', ':date_to')) + ->andWhere('wor.worker = :worker') + ->setParameters([ + 'worker' => $worker, + 'date_from' => $date_from, + 'date_to' => $date_to, + ]) + ->getQuery()->getResult(); + } } From 7eb2dbae95be4fcbfccbbee57044c3df698960c2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:31:55 +0200 Subject: [PATCH 078/134] Added datetimehelper service to help working with dates and weeks --- src/Service/DateTimeHelper.php | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/Service/DateTimeHelper.php diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php new file mode 100644 index 00000000..a84b57a3 --- /dev/null +++ b/src/Service/DateTimeHelper.php @@ -0,0 +1,57 @@ +format('Y'); + } + + $firstDateTime = (new \DateTime())->setISODate($year, $weekNumber); + $firstDateTime->setTime(0, 0, 0); + $firstDate = $firstDateTime->format($format); + + $lastDateTime = (new \DateTime())->setISODate($year, $weekNumber, 7); + $lastDateTime->setTime(0, 0, 0); + $lastDate = $lastDateTime->format($format); + + return [$firstDate, $lastDate]; + } + /** + * Returns an array of the weeks for the current year (ISO 8601). + * + * @return array + */ + public function getWeeksOfYear(?int $year = null): array + { + if (!$year) { + $year = (int) (new \DateTime())->format('Y'); + } + $weekArray = []; + $start = new \DateTime("{$year}-01-04"); // 4th of Jan always falls in the first week of the year. + $end = (new \DateTime("{$year}-12-28"))->modify('+1 week'); // 28th of Dec always falls in the last week of the year. + $interval = new \DateInterval('P1W'); + + foreach (new \DatePeriod($start, $interval, $end) as $date) { + $yearOfTheWeek = $date->format('o'); // Year of week. + + // If the "year of the week" is greater than the current year, skip this iteration. + if ($yearOfTheWeek > $year) { + continue; + } + + $weekNumber = (int) $date->format('W'); + $weekArray[] = $weekNumber; + } + + return $weekArray; + } +} From f447d6b8bf56cf12843530c219fbe40c8fccc49d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:32:25 +0200 Subject: [PATCH 079/134] modified workload report service to get data split into weeks --- src/Service/WorkloadReportService.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index d1b84d28..8d985373 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -13,29 +13,41 @@ class WorkloadReportService public function __construct( private readonly WorkerRepository $workerRepository, private readonly WorklogRepository $worklogRepository, + private readonly DateTimeHelper $dateTimeHelper, ) { } /** * @throws EconomicsException + * @throws \Exception */ public function getWorkloadReport(): WorkloadReportData { $workloadReportData = new WorkloadReportData(); $workers = $this->workerRepository->findAll(); + $weeksOfTheYear = $this->dateTimeHelper->getWeeksOfYear(); foreach ($workers as $worker) { $workloadReportWorker = new WorkloadReportWorker(); $workloadReportWorker->setEmail($worker->getUserIdentifier()); $workloadReportWorker->setWorkload($worker->getWorkload()); - $worklogs = $this->worklogRepository->findBy(['worker' => $worker->getUserIdentifier()]); - $loggedHours = 0; - foreach ($worklogs as $worklog) { - $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); + + foreach ($weeksOfTheYear as $week) { + $firstAndLastDateOfWeek = $this->dateTimeHelper->getFirstAndLastDateOfWeek($week); + $firstDay = $firstAndLastDateOfWeek[0]; + $lastDay = $firstAndLastDateOfWeek[1]; + $worklogs = $this->worklogRepository->findWorklogsByWorkerAndStartDateRange($worker->getUserIdentifier(), $firstDay, $lastDay); + $loggedHours = 0; + foreach ($worklogs as $worklog) { + $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); + } + $loggedPercentage = round(($loggedHours / $worker->getWorkload()) * 100); + $workloadReportData->yearWeeks->add($week); + $workloadReportWorker->hoursLogged->set($week, $loggedPercentage); + } - $workloadReportWorker->setHoursLogged($loggedHours); $workloadReportData->workers->add($workloadReportWorker); } From d1e0062c22fff74a22e9d39fb0de408874bc7288 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 4 Jun 2024 15:32:41 +0200 Subject: [PATCH 080/134] Added translations for workload report --- translations/messages.da.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index b5033cc8..db817361 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -563,6 +563,9 @@ reports: select_an_option: 'Vælg en mulighed' workload_report: title: 'Workloadrapport' + select_data_provider: 'Vælg datakilde' + workload: 'Normtid' + logged_hours: 'Loggede timer' view: list_title: 'Views' @@ -713,3 +716,7 @@ issue: description: "Beskrivelse" total: "I alt" list_no_records_found: "Ingen produkter" + +worker: + title: 'Medarbejdere' + index_description: 'På denne side kan synkroniserede medarbejderes norm tid defineres' From dff7aacfc0ea79483c4b31890245db1ac3923d95 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 09:08:12 +0200 Subject: [PATCH 081/134] Adding week and month view, and taking a more generalized approach to fetching data --- src/Controller/WorkloadReportController.php | 19 +++++- src/Repository/WorklogRepository.php | 2 +- src/Service/DateTimeHelper.php | 43 ++++++++++++-- src/Service/WorkloadReportService.php | 65 ++++++++++++++++----- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/src/Controller/WorkloadReportController.php b/src/Controller/WorkloadReportController.php index 3dcc4530..3b1eff08 100644 --- a/src/Controller/WorkloadReportController.php +++ b/src/Controller/WorkloadReportController.php @@ -9,10 +9,11 @@ use App\Model\Reports\WorkloadReportFormData; use App\Repository\DataProviderRepository; use App\Service\DataProviderService; -use App\Service\WorkloadReportService; use App\Service\ViewService; +use App\Service\WorkloadReportService; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -64,6 +65,18 @@ public function index(Request $request): Response 'choices' => $this->dataProviderRepository->findAll(), ]); + $form->add('viewMode', ChoiceType::class, [ + 'required' => false, + 'label' => 'reports.workload_report.select_viewmode', + 'label_attr' => ['class' => 'label'], + 'placeholder' => false, + 'attr' => [ + 'onchange' => 'this.form.submit()', + 'class' => 'form-element', + ], + 'choices' => $this->workloadReportService->getViewModes(), + ]); + $form->handleRequest($request); $requestData = $request->query->all('workload_report'); @@ -77,11 +90,11 @@ public function index(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider; + $viewMode = $form->get('viewMode')->getData() ?? 'week'; if ($selectedDataProvider) { - $reportData = $this->workloadReportService->getWorkloadReport(); + $reportData = $this->workloadReportService->getWorkloadReport($viewMode); } - } } diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index 5731e2f6..e702ae3b 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -166,7 +166,7 @@ public function getDistinctWorklogUsers(): array return $workers; } - public function findWorklogsByWorkerAndStartDateRange($worker, $date_from, $date_to) + public function findWorklogsByWorkerAndDateRange($worker, $date_from, $date_to) { $qb = $this->createQueryBuilder('wor'); diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index a84b57a3..54f8088d 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -8,8 +8,7 @@ public function __construct( ) { } - - public function getFirstAndLastDateOfWeek(int $weekNumber, ?int $year = null, ?string $format = 'Y-m-d H:i:s'): array + public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, ?string $format = 'Y-m-d H:i:s'): array { if (!$year) { $year = (int) (new \DateTime())->format('Y'); @@ -20,17 +19,35 @@ public function getFirstAndLastDateOfWeek(int $weekNumber, ?int $year = null, ?s $firstDate = $firstDateTime->format($format); $lastDateTime = (new \DateTime())->setISODate($year, $weekNumber, 7); - $lastDateTime->setTime(0, 0, 0); + $lastDateTime->setTime(23, 59, 59); + $lastDate = $lastDateTime->format($format); + + return ['first' => $firstDate, 'last' => $lastDate]; + } + + public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, ?string $format = 'Y-m-d H:i:s'): array + { + if (!$year) { + $year = (int) (new \DateTime())->format('Y'); + } + + $firstDateTime = (new \DateTime())->setDate($year, $monthNumber, 1); + $firstDateTime->setTime(0, 0, 0); + $firstDate = $firstDateTime->format($format); + + $lastDateTime = (new \DateTime())->setDate($year, $monthNumber, 1)->modify('last day of this month'); + $lastDateTime->setTime(23, 59, 59); $lastDate = $lastDateTime->format($format); - return [$firstDate, $lastDate]; + return ['first' => $firstDate, 'last' => $lastDate]; } + /** * Returns an array of the weeks for the current year (ISO 8601). * * @return array */ - public function getWeeksOfYear(?int $year = null): array + public function getWeeksOfYear(int $year = null): array { if (!$year) { $year = (int) (new \DateTime())->format('Y'); @@ -54,4 +71,20 @@ public function getWeeksOfYear(?int $year = null): array return $weekArray; } + + public function getMonthsOfYear(): array + { + $months = []; + for ($i = 1; $i <= 12; ++$i) { + $monthName = \DateTime::createFromFormat('!m', $i)->format('F'); + $months[$monthName] = $i; + } + + return $months; + } + + public function getMonthName(int $monthNumber): string + { + return \DateTime::createFromFormat('!m', $monthNumber)->format('F'); + } } diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 8d985373..2e615486 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -2,7 +2,6 @@ namespace App\Service; -use App\Exception\EconomicsException; use App\Model\Reports\WorkloadReportData; use App\Model\Reports\WorkloadReportWorker; use App\Repository\WorkerRepository; @@ -18,34 +17,66 @@ public function __construct( } /** - * @throws EconomicsException * @throws \Exception */ - public function getWorkloadReport(): WorkloadReportData + public function getWorkloadReport($viewMode): WorkloadReportData { - $workloadReportData = new WorkloadReportData(); + // Get period based on viewmode. + $periods = match ($viewMode) { + 'month' => $this->dateTimeHelper->getMonthsOfYear(), + 'week' => $this->dateTimeHelper->getWeeksOfYear(), + }; + // Callable to get first and last date of a given period. + $getDatesOfPeriod = match ($viewMode) { + 'month' => function ($monthNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfMonth($monthNumber); }, + 'week' => function ($weekNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfWeek($weekNumber); }, + }; + + // Callable to get a readable representation of a given period. + $getReadablePeriod = match ($viewMode) { + 'month' => fn ($monthNumber) => $this->dateTimeHelper->getMonthName($monthNumber), + 'week' => fn ($period) => $period, + }; + + return $this->getWorkloadData($periods, $getDatesOfPeriod, $getReadablePeriod, $viewMode); + } + + private function getWorkloadData(array $periods, callable $getDatesOfPeriod, callable $getReadablePeriod, string $viewMode): WorkloadReportData + { + $workloadReportData = new WorkloadReportData($viewMode); $workers = $this->workerRepository->findAll(); - $weeksOfTheYear = $this->dateTimeHelper->getWeeksOfYear(); foreach ($workers as $worker) { $workloadReportWorker = new WorkloadReportWorker(); $workloadReportWorker->setEmail($worker->getUserIdentifier()); $workloadReportWorker->setWorkload($worker->getWorkload()); - foreach ($weeksOfTheYear as $week) { - $firstAndLastDateOfWeek = $this->dateTimeHelper->getFirstAndLastDateOfWeek($week); - $firstDay = $firstAndLastDateOfWeek[0]; - $lastDay = $firstAndLastDateOfWeek[1]; - $worklogs = $this->worklogRepository->findWorklogsByWorkerAndStartDateRange($worker->getUserIdentifier(), $firstDay, $lastDay); + foreach ($periods as $period) { + // Get first and last date in period. + $firstAndLastDate = $getDatesOfPeriod($period); + + // Get all worklogs between the two dates. + $worklogs = $this->worklogRepository->findWorklogsByWorkerAndDateRange($worker->getUserIdentifier(), $firstAndLastDate['first'], $firstAndLastDate['last']); + + // Tally up logged hours in gathered worklogs for current period. $loggedHours = 0; foreach ($worklogs as $worklog) { $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); } - $loggedPercentage = round(($loggedHours / $worker->getWorkload()) * 100); - $workloadReportData->yearWeeks->add($week); - $workloadReportWorker->hoursLogged->set($week, $loggedPercentage); + // Add period number to general data for table headers. + $readablePeriod = $getReadablePeriod($period); + $workloadReportData->period->add($readablePeriod); + + // Workload is per week, so for a month, it has to be times 4. + $periodWorkload = ($viewMode == 'month') ? $worker->getWorkload() * 4 : $worker->getWorkload(); + + // Get percentage of logged hours based on worker workload. + $loggedPercentage = round(($loggedHours / $periodWorkload) * 100); + + // Add percentage result to worker for current period. + $workloadReportWorker->loggedPercentage->set($period, $loggedPercentage); } $workloadReportData->workers->add($workloadReportWorker); @@ -53,4 +84,12 @@ public function getWorkloadReport(): WorkloadReportData return $workloadReportData; } + + public function getViewModes(): array + { + return [ + 'Week' => 'week', + 'Month' => 'month', + ]; + } } From 2934d6a82508cc9d8fda2ffb1b9c18d37e4cbd43 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 09:08:49 +0200 Subject: [PATCH 082/134] Modified workload models to fit needs and added additional translations --- src/Model/Reports/WorkloadReportData.php | 10 ++++++---- src/Model/Reports/WorkloadReportFormData.php | 1 + src/Model/Reports/WorkloadReportWorker.php | 9 +++------ translations/messages.da.yaml | 2 ++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Model/Reports/WorkloadReportData.php b/src/Model/Reports/WorkloadReportData.php index bec0af53..61827f20 100644 --- a/src/Model/Reports/WorkloadReportData.php +++ b/src/Model/Reports/WorkloadReportData.php @@ -7,14 +7,16 @@ class WorkloadReportData { public readonly string $id; - /** @var ArrayCollection */ - public ArrayCollection $yearWeeks; + public readonly string $viewmode; + /** @var ArrayCollection */ + public ArrayCollection $period; /** @var ArrayCollection */ public ArrayCollection $workers; - public function __construct() + public function __construct(string $viewmode) { - $this->yearWeeks = new ArrayCollection(); + $this->viewmode = $viewmode; + $this->period = new ArrayCollection(); $this->workers = new ArrayCollection(); } } diff --git a/src/Model/Reports/WorkloadReportFormData.php b/src/Model/Reports/WorkloadReportFormData.php index 70ca779f..2ee7ee1c 100644 --- a/src/Model/Reports/WorkloadReportFormData.php +++ b/src/Model/Reports/WorkloadReportFormData.php @@ -7,4 +7,5 @@ class WorkloadReportFormData { public DataProvider $dataProvider; + public string $viewMode; } diff --git a/src/Model/Reports/WorkloadReportWorker.php b/src/Model/Reports/WorkloadReportWorker.php index 2a54ddef..d61c32fc 100644 --- a/src/Model/Reports/WorkloadReportWorker.php +++ b/src/Model/Reports/WorkloadReportWorker.php @@ -8,17 +8,14 @@ class WorkloadReportWorker extends Worker { /** @var ArrayCollection */ - public arrayCollection $hoursLogged; + public arrayCollection $loggedPercentage; public function __construct() { - $this->hoursLogged = new ArrayCollection(); + parent::__construct(); + $this->loggedPercentage = new ArrayCollection(); } - public function getHoursLogged(): ArrayCollection - { - return $this->hoursLogged; - } } diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index db817361..8328fe11 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -566,6 +566,8 @@ reports: select_data_provider: 'Vælg datakilde' workload: 'Normtid' logged_hours: 'Loggede timer' + select_viewmode: 'Vælg visningstype' + week: 'Uge' view: list_title: 'Views' From 01d4d844ad73411b919ed499d81c4df59a590c9c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 09:09:22 +0200 Subject: [PATCH 083/134] Added viewmode to workload report view --- src/Form/WorkloadReportType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Form/WorkloadReportType.php b/src/Form/WorkloadReportType.php index d883e9cc..02e812b6 100644 --- a/src/Form/WorkloadReportType.php +++ b/src/Form/WorkloadReportType.php @@ -15,6 +15,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('dataProvider') + ->add('viewMode') ; } From 3cd144f16b5a2fa9e838f8624d6da1a59ba7ed14 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 09:10:03 +0200 Subject: [PATCH 084/134] Modified workload report twig to fit more generalized approach --- templates/reports/workload_report.html.twig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/reports/workload_report.html.twig b/templates/reports/workload_report.html.twig index 3db56b33..2734f0ff 100644 --- a/templates/reports/workload_report.html.twig +++ b/templates/reports/workload_report.html.twig @@ -8,9 +8,13 @@ {{ 'reports.workload_report.workload'|trans }} - {% for weeks in data.yearWeeks %} + {% for period in data.period %} - {{ "Uge " ~ weeks }} + {% if data.viewmode == 'week' %} + {{ 'reports.workload_report.week'|trans }} {{ period }} + {% else %} + {{ period }} + {% endif %} {% endfor %} @@ -43,7 +47,7 @@ {{ worker.workload }} - {% for week in worker.hoursLogged %} + {% for week in worker.loggedPercentage %} {{ week ~ "%" }} From 3da18388f0db47e4c50f0d1de44be60ca6953e0d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 09:30:42 +0200 Subject: [PATCH 085/134] Moved calculation of logged hours percentage into seperate function for readability --- src/Service/WorkloadReportService.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 2e615486..e112f881 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -65,18 +65,15 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); } - // Add period number to general data for table headers. + // Get period specific readable period representation for table headers. $readablePeriod = $getReadablePeriod($period); $workloadReportData->period->add($readablePeriod); - // Workload is per week, so for a month, it has to be times 4. - $periodWorkload = ($viewMode == 'month') ? $worker->getWorkload() * 4 : $worker->getWorkload(); - - // Get percentage of logged hours based on worker workload. - $loggedPercentage = round(($loggedHours / $periodWorkload) * 100); + // Get total logged percentage based on weekly workload. + $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); // Add percentage result to worker for current period. - $workloadReportWorker->loggedPercentage->set($period, $loggedPercentage); + $workloadReportWorker->loggedPercentage->set($period, $roundedLoggedPercentage); } $workloadReportData->workers->add($workloadReportWorker); @@ -85,6 +82,15 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal return $workloadReportData; } + private function getRoundedLoggedPercentage(float $loggedHours, float $workloadWeekBase, string $viewMode): float + { + // Workload is weekly hours, so for expanded views, it has to be multiplied. + return match ($viewMode) { + 'week' => round(($loggedHours / $workloadWeekBase)*100), + 'month' => round(($loggedHours / ($workloadWeekBase*4))*100) + }; + } + public function getViewModes(): array { return [ From c3478ca0fab3b0a9526b89b77b1546b72095a225 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 10:30:47 +0200 Subject: [PATCH 086/134] coding standards --- src/Controller/WorkloadReportController.php | 10 +++++++--- src/Entity/Worker.php | 4 ++-- src/Service/DateTimeHelper.php | 11 ++++++----- src/Service/WorkloadReportService.php | 6 +++--- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Controller/WorkloadReportController.php b/src/Controller/WorkloadReportController.php index 3b1eff08..b43cc14d 100644 --- a/src/Controller/WorkloadReportController.php +++ b/src/Controller/WorkloadReportController.php @@ -38,7 +38,7 @@ public function __construct( public function index(Request $request): Response { $reportData = null; - + $error = null; $mode = 'workload_report'; $reportFormData = new WorkloadReportFormData(); @@ -93,7 +93,11 @@ public function index(Request $request): Response $viewMode = $form->get('viewMode')->getData() ?? 'week'; if ($selectedDataProvider) { - $reportData = $this->workloadReportService->getWorkloadReport($viewMode); + try { + $reportData = $this->workloadReportService->getWorkloadReport($viewMode); + } catch (\Exception $e) { + $error = $e->getMessage(); + } } } } @@ -101,7 +105,7 @@ public function index(Request $request): Response return $this->render('reports/reports.html.twig', $this->viewService->addView([ 'controller_name' => 'WorkloadReportController', 'form' => $form, - 'error' => $error ?? null, + 'error' => $error, 'data' => $reportData, 'mode' => $mode, ])); diff --git a/src/Entity/Worker.php b/src/Entity/Worker.php index 3229699b..e8afd114 100644 --- a/src/Entity/Worker.php +++ b/src/Entity/Worker.php @@ -41,12 +41,12 @@ public function setEmail(string $email): self return $this; } - public function getWorkload(): ?string + public function getWorkload(): ?float { return $this->workload; } - public function setWorkload(string $workload): self + public function setWorkload(?float $workload): self { $this->workload = $workload; diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 54f8088d..632765a6 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -8,7 +8,7 @@ public function __construct( ) { } - public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, ?string $format = 'Y-m-d H:i:s'): array + public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { if (!$year) { $year = (int) (new \DateTime())->format('Y'); @@ -25,7 +25,7 @@ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, ?st return ['first' => $firstDate, 'last' => $lastDate]; } - public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, ?string $format = 'Y-m-d H:i:s'): array + public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { if (!$year) { $year = (int) (new \DateTime())->format('Y'); @@ -58,7 +58,8 @@ public function getWeeksOfYear(int $year = null): array $interval = new \DateInterval('P1W'); foreach (new \DatePeriod($start, $interval, $end) as $date) { - $yearOfTheWeek = $date->format('o'); // Year of week. + // Year of week. + $yearOfTheWeek = $date->format('o'); // If the "year of the week" is greater than the current year, skip this iteration. if ($yearOfTheWeek > $year) { @@ -76,7 +77,7 @@ public function getMonthsOfYear(): array { $months = []; for ($i = 1; $i <= 12; ++$i) { - $monthName = \DateTime::createFromFormat('!m', $i)->format('F'); + $monthName = \DateTime::createFromFormat('!m', (string) $i)->format('F'); $months[$monthName] = $i; } @@ -85,6 +86,6 @@ public function getMonthsOfYear(): array public function getMonthName(int $monthNumber): string { - return \DateTime::createFromFormat('!m', $monthNumber)->format('F'); + return \DateTime::createFromFormat('!m', (string) $monthNumber)->format('F'); } } diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index e112f881..a80535bf 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -19,7 +19,7 @@ public function __construct( /** * @throws \Exception */ - public function getWorkloadReport($viewMode): WorkloadReportData + public function getWorkloadReport($viewMode = 'week'): WorkloadReportData { // Get period based on viewmode. $periods = match ($viewMode) { @@ -86,8 +86,8 @@ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadW { // Workload is weekly hours, so for expanded views, it has to be multiplied. return match ($viewMode) { - 'week' => round(($loggedHours / $workloadWeekBase)*100), - 'month' => round(($loggedHours / ($workloadWeekBase*4))*100) + 'week' => round(($loggedHours / $workloadWeekBase) * 100), + 'month' => round(($loggedHours / ($workloadWeekBase * 4)) * 100) }; } From a6ba3f990add86cb032bc2e47790b66b72e8004f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 10:33:57 +0200 Subject: [PATCH 087/134] coding standards --- src/Form/WorkloadReportType.php | 2 -- src/Model/Reports/WorkloadReportWorker.php | 3 --- src/Repository/WorkerRepository.php | 1 - templates/reports/reports.html.twig | 4 +--- templates/reports/workload_report.html.twig | 2 +- 5 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Form/WorkloadReportType.php b/src/Form/WorkloadReportType.php index 02e812b6..379437e2 100644 --- a/src/Form/WorkloadReportType.php +++ b/src/Form/WorkloadReportType.php @@ -2,10 +2,8 @@ namespace App\Form; -use App\Model\Reports\HourReportFormData; use App\Model\Reports\WorkloadReportFormData; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; diff --git a/src/Model/Reports/WorkloadReportWorker.php b/src/Model/Reports/WorkloadReportWorker.php index d61c32fc..9572326a 100644 --- a/src/Model/Reports/WorkloadReportWorker.php +++ b/src/Model/Reports/WorkloadReportWorker.php @@ -10,12 +10,9 @@ class WorkloadReportWorker extends Worker /** @var ArrayCollection */ public arrayCollection $loggedPercentage; - public function __construct() { parent::__construct(); $this->loggedPercentage = new ArrayCollection(); } - - } diff --git a/src/Repository/WorkerRepository.php b/src/Repository/WorkerRepository.php index 5a7dcfa4..ef6a7d34 100644 --- a/src/Repository/WorkerRepository.php +++ b/src/Repository/WorkerRepository.php @@ -2,7 +2,6 @@ namespace App\Repository; -use App\Entity\User; use App\Entity\Worker; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index 46fd7e90..f5fa9996 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -1,11 +1,9 @@ {% extends 'base.html.twig' %} - - {% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} {% block content %} -

    {{ ('reports.' ~ mode ~ '.title') | trans() }}

    +

    {{ ('reports.' ~ mode ~ '.title')|trans() }}

    {{ form_start(form) }} {{ form_row(form.dataProvider) }} diff --git a/templates/reports/workload_report.html.twig b/templates/reports/workload_report.html.twig index 2734f0ff..109cfc9f 100644 --- a/templates/reports/workload_report.html.twig +++ b/templates/reports/workload_report.html.twig @@ -67,7 +67,7 @@ {{ projectTicket.totalSpent }} {% endfor %} - #} + #} {% endfor %} From 26fafc48bcecc08aa6a63b24453b93ad5d37167a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 10:34:49 +0200 Subject: [PATCH 088/134] updated changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d38fc5..52bebc57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * NOTE: APP_DEFAULT_PLANNING_DATA_PROVIDER has been changed to APP_DEFAULT_DATA_PROVIDER. This has to be changed when releasing. * [PR-117](https://github.com/itk-dev/economics/pull/117) 1211: Added hour report - +* [PR-124](https://github.com/itk-dev/economics/pull/124) + 710: Workload report + ## [2.3.0] - 2024-06-03 * [PR-126](https://github.com/itk-dev/economics/pull/126) @@ -263,8 +265,7 @@ complete process. * Updated to authorization code flow. * Changed worklog save button styling to be sticky. -[Unreleased]: https://github.com/itk-dev/economics/compare/2.3.0...HEAD -[2.3.0]: https://github.com/itk-dev/economics/compare/2.2.0...2.3.0 +[Unreleased]: https://github.com/itk-dev/economics/compare/2.2.0...HEAD [2.2.0]: https://github.com/itk-dev/economics/compare/2.1.2...2.2.0 [2.1.2]: https://github.com/itk-dev/economics/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/itk-dev/economics/compare/2.1.0...2.1.1 From fb4d21ce665fbd5762f9f21fc569ee271d717e9b Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 10:57:02 +0200 Subject: [PATCH 089/134] added migration for worker and added tags and planhours to issue --- migrations/Version20240606085631.php | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 migrations/Version20240606085631.php diff --git a/migrations/Version20240606085631.php b/migrations/Version20240606085631.php new file mode 100644 index 00000000..81584aba --- /dev/null +++ b/migrations/Version20240606085631.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE worker'); + $this->addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); + } +} From be333f80bde9cdbb7b0e5d671b0611ec97a410e2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 11:20:36 +0200 Subject: [PATCH 090/134] Modified fixtures to randomize started datetime of worklogs within the current year --- src/DataFixtures/AppFixtures.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index fc496556..6f743354 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -121,7 +121,7 @@ public function load(ObjectManager $manager): void $worklog->setProject($project); $worklog->setWorker('test@test'); $worklog->setTimeSpentSeconds(60 * 15 * ($k + 1)); - $worklog->setStarted(new \DateTime()); + $worklog->setStarted(\DateTime::createFromFormat('U', rand(strtotime(date('Y-01-01')), strtotime(date('Y-12-31'))))); $worklog->setIssue($issue); $worklog->setDataProvider($dataProvider); From aa9f704f198661241b579942a0aff67340e87758 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 11:21:04 +0200 Subject: [PATCH 091/134] Improved the state of worker crud --- src/Form/WorkerType.php | 19 +++++++++++++++++-- templates/worker/edit.html.twig | 19 ++++++++++++------- translations/messages.da.yaml | 9 +++++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/Form/WorkerType.php b/src/Form/WorkerType.php index f27a73c1..e2f21950 100644 --- a/src/Form/WorkerType.php +++ b/src/Form/WorkerType.php @@ -4,6 +4,7 @@ use App\Entity\Worker; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -12,8 +13,22 @@ class WorkerType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('email') - ->add('workload') + ->add('email', TextType::class, [ + 'label' => 'worker.email', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'required' => false, + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('workload', TextType::class, [ + 'label' => 'worker.workload', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'required' => false, + 'row_attr' => ['class' => 'form-row'], + ]) ; } diff --git a/templates/worker/edit.html.twig b/templates/worker/edit.html.twig index c9e80b43..4d443709 100644 --- a/templates/worker/edit.html.twig +++ b/templates/worker/edit.html.twig @@ -1,13 +1,18 @@ {% extends 'base.html.twig' %} -{% block title %}Edit Worker{% endblock %} +{% block title %}{{ 'worker.edit'|trans }}{% endblock %} -{% block body %} -

    Edit Worker

    +{% block content %} +

    {{ 'worker.edit'|trans }}

    - {{ include('worker/_form.html.twig', {'button_label': 'Update'}) }} + {{ form_start(form) }} +
    + {{ form_rest(form) }} +
    + + {{ form_end(form) }} - back to list - - {{ include('worker/_delete_form.html.twig') }} + {% endblock %} diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 8328fe11..66da7907 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -59,6 +59,7 @@ navigation: planning_projects: 'Projekter' hour_report: "Hent timerapport" workload_report: "Hent workloadrapport" + worker: "Medarbejdere" planning: title: "Planlægning" @@ -722,3 +723,11 @@ issue: worker: title: 'Medarbejdere' index_description: 'På denne side kan synkroniserede medarbejderes norm tid defineres' + id: 'Id' + email: 'Email' + workload: 'Normtid' + actions: 'Handlinger' + action_edit: 'Rediger' + edit: 'Rediger medarbejder' + action_save: 'Gem' + back_to_list: "Tilbage til listen" From 985e89d6ca60c7828e18fd8d57d0568e842f6cec Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 12:35:11 +0200 Subject: [PATCH 092/134] Added preselected default service provider --- src/Controller/WorkloadReportController.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Controller/WorkloadReportController.php b/src/Controller/WorkloadReportController.php index b43cc14d..9bb92457 100644 --- a/src/Controller/WorkloadReportController.php +++ b/src/Controller/WorkloadReportController.php @@ -42,6 +42,13 @@ public function index(Request $request): Response $mode = 'workload_report'; $reportFormData = new WorkloadReportFormData(); + $dataProviders = $this->dataProviderRepository->findAll(); + $defaultProvider = $this->dataProviderRepository->find($this->defaultDataProvider); + + if (null === $defaultProvider && count($dataProviders) > 0) { + $defaultProvider = $dataProviders[0]; + } + $form = $this->createForm(WorkloadReportType::class, $reportFormData, [ 'action' => $this->generateUrl('app_workload_report', $this->viewService->addView([])), 'method' => 'GET', @@ -60,9 +67,9 @@ public function index(Request $request): Response 'attr' => [ 'onchange' => 'this.form.submit()', 'class' => 'form-element', - 'data-preselect' => $this->defaultDataProvider ?? '', ], - 'choices' => $this->dataProviderRepository->findAll(), + 'data' => $this->dataProviderRepository->find($this->defaultDataProvider), + 'choices' => $dataProviders, ]); $form->add('viewMode', ChoiceType::class, [ @@ -99,6 +106,13 @@ public function index(Request $request): Response $error = $e->getMessage(); } } + } elseif (null !== $defaultProvider) { + $viewMode = $form->get('viewMode')->getData() ?? 'week'; + try { + $reportData = $this->workloadReportService->getWorkloadReport($viewMode); + } catch (\Exception $e) { + $error = $e->getMessage(); + } } } From 82950ef33973ac60345ce365611eb8a11815ed84 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 12:35:29 +0200 Subject: [PATCH 093/134] taking lunch break into consideration --- src/Service/WorkloadReportService.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index a80535bf..6abf33f5 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -84,10 +84,13 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal private function getRoundedLoggedPercentage(float $loggedHours, float $workloadWeekBase, string $viewMode): float { + // Since lunch is paid, subtract this from the actual workload (0.5 * 5) + $actualWeeklyWorkload = $workloadWeekBase - 2.5; + // Workload is weekly hours, so for expanded views, it has to be multiplied. return match ($viewMode) { - 'week' => round(($loggedHours / $workloadWeekBase) * 100), - 'month' => round(($loggedHours / ($workloadWeekBase * 4)) * 100) + 'week' => round(($loggedHours / $actualWeeklyWorkload) * 100), + 'month' => round(($loggedHours / ($actualWeeklyWorkload * 4)) * 100) }; } From c3b476fde755518fcf5704f9af6698562af8f532 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 12:35:56 +0200 Subject: [PATCH 094/134] Clarifying workload is defined all inclusive --- translations/messages.da.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 66da7907..ed0b6305 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -725,7 +725,7 @@ worker: index_description: 'På denne side kan synkroniserede medarbejderes norm tid defineres' id: 'Id' email: 'Email' - workload: 'Normtid' + workload: 'Normtid (inkl. middagspause)' actions: 'Handlinger' action_edit: 'Rediger' edit: 'Rediger medarbejder' From a6c8a3eeca360308c1cc046e013f9ccba42cb1c9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 13:37:06 +0200 Subject: [PATCH 095/134] Added multiple workers, and split worklogs between them randomly --- src/DataFixtures/AppFixtures.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 6f743354..117c27e7 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -39,10 +39,16 @@ public function load(ObjectManager $manager): void $dataProviders[] = $dataProvider2; - $worker = new Worker(); - $worker->setEmail('test@test'); - $worker->setWorkload('34.5'); - $manager->persist($worker); + $workerArray = []; + + for ($i = 0; $i < 10; ++$i) { + $worker = new Worker(); + $worker->setEmail('test'.$i.'@test'); + $worker->setWorkload(37); + $manager->persist($worker); + $workerArray[] = 'test'.$i.'@test'; + } + foreach ($dataProviders as $key => $dataProvider) { $manager->persist($dataProvider); @@ -119,7 +125,7 @@ public function load(ObjectManager $manager): void $worklog->setDescription("Beskrivelse af worklog-$key-$i-$j-$k"); $worklog->setIsBilled(false); $worklog->setProject($project); - $worklog->setWorker('test@test'); + $worklog->setWorker($workerArray[(string) rand(0, 9)]); $worklog->setTimeSpentSeconds(60 * 15 * ($k + 1)); $worklog->setStarted(\DateTime::createFromFormat('U', rand(strtotime(date('Y-01-01')), strtotime(date('Y-12-31'))))); $worklog->setIssue($issue); From 62939752a1e1b3c8fecbba58a402f086954ebbe4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 13:39:18 +0200 Subject: [PATCH 096/134] Added trace of wrongly added hours_remaining on issue entity --- migrations/Version20240606085631.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/Version20240606085631.php b/migrations/Version20240606085631.php index 81584aba..a7e39037 100644 --- a/migrations/Version20240606085631.php +++ b/migrations/Version20240606085631.php @@ -21,13 +21,13 @@ public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); + $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL'); } public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs $this->addSql('DROP TABLE worker'); - $this->addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); + $this->addSql('ALTER TABLE issue DROP tags'); } } From 9b42a92e92f3e23e2df77a4261809c4c4a6a5253 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 13:39:51 +0200 Subject: [PATCH 097/134] Removed unique requirement for workload on Worker entity --- src/Entity/Worker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Worker.php b/src/Entity/Worker.php index e8afd114..623ef059 100644 --- a/src/Entity/Worker.php +++ b/src/Entity/Worker.php @@ -17,7 +17,7 @@ class Worker #[ORM\Column(length: 180, unique: true)] private ?string $email = null; - #[ORM\Column(length: 180, unique: true)] + #[ORM\Column(length: 180)] private ?float $workload = null; public function __construct() From 6f7b549bb447b958127e27fa99dc370b6a533bdd Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 14:36:34 +0200 Subject: [PATCH 098/134] looping period for headers only once --- src/Service/WorkloadReportService.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 6abf33f5..17d870f9 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -47,6 +47,11 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $workloadReportData = new WorkloadReportData($viewMode); $workers = $this->workerRepository->findAll(); + foreach ($periods as $period) { + // Get period specific readable period representation for table headers. + $readablePeriod = $getReadablePeriod($period); + $workloadReportData->period->add($readablePeriod); + } foreach ($workers as $worker) { $workloadReportWorker = new WorkloadReportWorker(); $workloadReportWorker->setEmail($worker->getUserIdentifier()); @@ -65,10 +70,6 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); } - // Get period specific readable period representation for table headers. - $readablePeriod = $getReadablePeriod($period); - $workloadReportData->period->add($readablePeriod); - // Get total logged percentage based on weekly workload. $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); @@ -86,7 +87,7 @@ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadW { // Since lunch is paid, subtract this from the actual workload (0.5 * 5) $actualWeeklyWorkload = $workloadWeekBase - 2.5; - + // Workload is weekly hours, so for expanded views, it has to be multiplied. return match ($viewMode) { 'week' => round(($loggedHours / $actualWeeklyWorkload) * 100), From f232a2410cfdc2f5c392197e0e57999af7e9d7ed Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Thu, 6 Jun 2024 14:37:27 +0200 Subject: [PATCH 099/134] Cleaned up in workload report template --- templates/reports/workload_report.html.twig | 25 ++------------------- translations/messages.da.yaml | 2 +- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/templates/reports/workload_report.html.twig b/templates/reports/workload_report.html.twig index 109cfc9f..45439524 100644 --- a/templates/reports/workload_report.html.twig +++ b/templates/reports/workload_report.html.twig @@ -4,7 +4,7 @@ class="table-auto border-separate border-spacing-0 border border-slate-600 relative" {{ stimulus_controller('planning-scroll') }}> - {{ 'planning.projects'|trans }} + {{ 'reports.workload_report.worker'|trans }} {{ 'reports.workload_report.workload'|trans }} @@ -36,12 +36,6 @@ data-toggle-target="button"> - @@ -53,22 +47,7 @@ {% endfor %} - {# - {% for projectTicket in projectTag.projectTickets %} - - -
    - - {{ projectTicket.headline }} - -
    - - {{ projectTicket.totalEstimated }} - {{ projectTicket.totalSpent }} - - {% endfor %} - #} - + {% endfor %} diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index ed0b6305..f84d716c 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -59,7 +59,6 @@ navigation: planning_projects: 'Projekter' hour_report: "Hent timerapport" workload_report: "Hent workloadrapport" - worker: "Medarbejdere" planning: title: "Planlægning" @@ -569,6 +568,7 @@ reports: logged_hours: 'Loggede timer' select_viewmode: 'Vælg visningstype' week: 'Uge' + workers: 'Medarbejdere' view: list_title: 'Views' From 67c11a6cc9fceacd9a5a29befd33cdc4e3966e58 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 09:24:52 +0200 Subject: [PATCH 100/134] Added translations --- translations/messages.da.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index f84d716c..b2f17a21 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -59,6 +59,7 @@ navigation: planning_projects: 'Projekter' hour_report: "Hent timerapport" workload_report: "Hent workloadrapport" + worker: "Medarbejdere" planning: title: "Planlægning" @@ -569,6 +570,7 @@ reports: select_viewmode: 'Vælg visningstype' week: 'Uge' workers: 'Medarbejdere' + worker: 'Medarbejder' view: list_title: 'Views' From 19884dae6991f36385ccd5d4fc7a79a42d4fa86d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 13:29:10 +0200 Subject: [PATCH 101/134] coding standards + generated documentation for service and helper method --- CHANGELOG.md | 2 +- src/Controller/WorkerController.php | 3 +- src/DataFixtures/AppFixtures.php | 6 +- src/Service/DateTimeHelper.php | 43 ++++++++++++-- src/Service/WorkloadReportService.php | 81 +++++++++++++++++++++++++-- 5 files changed, 120 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52bebc57..aa8e728f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [PR-117](https://github.com/itk-dev/economics/pull/117) 1211: Added hour report * [PR-124](https://github.com/itk-dev/economics/pull/124) - 710: Workload report + 710: Added workload report ## [2.3.0] - 2024-06-03 diff --git a/src/Controller/WorkerController.php b/src/Controller/WorkerController.php index 404d5914..e16474e7 100644 --- a/src/Controller/WorkerController.php +++ b/src/Controller/WorkerController.php @@ -71,7 +71,8 @@ public function edit(Request $request, Worker $worker, EntityManagerInterface $e #[Route('/{id}', name: 'app_worker_delete', methods: ['POST'])] public function delete(Request $request, Worker $worker, EntityManagerInterface $entityManager): Response { - if ($this->isCsrfTokenValid('delete'.$worker->getId(), $request->request->get('_token'))) { + $token = $request->request->get('_token'); + if (is_string($token) && $this->isCsrfTokenValid('delete'.$worker->getId(), $token)) { $entityManager->remove($worker); $entityManager->flush(); } diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 117c27e7..4915a34f 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -49,7 +49,6 @@ public function load(ObjectManager $manager): void $workerArray[] = 'test'.$i.'@test'; } - foreach ($dataProviders as $key => $dataProvider) { $manager->persist($dataProvider); @@ -119,15 +118,16 @@ public function load(ObjectManager $manager): void $manager->persist($issue); for ($k = 0; $k < 100; ++$k) { + $worklog = new Worklog(); $worklog->setProjectTrackerIssueId("worklog-$key-$i-$j-$k"); $worklog->setWorklogId($i * 100000 + $j * 1000 + $k); $worklog->setDescription("Beskrivelse af worklog-$key-$i-$j-$k"); $worklog->setIsBilled(false); $worklog->setProject($project); - $worklog->setWorker($workerArray[(string) rand(0, 9)]); + $worklog->setWorker($workerArray[rand(0, 9)]); $worklog->setTimeSpentSeconds(60 * 15 * ($k + 1)); - $worklog->setStarted(\DateTime::createFromFormat('U', rand(strtotime(date('Y-01-01')), strtotime(date('Y-12-31'))))); + $worklog->setStarted(\DateTime::createFromFormat('U', (string) rand(strtotime(date('Y-01-01')), strtotime(date('Y-12-31'))))); $worklog->setIssue($issue); $worklog->setDataProvider($dataProvider); diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 632765a6..d78f59a4 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -8,6 +8,17 @@ public function __construct( ) { } + /** + * Returns the first and last date of a given week in a year (ISO 8601). + * + * @param int $weekNumber The week number. + * + * @param int|null $year The year. If not provided, the current year will be used. + * + * @param string $format The date format to be returned. Defaults to 'Y-m-d H:i:s'. + * + * @return array An array with the first and last date of the week. + */ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { if (!$year) { @@ -25,6 +36,17 @@ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, str return ['first' => $firstDate, 'last' => $lastDate]; } + /** + * Returns the first and last date of the specified month and year. + * + * @param int $monthNumber The month number (1-12). + * + * @param int|null $year The year. If null, the current year will be used. + * + * @param string $format The format to use for the returned dates. + * + * @return array An array containing the first and last date of the specified month and year. + */ public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { if (!$year) { @@ -43,9 +65,11 @@ public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, s } /** - * Returns an array of the weeks for the current year (ISO 8601). + * Retrieves an array of week numbers for a given year. + * + * @param int|null $year The year for which to retrieve the week numbers. If null, the current year is used. * - * @return array + * @return array An array of week numbers. */ public function getWeeksOfYear(int $year = null): array { @@ -73,17 +97,28 @@ public function getWeeksOfYear(int $year = null): array return $weekArray; } + /** + * Retrieves an array of months and their corresponding numeric representation of a year. + * + * @return array An array where the keys are month names and the values are their corresponding numeric representation (1-12). + */ public function getMonthsOfYear(): array { $months = []; for ($i = 1; $i <= 12; ++$i) { - $monthName = \DateTime::createFromFormat('!m', (string) $i)->format('F'); - $months[$monthName] = $i; + $months[] = $i; } return $months; } + /** + * Retrieves the name of the month for a given month number. + * + * @param int $monthNumber The month number for which to retrieve the month name. + * + * @return string The name of the month. + */ public function getMonthName(int $monthNumber): string { return \DateTime::createFromFormat('!m', (string) $monthNumber)->format('F'); diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 17d870f9..1cca0f0d 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -1,11 +1,35 @@ $this->dateTimeHelper->getMonthsOfYear(), 'week' => $this->dateTimeHelper->getWeeksOfYear(), + default => throw new \Exception("Unexpected value for viewMode: $viewMode in periods match"), }; // Callable to get first and last date of a given period. $getDatesOfPeriod = match ($viewMode) { 'month' => function ($monthNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfMonth($monthNumber); }, 'week' => function ($weekNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfWeek($weekNumber); }, + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getDatesOfPeriod match"), }; // Callable to get a readable representation of a given period. $getReadablePeriod = match ($viewMode) { 'month' => fn ($monthNumber) => $this->dateTimeHelper->getMonthName($monthNumber), 'week' => fn ($period) => $period, + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), }; return $this->getWorkloadData($periods, $getDatesOfPeriod, $getReadablePeriod, $viewMode); } + /** + * Get the workload data based on the specified periods, date calculation method, readable period representation method + * and view mode. + * + * @param array $periods The list of periods + * + * @param callable $getDatesOfPeriod The callable to get the first and last date of a given period + * + * @param callable $getReadablePeriod The callable to get a readable representation of a given period + * + * @param string $viewMode The view mode + * + * @return WorkloadReportData The workload report data + * + * @throws \Exception When the calculated roundedLoggedPercentage is null + */ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, callable $getReadablePeriod, string $viewMode): WorkloadReportData { $workloadReportData = new WorkloadReportData($viewMode); @@ -73,6 +122,10 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal // Get total logged percentage based on weekly workload. $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); + if (!$roundedLoggedPercentage) { + throw new \Exception("Value of calculated roundedLoggedPercentage: $roundedLoggedPercentage cannot be null"); + } + // Add percentage result to worker for current period. $workloadReportWorker->loggedPercentage->set($period, $roundedLoggedPercentage); } @@ -83,6 +136,17 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal return $workloadReportData; } + /** + * Calculates the rounded percentage of logged hours based on the workload and view mode. + * + * @param float $loggedHours The number of logged hours. + * + * @param float $workloadWeekBase The base weekly workload (including lunch hours). + * + * @param string $viewMode The view mode ('week' or 'month'). + * + * @return float The rounded percentage of logged hours. + */ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadWeekBase, string $viewMode): float { // Since lunch is paid, subtract this from the actual workload (0.5 * 5) @@ -95,6 +159,11 @@ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadW }; } + /** + * Retrieves the available view modes. + * + * @return array The array containing the available view modes. + */ public function getViewModes(): array { return [ From 9aef803c0256ec6df2007b295f85c0417677807d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 13:36:14 +0200 Subject: [PATCH 102/134] coding standards --- src/DataFixtures/AppFixtures.php | 1 - src/Service/DateTimeHelper.php | 22 +++++++-------- src/Service/WorkloadReportService.php | 39 ++++++++++++--------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 4915a34f..002eb3c0 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -118,7 +118,6 @@ public function load(ObjectManager $manager): void $manager->persist($issue); for ($k = 0; $k < 100; ++$k) { - $worklog = new Worklog(); $worklog->setProjectTrackerIssueId("worklog-$key-$i-$j-$k"); $worklog->setWorklogId($i * 100000 + $j * 1000 + $k); diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index d78f59a4..5aaed723 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -11,13 +11,11 @@ public function __construct( /** * Returns the first and last date of a given week in a year (ISO 8601). * - * @param int $weekNumber The week number. - * + * @param int $weekNumber the week number * @param int|null $year The year. If not provided, the current year will be used. - * * @param string $format The date format to be returned. Defaults to 'Y-m-d H:i:s'. * - * @return array An array with the first and last date of the week. + * @return array an array with the first and last date of the week */ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { @@ -39,13 +37,11 @@ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year = null, str /** * Returns the first and last date of the specified month and year. * - * @param int $monthNumber The month number (1-12). - * + * @param int $monthNumber the month number (1-12) * @param int|null $year The year. If null, the current year will be used. + * @param string $format the format to use for the returned dates * - * @param string $format The format to use for the returned dates. - * - * @return array An array containing the first and last date of the specified month and year. + * @return array an array containing the first and last date of the specified month and year */ public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, string $format = 'Y-m-d H:i:s'): array { @@ -69,7 +65,7 @@ public function getFirstAndLastDateOfMonth(int $monthNumber, int $year = null, s * * @param int|null $year The year for which to retrieve the week numbers. If null, the current year is used. * - * @return array An array of week numbers. + * @return array an array of week numbers */ public function getWeeksOfYear(int $year = null): array { @@ -100,7 +96,7 @@ public function getWeeksOfYear(int $year = null): array /** * Retrieves an array of months and their corresponding numeric representation of a year. * - * @return array An array where the keys are month names and the values are their corresponding numeric representation (1-12). + * @return array an array where the keys are month names and the values are their corresponding numeric representation (1-12) */ public function getMonthsOfYear(): array { @@ -115,9 +111,9 @@ public function getMonthsOfYear(): array /** * Retrieves the name of the month for a given month number. * - * @param int $monthNumber The month number for which to retrieve the month name. + * @param int $monthNumber the month number for which to retrieve the month name * - * @return string The name of the month. + * @return string the name of the month */ public function getMonthName(int $monthNumber): string { diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 1cca0f0d..ab6baf0a 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -6,30 +6,30 @@ namespace App\Service; -use /** +use /* * Class WorkloadReportData * * This class retrieves workload data for generating reports. */ - App\Model\Reports\WorkloadReportData; -use /** +App\Model\Reports\WorkloadReportData; +use /* * @var Connection */ - App\Model\Reports\WorkloadReportWorker; -use /** +App\Model\Reports\WorkloadReportWorker; +use /* * @method Worker|null find($id, $lockMode = null, $lockVersion = null) * @method Worker|null findOneBy(array $criteria, array $orderBy = null) * @method Worker[] findAll() * @method Worker[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ - App\Repository\WorkerRepository; -use /** +App\Repository\WorkerRepository; +use /* * @method Worklog|null find($id, $lockMode = null, $lockVersion = null) * @method Worklog|null findOneBy(array $criteria, array $orderBy = null) * @method Worklog[] findAll() * @method Worklog[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ - App\Repository\WorklogRepository; +App\Repository\WorklogRepository; class WorkloadReportService { @@ -80,11 +80,8 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData * and view mode. * * @param array $periods The list of periods - * * @param callable $getDatesOfPeriod The callable to get the first and last date of a given period - * * @param callable $getReadablePeriod The callable to get a readable representation of a given period - * * @param string $viewMode The view mode * * @return WorkloadReportData The workload report data @@ -119,13 +116,13 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); } + if (!$worker->getWorkload()) { + $workerId = $worker->getUserIdentifier(); + throw new \Exception("Workload of worker: $workerId cannot be unset when generating workload report."); + } // Get total logged percentage based on weekly workload. $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); - if (!$roundedLoggedPercentage) { - throw new \Exception("Value of calculated roundedLoggedPercentage: $roundedLoggedPercentage cannot be null"); - } - // Add percentage result to worker for current period. $workloadReportWorker->loggedPercentage->set($period, $roundedLoggedPercentage); } @@ -139,13 +136,11 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal /** * Calculates the rounded percentage of logged hours based on the workload and view mode. * - * @param float $loggedHours The number of logged hours. - * - * @param float $workloadWeekBase The base weekly workload (including lunch hours). - * - * @param string $viewMode The view mode ('week' or 'month'). + * @param float $loggedHours the number of logged hours + * @param float $workloadWeekBase the base weekly workload (including lunch hours) + * @param string $viewMode the view mode ('week' or 'month') * - * @return float The rounded percentage of logged hours. + * @return float the rounded percentage of logged hours */ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadWeekBase, string $viewMode): float { @@ -162,7 +157,7 @@ private function getRoundedLoggedPercentage(float $loggedHours, float $workloadW /** * Retrieves the available view modes. * - * @return array The array containing the available view modes. + * @return array the array containing the available view modes */ public function getViewModes(): array { From 297a0004a94f0b2c15fb134398fba5d4ccb2377e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 13:38:37 +0200 Subject: [PATCH 103/134] coding standards --- src/Service/WorkloadReportService.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index ab6baf0a..e55a7dd9 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -120,9 +120,14 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $workerId = $worker->getUserIdentifier(); throw new \Exception("Workload of worker: $workerId cannot be unset when generating workload report."); } + // Get total logged percentage based on weekly workload. $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); + if (!$roundedLoggedPercentage) { + throw new \Exception("Value of calculated roundedLoggedPercentage: $roundedLoggedPercentage cannot be null"); + } + // Add percentage result to worker for current period. $workloadReportWorker->loggedPercentage->set($period, $roundedLoggedPercentage); } From f1f7ec8bcc6a8616b6e01c2dbf9a6a36ec8cf753 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 13:43:02 +0200 Subject: [PATCH 104/134] coding standards --- src/Service/WorkloadReportService.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index e55a7dd9..097abb56 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -116,13 +116,15 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $loggedHours += ($worklog->getTimeSpentSeconds() / 60 / 60); } - if (!$worker->getWorkload()) { + $workerWorkload = $worker->getWorkload(); + + if (!$workerWorkload) { $workerId = $worker->getUserIdentifier(); throw new \Exception("Workload of worker: $workerId cannot be unset when generating workload report."); } // Get total logged percentage based on weekly workload. - $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $worker->getWorkload(), $viewMode); + $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $workerWorkload, $viewMode); if (!$roundedLoggedPercentage) { throw new \Exception("Value of calculated roundedLoggedPercentage: $roundedLoggedPercentage cannot be null"); From 7338e5a4574e1cec9ea57146305e0ca03ecb372e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 13:59:15 +0200 Subject: [PATCH 105/134] Removed wrongly placed unique index from workload migration --- migrations/Version20240606085631.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20240606085631.php b/migrations/Version20240606085631.php index a7e39037..41e9d13b 100644 --- a/migrations/Version20240606085631.php +++ b/migrations/Version20240606085631.php @@ -20,7 +20,7 @@ public function getDescription(): string public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL'); } From a2d3e274f44541322a86a36245edb9a8d19a1c81 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 14:20:02 +0200 Subject: [PATCH 106/134] re-added hours_remaining that was wrongly removed --- migrations/Version20240606085631.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/migrations/Version20240606085631.php b/migrations/Version20240606085631.php index 41e9d13b..19a55068 100644 --- a/migrations/Version20240606085631.php +++ b/migrations/Version20240606085631.php @@ -20,7 +20,8 @@ public function getDescription(): string public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL'); } @@ -28,6 +29,7 @@ public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs $this->addSql('DROP TABLE worker'); + $this->addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); $this->addSql('ALTER TABLE issue DROP tags'); } } From f07de147a4857f31b9c10c5e94fc27742a40dd18 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 14:26:01 +0200 Subject: [PATCH 107/134] Messed up some migrations --- migrations/Version20240606085631.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/migrations/Version20240606085631.php b/migrations/Version20240606085631.php index 19a55068..81584aba 100644 --- a/migrations/Version20240606085631.php +++ b/migrations/Version20240606085631.php @@ -22,7 +22,6 @@ public function up(Schema $schema): void // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); - $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL'); } public function down(Schema $schema): void @@ -30,6 +29,5 @@ public function down(Schema $schema): void // this down() migration is auto-generated, please modify it to your needs $this->addSql('DROP TABLE worker'); $this->addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); - $this->addSql('ALTER TABLE issue DROP tags'); } } From c2226951eb3c86bf12fb49d1e375824010388e96 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 14:52:28 +0200 Subject: [PATCH 108/134] Added new migration to properly fix earlier migration changes --- migrations/Version20240607125158.php | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 migrations/Version20240607125158.php diff --git a/migrations/Version20240607125158.php b/migrations/Version20240607125158.php new file mode 100644 index 00000000..83087a09 --- /dev/null +++ b/migrations/Version20240607125158.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); + $this->addSql('DROP INDEX UNIQ_9FB2BF621203AA7B ON worker'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9FB2BF621203AA7B ON worker (workload)'); + } +} From 755b4574ea991cb7bdfe4cc675ec9f37c94765c5 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 15:00:15 +0200 Subject: [PATCH 109/134] Added a proposition of how to sync workers via worklog sync --- src/Service/DataSynchronizationService.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index 6b19dcf9..d3c43430 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -9,6 +9,7 @@ use App\Entity\Issue; use App\Entity\Project; use App\Entity\Version; +use App\Entity\Worker; use App\Entity\Worklog; use App\Exception\EconomicsException; use App\Exception\UnsupportedDataProviderException; @@ -20,6 +21,7 @@ use App\Repository\IssueRepository; use App\Repository\ProjectRepository; use App\Repository\VersionRepository; +use App\Repository\WorkerRepository; use App\Repository\WorklogRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -41,6 +43,7 @@ public function __construct( private readonly InvoiceRepository $invoiceRepository, private readonly DataProviderService $dataProviderService, private readonly DataProviderRepository $dataProviderRepository, + private readonly WorkerRepository $workerRepository, ) { } @@ -362,6 +365,14 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $this->entityManager->flush(); $this->entityManager->clear(); } + + $workerExists = $this->workerRepository->findOneBy(['email' => $worklog->getWorker()]); + + if (!$workerExists) { + $worker = new Worker(); + $worker->setEmail($worklog->getWorker()); + $this->entityManager->persist($worker); + } } // Remove leftover worklogs from project and remove the worklogs. From b27903695cf3b89e8d8bef98e688b37be4e25814 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 15:04:05 +0200 Subject: [PATCH 110/134] coding standards --- src/Service/DataSynchronizationService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index d3c43430..3bc083fb 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -370,7 +370,10 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac if (!$workerExists) { $worker = new Worker(); - $worker->setEmail($worklog->getWorker()); + $workerEmail = $worklog->getWorker(); + if ($workerEmail) { + $worker->setEmail($worklog->getWorker()); + } $this->entityManager->persist($worker); } } From e76cd7de7e00ef82cfdecccbbde15c7015106504 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 15:08:07 +0200 Subject: [PATCH 111/134] trying to fix php unit test error --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bbe5f45b..0bd75049 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -238,7 +238,7 @@ jobs: mariadb: image: mariadb:lts ports: - - 3306 + - 3306:3306 env: MYSQL_USER: db MYSQL_PASSWORD: db From 149431c21056d868b41218d1986bb5fe0281c8b7 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 7 Jun 2024 15:15:17 +0200 Subject: [PATCH 112/134] coding standards + undid github action fix --- .github/workflows/pr.yml | 2 +- src/Service/DataSynchronizationService.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0bd75049..bbe5f45b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -238,7 +238,7 @@ jobs: mariadb: image: mariadb:lts ports: - - 3306:3306 + - 3306 env: MYSQL_USER: db MYSQL_PASSWORD: db diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index 3bc083fb..4f224cc7 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -371,8 +371,8 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac if (!$workerExists) { $worker = new Worker(); $workerEmail = $worklog->getWorker(); - if ($workerEmail) { - $worker->setEmail($worklog->getWorker()); + if (isset($workerEmail)) { + $worker->setEmail($workerEmail); } $this->entityManager->persist($worker); } From 5db18a8ac8954b35085c8910ea4146759fb1e015 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 09:31:52 +0200 Subject: [PATCH 113/134] moved worker-adding code above flush --- src/Service/DataSynchronizationService.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index 4f224cc7..d021082a 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -360,12 +360,6 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac ++$worklogsAdded; } - // Flush and clear for each batch. - if (0 === $worklogsAdded % self::BATCH_SIZE) { - $this->entityManager->flush(); - $this->entityManager->clear(); - } - $workerExists = $this->workerRepository->findOneBy(['email' => $worklog->getWorker()]); if (!$workerExists) { @@ -376,6 +370,12 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac } $this->entityManager->persist($worker); } + + // Flush and clear for each batch. + if (0 === $worklogsAdded % self::BATCH_SIZE) { + $this->entityManager->flush(); + $this->entityManager->clear(); + } } // Remove leftover worklogs from project and remove the worklogs. From 1f121a1c3b62609660f997b5ae08626326c8e5ca Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 12:45:00 +0200 Subject: [PATCH 114/134] added active period highlight in table, and scroll to active period --- src/Model/Reports/WorkloadReportData.php | 25 ++++++++++++ src/Service/WorkloadReportService.php | 45 +++++++++------------ templates/reports/workload_report.html.twig | 27 ++++++++++--- translations/messages.da.yaml | 1 + 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/Model/Reports/WorkloadReportData.php b/src/Model/Reports/WorkloadReportData.php index 61827f20..c5a2421d 100644 --- a/src/Model/Reports/WorkloadReportData.php +++ b/src/Model/Reports/WorkloadReportData.php @@ -12,6 +12,7 @@ class WorkloadReportData public ArrayCollection $period; /** @var ArrayCollection */ public ArrayCollection $workers; + public int $currentPeriodNumeric; public function __construct(string $viewmode) { @@ -19,4 +20,28 @@ public function __construct(string $viewmode) $this->period = new ArrayCollection(); $this->workers = new ArrayCollection(); } + + /** + * Get current week. + * + * @return int $currentPeriodNumeric + */ + public function getCurrentPeriodNumeric(): int + { + return $this->currentPeriodNumeric; + } + + /** + * Set current week. + * + * @param int $currentPeriodNumeric + * + * @return self + */ + public function setCurrentPeriodNumeric(int $currentPeriodNumeric): self + { + $this->currentPeriodNumeric = $currentPeriodNumeric; + + return $this; + } } diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 097abb56..15dcabef 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -6,30 +6,10 @@ namespace App\Service; -use /* - * Class WorkloadReportData - * - * This class retrieves workload data for generating reports. - */ -App\Model\Reports\WorkloadReportData; -use /* - * @var Connection - */ -App\Model\Reports\WorkloadReportWorker; -use /* - * @method Worker|null find($id, $lockMode = null, $lockVersion = null) - * @method Worker|null findOneBy(array $criteria, array $orderBy = null) - * @method Worker[] findAll() - * @method Worker[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ -App\Repository\WorkerRepository; -use /* - * @method Worklog|null find($id, $lockMode = null, $lockVersion = null) - * @method Worklog|null findOneBy(array $criteria, array $orderBy = null) - * @method Worklog[] findAll() - * @method Worklog[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ -App\Repository\WorklogRepository; +use App\Model\Reports\WorkloadReportData; +use App\Model\Reports\WorkloadReportWorker; +use App\Repository\WorkerRepository; +use App\Repository\WorklogRepository; class WorkloadReportService { @@ -72,7 +52,14 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), }; - return $this->getWorkloadData($periods, $getDatesOfPeriod, $getReadablePeriod, $viewMode); + // Callable to get a current representation of a given period. + $currentPeriodNumeric = match ($viewMode) { + 'month' => (int) (new \DateTime())->format('n'), + 'week' => (int) (new \DateTime())->format('W'), + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getCurrentPeriodNumeric match"), + }; + + return $this->getWorkloadData($periods, $getDatesOfPeriod, $getReadablePeriod, $currentPeriodNumeric, $viewMode); } /** @@ -88,7 +75,7 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData * * @throws \Exception When the calculated roundedLoggedPercentage is null */ - private function getWorkloadData(array $periods, callable $getDatesOfPeriod, callable $getReadablePeriod, string $viewMode): WorkloadReportData + private function getWorkloadData(array $periods, callable $getDatesOfPeriod, callable $getReadablePeriod, int $currentPeriodNumeric, string $viewMode): WorkloadReportData { $workloadReportData = new WorkloadReportData($viewMode); $workers = $this->workerRepository->findAll(); @@ -96,7 +83,7 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal foreach ($periods as $period) { // Get period specific readable period representation for table headers. $readablePeriod = $getReadablePeriod($period); - $workloadReportData->period->add($readablePeriod); + $workloadReportData->period->set($period, $readablePeriod); } foreach ($workers as $worker) { $workloadReportWorker = new WorkloadReportWorker(); @@ -104,6 +91,10 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $workloadReportWorker->setWorkload($worker->getWorkload()); foreach ($periods as $period) { + // Add current period match-point (current week-number, month-number etc.) + if ($period === $currentPeriodNumeric) { + $workloadReportData->setCurrentPeriodNumeric($period); + } // Get first and last date in period. $firstAndLastDate = $getDatesOfPeriod($period); diff --git a/templates/reports/workload_report.html.twig b/templates/reports/workload_report.html.twig index 45439524..beb23b59 100644 --- a/templates/reports/workload_report.html.twig +++ b/templates/reports/workload_report.html.twig @@ -8,8 +8,17 @@ {{ 'reports.workload_report.workload'|trans }} - {% for period in data.period %} - + {% for periodNumeric, period in data.period %} + + {% if data.viewmode == 'week' %} {{ 'reports.workload_report.week'|trans }} {{ period }} {% else %} @@ -25,7 +34,7 @@ - +
    {{ worker.email }} @@ -37,12 +46,14 @@
    - + {{ worker.workload }} - {% for week in worker.loggedPercentage %} - + {% for periodNumeric, week in worker.loggedPercentage %} + {{ week ~ "%" }} {% endfor %} @@ -50,4 +61,8 @@ {% endfor %} +
    + {{ 'reports.workload_report.hidden-entries'|trans }}: + +
    diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index b2f17a21..6e7faeec 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -571,6 +571,7 @@ reports: week: 'Uge' workers: 'Medarbejdere' worker: 'Medarbejder' + hidden-entries: 'Skjulte medarbejdere' view: list_title: 'Views' From b33ebf7a4daf242bd326fcf55227285356cfe485 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 12:49:20 +0200 Subject: [PATCH 115/134] Restored correct changelog version links --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8e728f..ce1a652d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,7 +265,8 @@ complete process. * Updated to authorization code flow. * Changed worklog save button styling to be sticky. -[Unreleased]: https://github.com/itk-dev/economics/compare/2.2.0...HEAD +[Unreleased]: https://github.com/itk-dev/economics/compare/2.3.0...HEAD +[2.3.0]: https://github.com/itk-dev/economics/compare/2.2.0...2.3.0 [2.2.0]: https://github.com/itk-dev/economics/compare/2.1.2...2.2.0 [2.1.2]: https://github.com/itk-dev/economics/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/itk-dev/economics/compare/2.1.0...2.1.1 From af32d125170e4b51c8bc7e4329cc1bc1f73b358c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 12:53:01 +0200 Subject: [PATCH 116/134] Added missing param types in worklogRepository --- src/Repository/WorklogRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index e702ae3b..a4bb2ce7 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -166,7 +166,7 @@ public function getDistinctWorklogUsers(): array return $workers; } - public function findWorklogsByWorkerAndDateRange($worker, $date_from, $date_to) + public function findWorklogsByWorkerAndDateRange(string $worker, string $date_from, string $date_to) { $qb = $this->createQueryBuilder('wor'); From 9729c0effe4ce912f0a934cea8b972958ea162c4 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 13:04:06 +0200 Subject: [PATCH 117/134] Rolled back migrations and created a new one --- migrations/Version20240607125158.php | 33 ------------------- ...06085631.php => Version20240610110230.php} | 6 ++-- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 migrations/Version20240607125158.php rename migrations/{Version20240606085631.php => Version20240610110230.php} (61%) diff --git a/migrations/Version20240607125158.php b/migrations/Version20240607125158.php deleted file mode 100644 index 83087a09..00000000 --- a/migrations/Version20240607125158.php +++ /dev/null @@ -1,33 +0,0 @@ -addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); - $this->addSql('DROP INDEX UNIQ_9FB2BF621203AA7B ON worker'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_9FB2BF621203AA7B ON worker (workload)'); - } -} diff --git a/migrations/Version20240606085631.php b/migrations/Version20240610110230.php similarity index 61% rename from migrations/Version20240606085631.php rename to migrations/Version20240610110230.php index 81584aba..6dd05243 100644 --- a/migrations/Version20240606085631.php +++ b/migrations/Version20240610110230.php @@ -10,7 +10,7 @@ /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20240606085631 extends AbstractMigration +final class Version20240610110230 extends AbstractMigration { public function getDescription(): string { @@ -20,14 +20,12 @@ public function getDescription(): string public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), UNIQUE INDEX UNIQ_9FB2BF621203AA7B (workload), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE issue ADD tags VARCHAR(255) DEFAULT NULL, DROP hours_remaining'); + $this->addSql('CREATE TABLE worker (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, workload DOUBLE PRECISION NOT NULL, UNIQUE INDEX UNIQ_9FB2BF62E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); } public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs $this->addSql('DROP TABLE worker'); - $this->addSql('ALTER TABLE issue ADD hours_remaining DOUBLE PRECISION DEFAULT NULL, DROP tags'); } } From 865a60d665f7e1225e4684564e205726f51c0d29 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 13:24:32 +0200 Subject: [PATCH 118/134] added .idea/ folder to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 03edda3a..8903daeb 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ yarn-error.log ###> vincentlanglet/twig-cs-fixer ### /.twig-cs-fixer.cache ###< vincentlanglet/twig-cs-fixer ### + +# Ignore files generated by PhpStorm +/.idea/ From 02b55fca9ba5a04d444423b49ea0ff60e7f0a5e1 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 13:34:29 +0200 Subject: [PATCH 119/134] Removed unneccessary doc comment --- src/Service/WorkloadReportService.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 15dcabef..b55500be 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -1,9 +1,5 @@ Date: Mon, 10 Jun 2024 13:37:57 +0200 Subject: [PATCH 120/134] Corrected doc comment --- src/Service/DateTimeHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 5aaed723..39dabbd1 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -96,7 +96,7 @@ public function getWeeksOfYear(int $year = null): array /** * Retrieves an array of months and their corresponding numeric representation of a year. * - * @return array an array where the keys are month names and the values are their corresponding numeric representation (1-12) + * @return array an array of months with corresponding numeric representation (1-12) */ public function getMonthsOfYear(): array { From 353a0adecdb243ba31b46959efd6ba5dbf8a357a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 10 Jun 2024 13:52:10 +0200 Subject: [PATCH 121/134] updated comment --- src/Service/WorkloadReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index b55500be..5de34742 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -48,7 +48,7 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), }; - // Callable to get a current representation of a given period. + // Callable to get a current representation of a given period (current week-number, month-number etc.). $currentPeriodNumeric = match ($viewMode) { 'month' => (int) (new \DateTime())->format('n'), 'week' => (int) (new \DateTime())->format('W'), From b7360a0df4bfd0d4af04f61f4a46b2aaca44c51c Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 10:43:06 +0200 Subject: [PATCH 122/134] Remoned idea folder from project gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8903daeb..03edda3a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,3 @@ yarn-error.log ###> vincentlanglet/twig-cs-fixer ### /.twig-cs-fixer.cache ###< vincentlanglet/twig-cs-fixer ### - -# Ignore files generated by PhpStorm -/.idea/ From a692c3c7ea5c50bea8776971dd4da791a6422d2f Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 11:03:16 +0200 Subject: [PATCH 123/134] Changed use of snake_case to camelCase in worklog repository method --- src/Repository/WorklogRepository.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index a4bb2ce7..be925e6a 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -166,7 +166,7 @@ public function getDistinctWorklogUsers(): array return $workers; } - public function findWorklogsByWorkerAndDateRange(string $worker, string $date_from, string $date_to) + public function findWorklogsByWorkerAndDateRange(string $worker, string $dateFrom, string $dateTo) { $qb = $this->createQueryBuilder('wor'); @@ -175,8 +175,8 @@ public function findWorklogsByWorkerAndDateRange(string $worker, string $date_fr ->andWhere('wor.worker = :worker') ->setParameters([ 'worker' => $worker, - 'date_from' => $date_from, - 'date_to' => $date_to, + 'date_from' => $dateFrom, + 'date_to' => $dateTo, ]) ->getQuery()->getResult(); } From 90e25d5193bcd323468d0b4ea65890b74ae57640 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 11:04:04 +0200 Subject: [PATCH 124/134] Replaced generic loop method with use of range method --- src/Service/DateTimeHelper.php | 15 --------------- src/Service/WorkloadReportService.php | 7 ++----- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 39dabbd1..c4c14765 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -93,21 +93,6 @@ public function getWeeksOfYear(int $year = null): array return $weekArray; } - /** - * Retrieves an array of months and their corresponding numeric representation of a year. - * - * @return array an array of months with corresponding numeric representation (1-12) - */ - public function getMonthsOfYear(): array - { - $months = []; - for ($i = 1; $i <= 12; ++$i) { - $months[] = $i; - } - - return $months; - } - /** * Retrieves the name of the month for a given month number. * diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 5de34742..5e1dcdb9 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -29,7 +29,7 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData { // Get period based on viewmode. $periods = match ($viewMode) { - 'month' => $this->dateTimeHelper->getMonthsOfYear(), + 'month' => range(1, 12), 'week' => $this->dateTimeHelper->getWeeksOfYear(), default => throw new \Exception("Unexpected value for viewMode: $viewMode in periods match"), }; @@ -81,6 +81,7 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal $readablePeriod = $getReadablePeriod($period); $workloadReportData->period->set($period, $readablePeriod); } + foreach ($workers as $worker) { $workloadReportWorker = new WorkloadReportWorker(); $workloadReportWorker->setEmail($worker->getUserIdentifier()); @@ -113,10 +114,6 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal // Get total logged percentage based on weekly workload. $roundedLoggedPercentage = $this->getRoundedLoggedPercentage($loggedHours, $workerWorkload, $viewMode); - if (!$roundedLoggedPercentage) { - throw new \Exception("Value of calculated roundedLoggedPercentage: $roundedLoggedPercentage cannot be null"); - } - // Add percentage result to worker for current period. $workloadReportWorker->loggedPercentage->set($period, $roundedLoggedPercentage); } From 8669862286be89503006e28eea866f1a189dee7e Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 11:23:37 +0200 Subject: [PATCH 125/134] Corrected a method description --- src/Service/WorkloadReportService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 5e1dcdb9..f24d2596 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -48,7 +48,7 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), }; - // Callable to get a current representation of a given period (current week-number, month-number etc.). + // Get current numeric representation of a given period type (current week-number, month-number etc.). $currentPeriodNumeric = match ($viewMode) { 'month' => (int) (new \DateTime())->format('n'), 'week' => (int) (new \DateTime())->format('W'), From d00b9efbdb9dfb4e70e19f4f03b0a76803931377 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 15:29:07 +0200 Subject: [PATCH 126/134] Moved two callable values into a private function each --- src/Service/WorkloadReportService.php | 58 +++++++++++++++++++-------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index f24d2596..cbce3a5f 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -27,13 +27,6 @@ public function __construct( */ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData { - // Get period based on viewmode. - $periods = match ($viewMode) { - 'month' => range(1, 12), - 'week' => $this->dateTimeHelper->getWeeksOfYear(), - default => throw new \Exception("Unexpected value for viewMode: $viewMode in periods match"), - }; - // Callable to get first and last date of a given period. $getDatesOfPeriod = match ($viewMode) { 'month' => function ($monthNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfMonth($monthNumber); }, @@ -48,21 +41,13 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), }; - // Get current numeric representation of a given period type (current week-number, month-number etc.). - $currentPeriodNumeric = match ($viewMode) { - 'month' => (int) (new \DateTime())->format('n'), - 'week' => (int) (new \DateTime())->format('W'), - default => throw new \Exception("Unexpected value for viewMode: $viewMode in getCurrentPeriodNumeric match"), - }; - - return $this->getWorkloadData($periods, $getDatesOfPeriod, $getReadablePeriod, $currentPeriodNumeric, $viewMode); + return $this->getWorkloadData($getDatesOfPeriod, $getReadablePeriod, $viewMode); } /** * Get the workload data based on the specified periods, date calculation method, readable period representation method * and view mode. * - * @param array $periods The list of periods * @param callable $getDatesOfPeriod The callable to get the first and last date of a given period * @param callable $getReadablePeriod The callable to get a readable representation of a given period * @param string $viewMode The view mode @@ -71,10 +56,11 @@ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData * * @throws \Exception When the calculated roundedLoggedPercentage is null */ - private function getWorkloadData(array $periods, callable $getDatesOfPeriod, callable $getReadablePeriod, int $currentPeriodNumeric, string $viewMode): WorkloadReportData + private function getWorkloadData(callable $getDatesOfPeriod, callable $getReadablePeriod, string $viewMode): WorkloadReportData { $workloadReportData = new WorkloadReportData($viewMode); $workers = $this->workerRepository->findAll(); + $periods = $this->getPeriods($viewMode); foreach ($periods as $period) { // Get period specific readable period representation for table headers. @@ -89,6 +75,7 @@ private function getWorkloadData(array $periods, callable $getDatesOfPeriod, cal foreach ($periods as $period) { // Add current period match-point (current week-number, month-number etc.) + $currentPeriodNumeric = $this->getCurrentPeriodNumeric($viewMode); if ($period === $currentPeriodNumeric) { $workloadReportData->setCurrentPeriodNumeric($period); } @@ -157,4 +144,41 @@ public function getViewModes(): array 'Month' => 'month', ]; } + + /** + * Retrieves the current period as a numeric value based on the given view mode. + * + * @param string $viewMode the view mode to determine the current period + * + * @return int the current period as a numeric value + * + * @throws \Exception when an unexpected value for viewMode is provided + */ + private function getCurrentPeriodNumeric(string $viewMode): int + { + return match ($viewMode) { + 'month' => (int) (new \DateTime())->format('n'), + 'week' => (int) (new \DateTime())->format('W'), + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getCurrentPeriodNumeric match"), + }; + } + + /** + * Retrieves an array of periods based on the given view mode. + * + * @param string $viewMode the view mode to determine the periods + * + * @return array an array of periods + * + * @throws \Exception when an unexpected value for viewMode is provided + */ + private function getPeriods(string $viewMode): array + { + // Get period based on viewmode. + return match ($viewMode) { + 'month' => range(1, 12), + 'week' => $this->dateTimeHelper->getWeeksOfYear(), + default => throw new \Exception("Unexpected value for viewMode: $viewMode in periods match"), + }; + } } From 5b83ac9c4587f3a41beed144b9e16fb8663ad0b1 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 11 Jun 2024 15:57:46 +0200 Subject: [PATCH 127/134] Moved last two callables into private methods --- src/Service/WorkloadReportService.php | 87 +++++++++++++++------------ 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index cbce3a5f..840212c7 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -17,46 +17,15 @@ public function __construct( } /** - * Get the workload report based on the specified view mode. + * Retrieves the workload report data for the given view mode. * - * @param string $viewMode The view mode (default: 'week') + * @param string $viewMode the view mode to generate the report for * - * @return WorkloadReportData The workload report data + * @return WorkloadReportData the workload report data * - * @throws \Exception When an unexpected value for viewMode is provided + * @throws \Exception when the workload of a worker cannot be unset */ public function getWorkloadReport(string $viewMode = 'week'): WorkloadReportData - { - // Callable to get first and last date of a given period. - $getDatesOfPeriod = match ($viewMode) { - 'month' => function ($monthNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfMonth($monthNumber); }, - 'week' => function ($weekNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfWeek($weekNumber); }, - default => throw new \Exception("Unexpected value for viewMode: $viewMode in getDatesOfPeriod match"), - }; - - // Callable to get a readable representation of a given period. - $getReadablePeriod = match ($viewMode) { - 'month' => fn ($monthNumber) => $this->dateTimeHelper->getMonthName($monthNumber), - 'week' => fn ($period) => $period, - default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), - }; - - return $this->getWorkloadData($getDatesOfPeriod, $getReadablePeriod, $viewMode); - } - - /** - * Get the workload data based on the specified periods, date calculation method, readable period representation method - * and view mode. - * - * @param callable $getDatesOfPeriod The callable to get the first and last date of a given period - * @param callable $getReadablePeriod The callable to get a readable representation of a given period - * @param string $viewMode The view mode - * - * @return WorkloadReportData The workload report data - * - * @throws \Exception When the calculated roundedLoggedPercentage is null - */ - private function getWorkloadData(callable $getDatesOfPeriod, callable $getReadablePeriod, string $viewMode): WorkloadReportData { $workloadReportData = new WorkloadReportData($viewMode); $workers = $this->workerRepository->findAll(); @@ -64,8 +33,8 @@ private function getWorkloadData(callable $getDatesOfPeriod, callable $getReadab foreach ($periods as $period) { // Get period specific readable period representation for table headers. - $readablePeriod = $getReadablePeriod($period); - $workloadReportData->period->set($period, $readablePeriod); + $readablePeriod = $this->getReadablePeriod($period, $viewMode); + $workloadReportData->period->set((string) $period, $readablePeriod); } foreach ($workers as $worker) { @@ -80,7 +49,7 @@ private function getWorkloadData(callable $getDatesOfPeriod, callable $getReadab $workloadReportData->setCurrentPeriodNumeric($period); } // Get first and last date in period. - $firstAndLastDate = $getDatesOfPeriod($period); + $firstAndLastDate = $this->getDatesOfPeriod($period, $viewMode); // Get all worklogs between the two dates. $worklogs = $this->worklogRepository->findWorklogsByWorkerAndDateRange($worker->getUserIdentifier(), $firstAndLastDate['first'], $firstAndLastDate['last']); @@ -163,6 +132,48 @@ private function getCurrentPeriodNumeric(string $viewMode): int }; } + /** + * Retrieves an array of dates for a given period based on the view mode. + * + * @param int $period the period for which to retrieve dates + * @param string $viewMode the view mode to determine the dates of the period + * + * @return array an array of dates for the given period + * + * @throws \Exception when an unexpected value for viewMode is provided + */ + private function getDatesOfPeriod(int $period, string $viewMode): array + { + $periodDates = match ($viewMode) { + 'month' => function ($monthNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfMonth($monthNumber); }, + 'week' => function ($weekNumber) { return $this->dateTimeHelper->getFirstAndLastDateOfWeek($weekNumber); }, + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getDatesOfPeriod match"), + }; + + return $periodDates($period); + } + + /** + * Retrieves the readable period based on the given period and view mode. + * + * @param int $period the period to be made readable + * @param string $viewMode the view mode to determine the format of the readable period + * + * @return string the readable period + * + * @throws \Exception when an unexpected value for viewMode is provided + */ + private function getReadablePeriod(int $period, string $viewMode): string + { + $readablePeriod = match ($viewMode) { + 'month' => fn ($monthNumber) => $this->dateTimeHelper->getMonthName($monthNumber), + 'week' => fn ($weekNumber) => (string) $weekNumber, + default => throw new \Exception("Unexpected value for viewMode: $viewMode in getReadablePeriod match"), + }; + + return $readablePeriod($period); + } + /** * Retrieves an array of periods based on the given view mode. * From 37f8767f4059b860841e4e5986698619f7151ac8 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 12 Jun 2024 11:14:41 +0200 Subject: [PATCH 128/134] Fixed docker service healthcheck --- .github/workflows/pr.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bbe5f45b..12f81df5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -244,7 +244,24 @@ jobs: MYSQL_PASSWORD: db MYSQL_DATABASE: db_test MYSQL_ROOT_PASSWORD: password - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + # https://mariadb.org/mariadb-server-docker-official-images-healthcheck-without-mysqladmin/ + # healthcheck: + # test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] + # start_period: 1m + # start_interval: 10s + # interval: 5s + # timeout: 2s + # retries: 3 + # + # Actions report + # + # The workflow is not valid. .github/workflows/pr.yml (Line: 17, Col: 17): Unexpected value 'healthcheck' + # + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=2s + --health-retries=3 strategy: fail-fast: false matrix: From 370da9597f69d4971fb672360e306379b482f368 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:55:42 +0200 Subject: [PATCH 129/134] 1742: Fixed synchronization issues --- CHANGELOG.md | 2 ++ migrations/Version20240620091523.php | 31 ++++++++++++++++++++++ src/Entity/Worker.php | 2 +- src/Model/Invoices/IssueData.php | 2 ++ src/Service/DataSynchronizationService.php | 13 ++++++--- src/Service/LeantimeApiService.php | 2 ++ 6 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 migrations/Version20240620091523.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 548f37a2..a545897d 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-132](https://github.com/itk-dev/economics/pull/132) + 1742: Fixed synchronization issues. * [PR-128](https://github.com/itk-dev/economics/pull/128) 1595: Added retryable http client decorator for handling rate limiting. * [PR-117](https://github.com/itk-dev/economics/pull/117) diff --git a/migrations/Version20240620091523.php b/migrations/Version20240620091523.php new file mode 100644 index 00000000..6c331e83 --- /dev/null +++ b/migrations/Version20240620091523.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE worker CHANGE workload workload DOUBLE PRECISION DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE worker CHANGE workload workload DOUBLE PRECISION NOT NULL'); + } +} diff --git a/src/Entity/Worker.php b/src/Entity/Worker.php index 623ef059..18763774 100644 --- a/src/Entity/Worker.php +++ b/src/Entity/Worker.php @@ -17,7 +17,7 @@ class Worker #[ORM\Column(length: 180, unique: true)] private ?string $email = null; - #[ORM\Column(length: 180)] + #[ORM\Column(length: 180, nullable: true)] private ?float $workload = null; public function __construct() diff --git a/src/Model/Invoices/IssueData.php b/src/Model/Invoices/IssueData.php index 74ad44be..76b2f46e 100644 --- a/src/Model/Invoices/IssueData.php +++ b/src/Model/Invoices/IssueData.php @@ -20,6 +20,8 @@ class IssueData public ?Collection $versions; public ?\DateTime $resolutionDate = null; public string $projectId; + public ?int $planHours; + public ?int $hourRemaining; public function __construct() { diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index d021082a..f9025bca 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -320,6 +320,7 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac } $worklog = $this->worklogRepository->findOneBy(['worklogId' => $worklogDatum->projectTrackerId]); + if (!$worklog) { $worklog = new Worklog(); @@ -329,6 +330,7 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $this->entityManager->persist($worklog); } + $worklog ->setWorklogId($worklogDatum->projectTrackerId) ->setDescription($worklogDatum->comment) @@ -350,6 +352,7 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $project->addWorklog($worklog); // Keep the worklog. $worklogId = $worklog->getId(); + if (null !== $worklogId) { unset($worklogsToDeleteIds[$worklogId]); } @@ -360,15 +363,17 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac ++$worklogsAdded; } - $workerExists = $this->workerRepository->findOneBy(['email' => $worklog->getWorker()]); + $workerEmail = $worklog->getWorker(); + + $workerExists = $this->workerRepository->findOneBy(['email' => $workerEmail]); if (!$workerExists) { - $worker = new Worker(); - $workerEmail = $worklog->getWorker(); if (isset($workerEmail)) { + $worker = new Worker(); $worker->setEmail($workerEmail); + $this->entityManager->persist($worker); + $this->entityManager->flush(); } - $this->entityManager->persist($worker); } // Flush and clear for each batch. diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 57406616..a368ce74 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -158,6 +158,8 @@ public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $m $issueData->accountKey = ''; $issueData->epicKey = $issue->tags; $issueData->epicName = $issue->tags; + $issueData->planHours = $issue->planHours; + $issueData->hourRemaining = $issue->hourRemaining; if (isset($issue->milestoneid) && isset($issue->milestoneHeadline)) { $issueData->versions?->add(new VersionData($issue->milestoneid, $issue->milestoneHeadline)); } From 0c59a19af6beeb89ad04b98445269fc64d8dc655 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:14:04 +0200 Subject: [PATCH 130/134] 1742: Simplified hour report form --- CHANGELOG.md | 2 + src/Controller/HourReportController.php | 183 ++++-------------- src/Form/HourReportType.php | 111 +++++++++-- src/Model/Reports/HourReportFormData.php | 6 +- src/Service/HourReportService.php | 50 +---- ...Report.html.twig => hour_report.html.twig} | 11 +- templates/reports/reports.html.twig | 6 +- translations/messages.da.yaml | 26 +-- 8 files changed, 169 insertions(+), 226 deletions(-) rename templates/reports/{hourReport.html.twig => hour_report.html.twig} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a545897d..c4cab544 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-133](https://github.com/itk-dev/economics/pull/133) + 1742: Simplified hour report form. * [PR-132](https://github.com/itk-dev/economics/pull/132) 1742: Fixed synchronization issues. * [PR-128](https://github.com/itk-dev/economics/pull/128) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index d1aa4c67..d3a4110e 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -3,10 +3,14 @@ namespace App\Controller; use App\Entity\DataProvider; +use App\Entity\Project; +use App\Entity\Version; use App\Exception\EconomicsException; use App\Form\HourReportType; use App\Model\Reports\HourReportFormData; use App\Repository\DataProviderRepository; +use App\Repository\ProjectRepository; +use App\Repository\VersionRepository; use App\Service\DataProviderService; use App\Service\HourReportService; use App\Service\ViewService; @@ -23,11 +27,12 @@ class HourReportController extends AbstractController { public function __construct( - private readonly DataProviderService $dataProviderService, private readonly DataProviderRepository $dataProviderRepository, - private readonly ViewService $viewService, - private readonly HourReportService $hourReportService, - private readonly ?string $defaultDataProvider, + private readonly ViewService $viewService, + private readonly HourReportService $hourReportService, + private readonly ?string $defaultDataProvider, + private readonly ProjectRepository $projectRepository, + private readonly VersionRepository $versionRepository, ) { } @@ -39,158 +44,51 @@ public function __construct( public function index(Request $request): Response { $reportData = null; - - $mode = 'hour_report'; - $error = null; $reportFormData = new HourReportFormData(); - $form = $this->createForm(HourReportType::class, $reportFormData, [ - 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), - 'method' => 'GET', - 'attr' => [ - 'id' => 'hour_report', - ], - // Since this is only a filtering form, csrf is not needed. - 'csrf_protection' => false, - ]); - - $form->add('dataProvider', EntityType::class, [ - 'class' => DataProvider::class, - 'required' => true, - 'label' => 'reports.hour_report.select_data_provider', - 'label_attr' => ['class' => 'label'], - 'attr' => [ - 'onchange' => 'this.form.submit()', - 'class' => 'form-element', - 'data-preselect' => $this->defaultDataProvider ?? '', - ], - 'help' => 'sprint_report.data_provider_helptext', - 'choices' => $this->dataProviderRepository->findAll(), - ]); - $form->add('projectId', ChoiceType::class, [ - 'placeholder' => 'reports.hour_report.select_option', - 'choices' => [], - 'required' => true, - 'label' => 'reports.hour_report.select_project', - 'label_attr' => ['class' => 'label'], - 'attr' => [ - 'disabled' => true, - 'class' => 'form-element', - ], - ]); - $form->add('versionId', ChoiceType::class, [ - 'placeholder' => 'reports.hour_report.select_option', - 'choices' => [], - 'required' => true, - 'label' => 'reports.hour_report.select_milestone', - 'label_attr' => ['class' => 'label'], - 'attr' => [ - 'disabled' => true, - 'class' => 'form-element', - ], - ]); + $dataProvider = null; + $project = null; + $version = null; $requestData = $request->query->all('hour_report'); - if (isset($requestData['sprint_report'])) { - $requestData = $requestData['sprint_report']; + if (!empty($requestData['dataProvider'])) { + $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); + } else if ($this->defaultDataProvider !== null) { + $dataProvider = $this->dataProviderRepository->find($this->defaultDataProvider); } - if (!empty($requestData['dataProvider']) || $this->defaultDataProvider) { - if (!empty($requestData['dataProvider'])) { - $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); - } else { - $dataProvider = $this->dataProviderRepository->find($this->defaultDataProvider); - } - - if (null != $dataProvider) { - $projectChoices = $this->hourReportService->getProjects(); - - // Override projectId with element with choices. - $form->add('projectId', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', - 'choices' => $projectChoices, - 'required' => true, - 'label' => 'sprint_report.select_project', - 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-choices'], - 'attr' => [ - 'class' => 'form-element', - 'onchange' => 'this.form.submit()', - ], - ]); - } + if (!empty($requestData['project'])) { + $project = $this->projectRepository->find($requestData['project']); } - if ((!empty($requestData['dataProvider']) || $this->defaultDataProvider) && !empty($requestData['projectId'])) { - $projectId = $requestData['projectId']; - $milestoneChoices = $this->hourReportService->getMilestones($projectId, true); - - // Override projectId with element with choices. - $form->add('versionId', ChoiceType::class, [ - 'placeholder' => 'reports.hour_report.select_an_option', - 'choices' => $milestoneChoices, - 'required' => true, - 'label' => 'reports.hour_report.select_milestone', - 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-choices'], - 'attr' => [ - 'class' => 'form-element', - ], - ]); - $form->add('fromDate', DateType::class, [ - 'widget' => 'single_text', - 'input' => 'datetime', - 'required' => false, - 'label' => 'reports.hour_report.select_fromdate', - 'label_attr' => ['class' => 'label'], - 'empty_data' => '', - 'by_reference' => true, - 'attr' => [ - 'class' => 'form-element', - 'data-preselect-date' => $this->hourReportService->getFromDate(), - ], - ]); - $form->add('toDate', DateType::class, [ - 'widget' => 'single_text', - 'input' => 'datetime', - 'required' => false, - 'label' => 'reports.hour_report.select_todate', - 'label_attr' => ['class' => 'label'], - 'empty_data' => '', - 'by_reference' => true, - 'attr' => [ - 'class' => 'form-element disabled', - 'data-preselect-date' => $this->hourReportService->getToDate(), - ], - ]); - $form->add('submit', ButtonType::class, [ - 'block_name' => 'Submit', - 'attr' => [ - 'onclick' => 'this.form.submit()', - 'class' => 'hour-report-submit button', - ], - ]); + if (!empty($requestData['version'])) { + $version = $this->versionRepository->find($requestData['version']); } + $form = $this->createForm(HourReportType::class, $reportFormData, [ + // Since this is only a filtering form, csrf is not needed. + 'csrf_protection' => false, + 'action' => $this->generateUrl('app_hour_report', $this->viewService->addView([])), + 'method' => 'GET', + 'attr' => [ + 'id' => 'hour_report', + ], + 'data_provider' => $dataProvider, + 'project' => $project, + 'version' => $version, + ]); + $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $projectId = $form->get('projectId')->getData(); - $milestoneId = $form->get('versionId')->getData(); - $selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider ?? null; - $fromDate = $form->has('fromDate') ? $form->get('fromDate')->getData() : new \DateTime($this->hourReportService->getFromDate()); - $toDate = $form->has('toDate') ? $form->get('toDate')->getData() : new \DateTime($this->hourReportService->getToDate()); - - if (!empty($milestoneId) && !empty($projectId) && !empty($dataProvider)) { - $reportData = $this->hourReportService->getHourReport($projectId, $fromDate, $toDate, $milestoneId); - $mode = 'hourReport'; - } + $project = $form->get('project')->getData() ?? null; + $version = $form->get('version')->getData() ?? null; + $fromDate = $form->get('fromDate')->getData() ?? null; + $toDate = $form->get('toDate')->getData() ?? null; - // If milestone is '0', it will evaluate as empty above, but really we want to get the report for all milestones - if (!empty($projectId) && !empty($selectedDataProvider) && '0' === $milestoneId) { - $reportData = $this->hourReportService->getHourReport($projectId, $fromDate, $toDate); - $mode = 'hourReport'; + if ($project !== null) { + $reportData = $this->hourReportService->getHourReport($project, $fromDate, $toDate, $version); } } @@ -198,8 +96,7 @@ public function index(Request $request): Response 'controller_name' => 'HourReportController', 'form' => $form, 'data' => $reportData, - 'mode' => $mode, - 'error' => $error, + 'mode' => 'hour_report', ])); } } diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index c66ce0d3..42aca8d4 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -2,44 +2,117 @@ namespace App\Form; +use App\Entity\DataProvider; +use App\Entity\Project; +use App\Entity\Version; use App\Model\Reports\HourReportFormData; +use App\Repository\DataProviderRepository; +use App\Repository\ProjectRepository; +use App\Repository\VersionRepository; +use App\Service\HourReportService; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class HourReportType extends AbstractType { + public function __construct( + private readonly HourReportService $hourReportService, + private readonly DataProviderRepository $dataProviderRepository, + ) + { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('dataProvider') - ->add('projectId', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', - 'required' => true, - 'label' => 'sprint_report.select_project', + ->add('dataProvider', EntityType::class, [ + 'class' => DataProvider::class, + 'required' => false, + 'label' => 'hour_report.data_provider', 'label_attr' => ['class' => 'label'], - 'disabled' => true, + 'placeholder' => 'hour_report.select_data_provider', 'attr' => [ + 'onchange' => 'this.form.submit()', 'class' => 'form-element', - 'data-sprint-report-target' => 'project', - 'data-action' => 'sprint-report#submitFormProjectId', ], - 'row_attr' => ['class' => 'form-row form-choices'], + 'help' => 'sprint_report.data_provider_helptext', + 'choices' => $this->dataProviderRepository->findAll(), ]) - ->add('versionId', ChoiceType::class, [ - 'placeholder' => 'sprint_report.select_an_option', + ->add('project', EntityType::class, [ + 'class' => Project::class, 'required' => false, - 'label' => 'sprint_report.select_version', + 'query_builder' => function (ProjectRepository $projectRepository) use ($options) { + $query = $projectRepository->getIncluded(); + if ($options['data_provider'] !== null) { + $query->where('project.dataProvider = :dataProvider')->setParameter('dataProvider', $options['data_provider']); + } + return $query; + }, + 'placeholder' => 'hour_report.select_project', + 'choice_label' => function (Project $pr) { + return $pr->getName() . ' (' . $pr->getDataProvider() . ')'; + }, + 'attr' => [ + 'class' => 'form-element', + 'onchange' => 'this.form.submit()', + ], + 'label' => 'hour_report.project', 'label_attr' => ['class' => 'label'], - 'disabled' => true, + 'disabled' => empty($options['data_provider']), + ]) + ->add('version', EntityType::class, [ + 'class' => Version::class, + 'required' => false, + 'query_builder' => function (VersionRepository $versionRepository) use ($options) { + $query = $versionRepository->createQueryBuilder('version'); + if ($options['project'] !== null) { + $query->where('version.project = :project')->setParameter('project', $options['project']); + } + return $query; + }, + 'choice_label' => function (Version $version) { + return $version->getName() . ' (' . $version->getProject() . ')'; + }, 'attr' => [ 'class' => 'form-element', - 'data-sprint-report-target' => 'version', - 'data-action' => 'sprint-report#submitForm', + 'onchange' => 'this.form.submit()', ], 'row_attr' => ['class' => 'form-row form-choices'], + 'placeholder' => 'hour_report.all_versions', + 'label' => 'hour_report.version', + 'label_attr' => ['class' => 'label'], + 'disabled' => empty($options['project']), + ]) + ->add('fromDate', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'hour_report.from_date', + 'label_attr' => ['class' => 'label'], + 'by_reference' => true, + 'data' => $options['fromDate'] ?? $this->hourReportService->getDefaultFromDate(), + 'attr' => [ + 'class' => 'form-element', + 'onchange' => 'this.form.submit()', + ], + ]) + ->add('toDate', DateType::class, [ + 'widget' => 'single_text', + 'input' => 'datetime', + 'required' => false, + 'label' => 'hour_report.to_date', + 'label_attr' => ['class' => 'label'], + 'data' => $options['toDate'] ?? $this->hourReportService->getDefaultToDate(), + 'by_reference' => true, + 'attr' => [ + 'class' => 'form-element', + 'onchange' => 'this.form.submit()', + ], ]); + } public function configureOptions(OptionsResolver $resolver): void @@ -49,6 +122,10 @@ public function configureOptions(OptionsResolver $resolver): void 'attr' => [ 'data-sprint-report-target' => 'form', ], - ]); + ]) + ->setRequired('data_provider') + ->setRequired('project') + ->setRequired('version') + ; } } diff --git a/src/Model/Reports/HourReportFormData.php b/src/Model/Reports/HourReportFormData.php index 525bd1a5..f2a8bc18 100644 --- a/src/Model/Reports/HourReportFormData.php +++ b/src/Model/Reports/HourReportFormData.php @@ -3,12 +3,14 @@ namespace App\Model\Reports; use App\Entity\DataProvider; +use App\Entity\Project; +use App\Entity\Version; class HourReportFormData { public DataProvider $dataProvider; - public string $projectId; - public string $versionId; + public Project $project; + public Version $version; public \DateTimeInterface $fromDate; public \DateTimeInterface $toDate; } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index a5a28521..6d36dfd3 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -2,14 +2,13 @@ namespace App\Service; +use App\Entity\Project; use App\Exception\EconomicsException; use App\Model\Reports\HourReportData; use App\Model\Reports\HourReportProjectTag; use App\Model\Reports\HourReportProjectTicket; use App\Model\Reports\HourReportTimesheet; use App\Repository\IssueRepository; -use App\Repository\ProjectRepository; -use App\Repository\VersionRepository; use App\Repository\WorklogRepository; class HourReportService @@ -17,55 +16,21 @@ class HourReportService public function __construct( private readonly IssueRepository $issueRepository, private readonly WorklogRepository $worklogRepository, - private readonly ProjectRepository $projectRepository, - private readonly VersionRepository $versionRepository, ) { } /** * @throws EconomicsException */ - public function getProjects(): array + public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate, int $versionId = null): HourReportData { - $projects = $this->projectRepository->findAll(); - $projectChoices = []; - foreach ($projects as $project) { - $projectChoices[$project->getName()] = $project->getId(); - } - - return $projectChoices; - } - - public function getMilestones(string $projectId, bool $includeAllOption = false): array - { - $milestones = $this->versionRepository->findBy(['project' => $projectId]); - $milestoneChoices = []; - if ($includeAllOption) { - $milestoneChoices['All milestones'] = '0'; - } - foreach ($milestones as $milestone) { - $milestoneName = $milestone->getName() ?? ''; - $milestoneChoices[$milestoneName] = $milestone->getId(); - } - - return $milestoneChoices; - } - - /** - * @throws EconomicsException - */ - public function getHourReport(string $projectId, ?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate, int $versionId = null): HourReportData - { - if (!$projectId) { - throw new EconomicsException('No project id specified'); - } $hourReportData = new HourReportData(0, 0); // If version is provided, we only want the issues containing the versionId if ($versionId) { $projectIssues = $this->issueRepository->issuesContainingVersion($versionId); } else { - $projectIssues = $this->issueRepository->findBy(['project' => $projectId]); + $projectIssues = $this->issueRepository->findBy(['project' => $project]); } foreach ($projectIssues as $issue) { @@ -131,18 +96,19 @@ private function processTimesheetsData($timesheetsData, $fromDate, $toDate): arr return [$timesheets, $totalTicketSpent]; } - public function getFromDate(): string + public function getDefaultFromDate(): \DateTime { $fromDate = new \DateTime(); $fromDate->modify('first day of this month'); - return $fromDate->format('Y-m-d'); + return $fromDate; } - public function getToDate(): string + public function getDefaultToDate(): \DateTime { $fromDate = new \DateTime(); + $fromDate->modify('last day of this month'); - return $fromDate->format('Y-m-d'); + return $fromDate; } } diff --git a/templates/reports/hourReport.html.twig b/templates/reports/hour_report.html.twig similarity index 93% rename from templates/reports/hourReport.html.twig rename to templates/reports/hour_report.html.twig index 118fd36e..685a9f47 100644 --- a/templates/reports/hourReport.html.twig +++ b/templates/reports/hour_report.html.twig @@ -6,19 +6,16 @@ {{ 'planning.projects'|trans }} - {{ 'reports.hour_report.estimated_hours'|trans }} + {{ 'hour_report.estimated_hours'|trans }} - {{ 'reports.hour_report.logged_hours'|trans }} + {{ 'hour_report.logged_hours'|trans }} - {% for projectTag in data.projectTags %} - -
    @@ -45,7 +42,7 @@
    - {{ projectTicket.headline }} +   {{ projectTicket.headline }}
    @@ -56,7 +53,7 @@ {% endfor %} - {{ 'reports.hour_report.total'|trans }} + {{ 'hour_report.total'|trans }} {{ data.projectTotalEstimated }} {{ data.projectTotalSpent }} diff --git a/templates/reports/reports.html.twig b/templates/reports/reports.html.twig index f5fa9996..fc993b4f 100644 --- a/templates/reports/reports.html.twig +++ b/templates/reports/reports.html.twig @@ -1,15 +1,15 @@ {% extends 'base.html.twig' %} -{% block title %}{{ 'reports.hour_report.title'|trans }}{% endblock %} +{% block title %}{{ 'hour_report.title'|trans }}{% endblock %} {% block content %} -

    {{ ('reports.' ~ mode ~ '.title')|trans() }}

    +

    {{ (mode ~ '.title')|trans() }}

    {{ form_start(form) }} {{ form_row(form.dataProvider) }} {{ form_end(form) }} - {% if error is not null %} + {% if error is defined and error is not null %} {% include 'components/alert.html.twig' with {level: "danger", text: error} %} {% endif %} {% if data is not empty %} diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 6e7faeec..938a3b65 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -550,18 +550,6 @@ reports: team_time_spent: 'Timeforbrug' team_description: 'Beskrivelse' team_no_records_found: 'Ingen resultater' - hour_report: - title: 'Timerapport' - select_option: 'Vælg en mulighed' - select_data_provider: 'Vælg datakilde' - select_project: 'Vælg projekt' - select_milestone: 'Vælg milestone' - estimated_hours: 'Estimeret' - logged_hours: 'Logget' - total: 'Total' - select_fromdate: 'Fra dato' - select_todate: 'Til dato' - select_an_option: 'Vælg en mulighed' workload_report: title: 'Workloadrapport' select_data_provider: 'Vælg datakilde' @@ -573,6 +561,20 @@ reports: worker: 'Medarbejder' hidden-entries: 'Skjulte medarbejdere' +hour_report: + title: 'Timerapport' + data_provider: 'Datakilde' + project: 'Projekt' + version: 'Version' + estimated_hours: 'Estimeret' + logged_hours: 'Registeret' + total: 'Total' + from_date: 'Fra dato' + to_date: 'Til dato' + select_data_provider: 'Vælg datakilde' + select_project: 'Vælg project' + all_versions: 'Alle versioner' + view: list_title: 'Views' list_description: 'Oversigt over de views der er mulige i EConomics' From 6facbb828f82441524740aa3e636717cbeda2b7c Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:20:22 +0200 Subject: [PATCH 131/134] 1742: Applied coding standards --- src/Controller/HourReportController.php | 20 ++++++-------------- src/Form/HourReportType.php | 14 +++++++------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/Controller/HourReportController.php b/src/Controller/HourReportController.php index d3a4110e..fb958087 100644 --- a/src/Controller/HourReportController.php +++ b/src/Controller/HourReportController.php @@ -2,23 +2,15 @@ namespace App\Controller; -use App\Entity\DataProvider; -use App\Entity\Project; -use App\Entity\Version; use App\Exception\EconomicsException; use App\Form\HourReportType; use App\Model\Reports\HourReportFormData; use App\Repository\DataProviderRepository; use App\Repository\ProjectRepository; use App\Repository\VersionRepository; -use App\Service\DataProviderService; use App\Service\HourReportService; use App\Service\ViewService; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\Form\Extension\Core\Type\ButtonType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -28,10 +20,10 @@ class HourReportController extends AbstractController { public function __construct( private readonly DataProviderRepository $dataProviderRepository, - private readonly ViewService $viewService, - private readonly HourReportService $hourReportService, - private readonly ?string $defaultDataProvider, - private readonly ProjectRepository $projectRepository, + private readonly ViewService $viewService, + private readonly HourReportService $hourReportService, + private readonly ?string $defaultDataProvider, + private readonly ProjectRepository $projectRepository, private readonly VersionRepository $versionRepository, ) { } @@ -54,7 +46,7 @@ public function index(Request $request): Response if (!empty($requestData['dataProvider'])) { $dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']); - } else if ($this->defaultDataProvider !== null) { + } elseif (null !== $this->defaultDataProvider) { $dataProvider = $this->dataProviderRepository->find($this->defaultDataProvider); } @@ -87,7 +79,7 @@ public function index(Request $request): Response $fromDate = $form->get('fromDate')->getData() ?? null; $toDate = $form->get('toDate')->getData() ?? null; - if ($project !== null) { + if (null !== $project) { $reportData = $this->hourReportService->getHourReport($project, $fromDate, $toDate, $version); } } diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index 42aca8d4..e2b44980 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -21,8 +21,7 @@ class HourReportType extends AbstractType public function __construct( private readonly HourReportService $hourReportService, private readonly DataProviderRepository $dataProviderRepository, - ) - { + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -46,14 +45,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'query_builder' => function (ProjectRepository $projectRepository) use ($options) { $query = $projectRepository->getIncluded(); - if ($options['data_provider'] !== null) { + if (null !== $options['data_provider']) { $query->where('project.dataProvider = :dataProvider')->setParameter('dataProvider', $options['data_provider']); } + return $query; }, 'placeholder' => 'hour_report.select_project', 'choice_label' => function (Project $pr) { - return $pr->getName() . ' (' . $pr->getDataProvider() . ')'; + return $pr->getName().' ('.$pr->getDataProvider().')'; }, 'attr' => [ 'class' => 'form-element', @@ -68,13 +68,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'query_builder' => function (VersionRepository $versionRepository) use ($options) { $query = $versionRepository->createQueryBuilder('version'); - if ($options['project'] !== null) { + if (null !== $options['project']) { $query->where('version.project = :project')->setParameter('project', $options['project']); } + return $query; }, 'choice_label' => function (Version $version) { - return $version->getName() . ' (' . $version->getProject() . ')'; + return $version->getName().' ('.$version->getProject().')'; }, 'attr' => [ 'class' => 'form-element', @@ -112,7 +113,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'onchange' => 'this.form.submit()', ], ]); - } public function configureOptions(OptionsResolver $resolver): void From ab313e8e4fa2e9755b21da8871caa3f94b7d7ca6 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:28:20 +0200 Subject: [PATCH 132/134] 1742: Fixed version parameter issue --- src/Form/HourReportType.php | 6 +++--- src/Repository/IssueRepository.php | 5 +++-- src/Service/HourReportService.php | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index e2b44980..28eccd61 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -52,8 +52,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void return $query; }, 'placeholder' => 'hour_report.select_project', - 'choice_label' => function (Project $pr) { - return $pr->getName().' ('.$pr->getDataProvider().')'; + 'choice_label' => function (Project $project) { + return $project->getName(); }, 'attr' => [ 'class' => 'form-element', @@ -75,7 +75,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void return $query; }, 'choice_label' => function (Version $version) { - return $version->getName().' ('.$version->getProject().')'; + return $version->getName(); }, 'attr' => [ 'class' => 'form-element', diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 31ab79f2..fa92db04 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -4,6 +4,7 @@ use App\Entity\Issue; use App\Entity\Project; +use App\Entity\Version; use App\Model\Invoices\IssueFilterData; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -111,11 +112,11 @@ private function getClosedStatuses(Project $project): array ]; } - public function issuesContainingVersion(int $versionId): array + public function issuesContainingVersion(Version $version): array { $qb = $this->createQueryBuilder('issue') ->where(':version MEMBER OF issue.versions') - ->setParameter('version', $versionId); + ->setParameter('version', $version); return $qb->getQuery()->getResult(); } diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 6d36dfd3..2cd6de47 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -3,6 +3,7 @@ namespace App\Service; use App\Entity\Project; +use App\Entity\Version; use App\Exception\EconomicsException; use App\Model\Reports\HourReportData; use App\Model\Reports\HourReportProjectTag; @@ -22,13 +23,13 @@ public function __construct( /** * @throws EconomicsException */ - public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate, int $versionId = null): HourReportData + public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ?\DateTimeInterface $toDate, Version $version = null): HourReportData { $hourReportData = new HourReportData(0, 0); // If version is provided, we only want the issues containing the versionId - if ($versionId) { - $projectIssues = $this->issueRepository->issuesContainingVersion($versionId); + if ($version) { + $projectIssues = $this->issueRepository->issuesContainingVersion($version); } else { $projectIssues = $this->issueRepository->findBy(['project' => $project]); } From 889ffd3d21f6d15e1d7d46df80158e50a9668c66 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:06:48 +0200 Subject: [PATCH 133/134] 1742: Fixed from/to date filtering for issues --- migrations/Version20240621095459.php | 31 ++++++++++++++++++++++ src/Entity/Issue.php | 15 +++++++++++ src/Model/Invoices/IssueData.php | 1 + src/Service/DataSynchronizationService.php | 1 + src/Service/HourReportService.php | 29 +++++++++++++++----- src/Service/LeantimeApiService.php | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 migrations/Version20240621095459.php diff --git a/migrations/Version20240621095459.php b/migrations/Version20240621095459.php new file mode 100644 index 00000000..40b9eec5 --- /dev/null +++ b/migrations/Version20240621095459.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE issue ADD due_date DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE issue DROP due_date'); + } +} diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index b9f45126..ea441f4f 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -61,6 +61,9 @@ class Issue extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] public ?float $hoursRemaining; + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTimeInterface $dueDate = null; + public function __construct() { $this->versions = new ArrayCollection(); @@ -295,4 +298,16 @@ public function setHoursRemaining(?float $hoursRemaining): self return $this; } + + public function getDueDate(): ?\DateTimeInterface + { + return $this->dueDate; + } + + public function setDueDate(?\DateTimeInterface $dueDate): static + { + $this->dueDate = $dueDate; + + return $this; + } } diff --git a/src/Model/Invoices/IssueData.php b/src/Model/Invoices/IssueData.php index 76b2f46e..17edf1fd 100644 --- a/src/Model/Invoices/IssueData.php +++ b/src/Model/Invoices/IssueData.php @@ -22,6 +22,7 @@ class IssueData public string $projectId; public ?int $planHours; public ?int $hourRemaining; + public ?\DateTime $dueDate = null; public function __construct() { diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index f9025bca..2dc20e2c 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -235,6 +235,7 @@ public function syncIssuesForProject(int $projectId, callable $progressCallback $issue->setStatus($issueDatum->status); $issue->setPlanHours($issueDatum->planHours); $issue->setHoursRemaining($issueDatum->hourRemaining); + $issue->setDueDate($issueDatum->dueDate); // Leantime (as of now) supports only a single version (milestone) per issue. if (LeantimeApiService::class === $dataProvider?->getClass()) { diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 2cd6de47..b5a08443 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -4,6 +4,7 @@ use App\Entity\Project; use App\Entity\Version; +use App\Entity\Worklog; use App\Exception\EconomicsException; use App\Model\Reports\HourReportData; use App\Model\Reports\HourReportProjectTag; @@ -27,7 +28,7 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? { $hourReportData = new HourReportData(0, 0); - // If version is provided, we only want the issues containing the versionId + // If version is provided, we only want the issues containing the version. if ($version) { $projectIssues = $this->issueRepository->issuesContainingVersion($version); } else { @@ -36,11 +37,18 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? foreach ($projectIssues as $issue) { $totalTicketEstimated = (float) $issue->planHours; + $dueDate = $issue->getDueDate(); $timesheetData = $this->worklogRepository->findBy(['issue' => $issue->getId()]); list($timesheets, $totalTicketSpent) = $this->processTimesheetsData($timesheetData, $fromDate, $toDate); + // If no worklogs have been registered in the interval or if the due date is not in the interval, + // ignore the issue in the report. + if ($totalTicketSpent === 0 || $dueDate < $fromDate || $dueDate > $toDate) { + continue; + } + $projectTicket = new HourReportProjectTicket( $issue->getId(), $issue->getName(), @@ -65,6 +73,7 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? if (!$projectTag) { throw new EconomicsException('Project tag not found'); } + $projectTag->projectTickets->add($projectTicket); $hourReportData->projectTags->set((string) $issueEpicName, $projectTag); @@ -75,20 +84,28 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? return $hourReportData; } - private function processTimesheetsData($timesheetsData, $fromDate, $toDate): array + private function processTimesheetsData(array $timesheetsData, \DateTimeInterface $fromDate = null, \DateTimeInterface $toDate = null): array { $timesheets = []; $totalTicketSpent = 0; + /** @var Worklog $timesheetDatum */ foreach ($timesheetsData as $timesheetDatum) { - if ($fromDate && $toDate) { - $timesheetDate = $timesheetDatum->getStarted(); - if ($timesheetDate < $fromDate || $timesheetDate > $toDate) { + $timesheetDate = $timesheetDatum->getStarted(); + + if ($fromDate !== null) { + if ($timesheetDate < $fromDate) { + continue; + } + } + + if ($toDate !== null) { + if ($timesheetDate > $toDate) { continue; } } - $hoursSpent = (float) ($timesheetDatum->getTimeSpentSeconds() / 3600); + $hoursSpent = $timesheetDatum->getTimeSpentSeconds() / 3600; $timesheet = new HourReportTimesheet($timesheetDatum->getId(), $hoursSpent); $timesheets[] = $timesheet; $totalTicketSpent += $hoursSpent; diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index a368ce74..37291e9a 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -160,6 +160,7 @@ public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $m $issueData->epicName = $issue->tags; $issueData->planHours = $issue->planHours; $issueData->hourRemaining = $issue->hourRemaining; + $issueData->dueDate = !empty($issue->dateToFinish) && $issue->dateToFinish !== '0000-00-00 00:00:00' ? new \DateTime($issue->dateToFinish) : null; if (isset($issue->milestoneid) && isset($issue->milestoneHeadline)) { $issueData->versions?->add(new VersionData($issue->milestoneid, $issue->milestoneHeadline)); } From b9fc849a89695720116ffcd05fbf83e72eff8f74 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:01:37 +0200 Subject: [PATCH 134/134] 1742: Applied coding standards --- ...ortTimesheet.php => HourReportWorklog.php} | 6 +++--- src/Service/HourReportService.php | 20 +++++++++---------- src/Service/LeantimeApiService.php | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) rename src/Model/Reports/{HourReportTimesheet.php => HourReportWorklog.php} (71%) diff --git a/src/Model/Reports/HourReportTimesheet.php b/src/Model/Reports/HourReportWorklog.php similarity index 71% rename from src/Model/Reports/HourReportTimesheet.php rename to src/Model/Reports/HourReportWorklog.php index dd2c83de..fa64d134 100644 --- a/src/Model/Reports/HourReportTimesheet.php +++ b/src/Model/Reports/HourReportWorklog.php @@ -4,13 +4,13 @@ use Doctrine\Common\Collections\ArrayCollection; -class HourReportTimesheet +class HourReportWorklog { - public readonly string $id; + public readonly ?int $id; public readonly float $hours; public ArrayCollection $projectTicket; - public function __construct(string $id, float $hours) + public function __construct(?int $id, float $hours) { $this->id = $id; $this->hours = $hours; diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index b5a08443..57737f97 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -9,7 +9,7 @@ use App\Model\Reports\HourReportData; use App\Model\Reports\HourReportProjectTag; use App\Model\Reports\HourReportProjectTicket; -use App\Model\Reports\HourReportTimesheet; +use App\Model\Reports\HourReportWorklog; use App\Repository\IssueRepository; use App\Repository\WorklogRepository; @@ -45,7 +45,7 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? // If no worklogs have been registered in the interval or if the due date is not in the interval, // ignore the issue in the report. - if ($totalTicketSpent === 0 || $dueDate < $fromDate || $dueDate > $toDate) { + if (0 === $totalTicketSpent || $dueDate < $fromDate || $dueDate > $toDate) { continue; } @@ -84,29 +84,29 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? return $hourReportData; } - private function processTimesheetsData(array $timesheetsData, \DateTimeInterface $fromDate = null, \DateTimeInterface $toDate = null): array + private function processTimesheetsData(array $worklogs, \DateTimeInterface $fromDate = null, \DateTimeInterface $toDate = null): array { $timesheets = []; $totalTicketSpent = 0; - /** @var Worklog $timesheetDatum */ - foreach ($timesheetsData as $timesheetDatum) { - $timesheetDate = $timesheetDatum->getStarted(); + /** @var Worklog $worklog */ + foreach ($worklogs as $worklog) { + $timesheetDate = $worklog->getStarted(); - if ($fromDate !== null) { + if (null !== $fromDate) { if ($timesheetDate < $fromDate) { continue; } } - if ($toDate !== null) { + if (null !== $toDate) { if ($timesheetDate > $toDate) { continue; } } - $hoursSpent = $timesheetDatum->getTimeSpentSeconds() / 3600; - $timesheet = new HourReportTimesheet($timesheetDatum->getId(), $hoursSpent); + $hoursSpent = $worklog->getTimeSpentSeconds() / 3600; + $timesheet = new HourReportWorklog($worklog->getId(), $hoursSpent); $timesheets[] = $timesheet; $totalTicketSpent += $hoursSpent; } diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 37291e9a..8da177f9 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -160,7 +160,7 @@ public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $m $issueData->epicName = $issue->tags; $issueData->planHours = $issue->planHours; $issueData->hourRemaining = $issue->hourRemaining; - $issueData->dueDate = !empty($issue->dateToFinish) && $issue->dateToFinish !== '0000-00-00 00:00:00' ? new \DateTime($issue->dateToFinish) : null; + $issueData->dueDate = !empty($issue->dateToFinish) && '0000-00-00 00:00:00' !== $issue->dateToFinish ? new \DateTime($issue->dateToFinish) : null; if (isset($issue->milestoneid) && isset($issue->milestoneHeadline)) { $issueData->versions?->add(new VersionData($issue->milestoneid, $issue->milestoneHeadline)); }