Skip to content

Commit

Permalink
Merge pull request #184 from itk-dev/feature/3489-workload-report-per…
Browse files Browse the repository at this point in the history
…iod-average-etc

3489: workload report period average + etc
  • Loading branch information
jeppekroghitk authored Jan 13, 2025
2 parents 8de270a + 2f88958 commit ccb5044
Show file tree
Hide file tree
Showing 32 changed files with 1,132 additions and 92 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-182](https://github.com/itk-dev/economics/pull/182)
2597: Added invoicing rate report
* [PR-185](https://github.com/itk-dev/economics/pull/186)
2597: Added epic migration command.
* [PR-184](https://github.com/itk-dev/economics/pull/184)
3489: Workload report period averages.
* [PR-183](https://github.com/itk-dev/economics/pull/183)
2597: Added epic relations.
* [PR-187](https://github.com/itk-dev/economics/pull/187)
Expand Down
71 changes: 71 additions & 0 deletions assets/controllers/table_highlight_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Controller } from "@hotwired/stimulus";

/**
* Table highlight controller.
*
* Highlights the `<thead>` and first cell (`<th>` or `<td>`) of the row when
* hovering over a cell.
*/
export default class extends Controller {
connect() {
this.element.addEventListener(
"mouseenter",
(event) => this.highlight(event),
true,
);
this.element.addEventListener(
"mouseleave",
(event) => this.clearHighlights(event),
true,
);
}

/**
* Handles the highlighting of the appropriate `<thead>` column and row
* header.
*
* @param {MouseEvent} event
*/
highlight(event) {
// Only run if hovering a `<td>` (and not child elements like <span> or <a>)
if (event.target.tagName !== "TD") return;

const cell = event.target; // The actual hovered cell

// Find the index of the hovered cell (column index)
const cellIndex = Array.from(cell.parentNode.children).indexOf(cell);

// Highlight the corresponding column header (<th>) in the `<thead>`
const columnHeader = this.element.querySelector(
`thead th:nth-child(${cellIndex + 1})`,
);
if (columnHeader) columnHeader.classList.add("highlight-column");

// Highlight the first cell in the row (supports both <td> and <th>)
const rowStartCell = cell.parentNode.querySelector(
"th:first-child, td:first-child",
);
if (rowStartCell) rowStartCell.classList.add("highlight-row");
}

/**
* Clears all highlights when leaving a cell.
*
* @param {MouseEvent} event
*/
clearHighlights(event) {
// Only run if leaving a `<td>` (and not child elements)
if (event.target.tagName !== "TD") return;

// Remove the highlight class from all highlighted elements
this.element.querySelectorAll(".highlight-column").forEach((header) => {
header.classList.remove("highlight-column");
});

this.element
.querySelectorAll(".highlight-row")
.forEach((rowStartCell) => {
rowStartCell.classList.remove("highlight-row");
});
}
}
8 changes: 2 additions & 6 deletions assets/controllers/toggle-parent-child_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ export default class extends Controller {

displayChildrenForParentIds = [];

svgExpand = `<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"></path>
</svg>`;
svgExpand = `<svg class="svg-inline--fa fa-caret-right" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-right" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512" data-fa-i2svg=""><path fill="currentColor" d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z"></path></svg>`;

svgCollapse = `<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25"></path>
</svg>`;
svgCollapse = `<svg class="svg-inline--fa fa-caret-down" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" data-fa-i2svg=""><path fill="currentColor" d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"></path></svg>`;

connect() {
this.parentTargets.forEach((target) => {
Expand Down
4 changes: 3 additions & 1 deletion assets/fontawesome.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
faMaximize,
faEyeSlash,
faMinimize,
faCaretRight,
faCaretDown,
} from "@fortawesome/free-solid-svg-icons";

library.add(faMaximize, faEyeSlash, faMinimize);
library.add(faMaximize, faEyeSlash, faMinimize, faCaretRight, faCaretDown);
dom.i2svg();
15 changes: 15 additions & 0 deletions assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -454,5 +454,20 @@
@apply text-right;
max-width: 200px;
}
.double-number-container {
span {
max-width: 47px;
}
span:last-child {
cursor: help;
}
}
.highlight-column, .highlight-row {
border: 1px solid #fff !important;
}

tr[data-toggle-parent-child-target="child"]:not(.hidden) + tr[data-toggle-parent-child-target="parent"] > td,
tr[data-toggle-parent-child-target="child"]:not(.hidden) + tr[data-toggle-parent-child-target="parent"] > th {
border-top: 1px solid #fff;
}
}
31 changes: 31 additions & 0 deletions migrations/Version20250102120303.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250102120303 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE worker ADD include_in_reports TINYINT(1) DEFAULT 1 NOT NULL');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE worker DROP include_in_reports');
}
}
72 changes: 72 additions & 0 deletions src/Controller/InvoicingRateReportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace App\Controller;

use App\Form\InvoicingRateReportType;
use App\Model\Reports\InvoicingRateReportFormData;
use App\Model\Reports\InvoicingRateReportViewModeEnum;
use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum;
use App\Service\InvoicingRateReportService;
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\Security\Http\Attribute\IsGranted;

#[Route('/admin/reports/invoicing_rate_report')]
#[IsGranted('ROLE_REPORT')]
class InvoicingRateReportController extends AbstractController
{
public function __construct(
private readonly InvoicingRateReportService $invoicingRateReportService,
) {
}

/**
* @throws \DateMalformedStringException
*/
#[Route('/', name: 'app_invoicing_rate_report')]
public function index(Request $request): Response
{
$reportData = null;
$error = null;
$mode = 'invoicing_rate_report';
$reportFormData = new InvoicingRateReportFormData();

$form = $this->createForm(InvoicingRateReportType::class, $reportFormData, [
'action' => $this->generateUrl('app_invoicing_rate_report'),
'method' => 'GET',
'attr' => [
'id' => 'sprint_report',
],
'years' => [
(new \DateTime())->modify('-1 year')->format('Y'),
(new \DateTime())->format('Y'),
],
'csrf_protection' => false,
]);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$viewPeriodType = $form->get('viewPeriodType')->getData() ?? PeriodTypeEnum::WEEK;
$viewMode = InvoicingRateReportViewModeEnum::SUMMARY;
$year = $form->get('year')->getData();
$includeIssues = $form->get('includeIssues')->getData();

try {
$reportData = $this->invoicingRateReportService->getInvoicingRateReport($year, $viewPeriodType, $viewMode, $includeIssues);
} catch (\Exception $e) {
$error = $e->getMessage();
}
}

return $this->render('reports/reports.html.twig', [
'controller_name' => 'InvoicingRateReportController',
'form' => $form,
'error' => $error,
'data' => $reportData,
'mode' => $mode,
]);
}
}
6 changes: 5 additions & 1 deletion src/Controller/PlanningController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ private function preparePlanningData(Request $request): array
'attr' => [
'id' => 'sprint_report',
],
'years' => [(new \DateTime())->format('Y'), (new \DateTime())->modify('+1 year')->format('Y')],
'years' => [
(new \DateTime())->modify('-1 year')->format('Y'),
(new \DateTime())->format('Y'),
(new \DateTime())->modify('+1 year')->format('Y'),
],
'method' => 'GET',
'csrf_protection' => false,
]);
Expand Down
33 changes: 15 additions & 18 deletions src/Controller/WorkloadReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use App\Model\Reports\WorkloadReportFormData;
use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum;
use App\Model\Reports\WorkloadReportViewModeEnum as ViewModeEnum;
use App\Repository\DataProviderRepository;
use App\Service\WorkloadReportService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -19,11 +18,13 @@
class WorkloadReportController extends AbstractController
{
public function __construct(
private readonly DataProviderRepository $dataProviderRepository,
private readonly WorkloadReportService $workloadReportService,
) {
}

/**
* @throws \DateMalformedStringException
*/
#[Route('/', name: 'app_workload_report')]
public function index(Request $request): Response
{
Expand All @@ -38,28 +39,24 @@ public function index(Request $request): Response
'attr' => [
'id' => 'sprint_report',
],
'years' => [
(new \DateTime())->modify('-1 year')->format('Y'),
(new \DateTime())->format('Y'),
],
'csrf_protection' => false,
]);

$form->handleRequest($request);

$requestData = $request->query->all('workload_report');

if (!empty($requestData['dataProvider'])) {
$dataProvider = $this->dataProviderRepository->find($requestData['dataProvider']);

if ($form->isSubmitted() && $form->isValid()) {
$selectedDataProvider = $form->get('dataProvider')->getData() ?? $dataProvider;
$viewPeriodType = $form->get('viewPeriodType')->getData() ?? PeriodTypeEnum::WEEK;
$viewMode = $form->get('viewMode')->getData() ?? ViewModeEnum::WORKLOAD;
if ($form->isSubmitted() && $form->isValid()) {
$viewPeriodType = $form->get('viewPeriodType')->getData() ?? PeriodTypeEnum::WEEK;
$viewMode = $form->get('viewMode')->getData() ?? ViewModeEnum::WORKLOAD;
$year = $form->get('year')->getData();

if ($selectedDataProvider) {
try {
$reportData = $this->workloadReportService->getWorkloadReport($viewPeriodType, $viewMode);
} catch (\Exception $e) {
$error = $e->getMessage();
}
}
try {
$reportData = $this->workloadReportService->getWorkloadReport($year, $viewPeriodType, $viewMode);
} catch (\Exception $e) {
$error = $e->getMessage();
}
}

Expand Down
32 changes: 16 additions & 16 deletions src/Entity/Issue.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,49 +182,49 @@ public function setEpicName(?string $epicName): self
}

/**
* @return Collection<int, Epic>
* @return Collection<int, Version>
*/
public function getEpics(): Collection
public function getVersions(): Collection
{
return $this->epics;
return $this->versions;
}

public function addEpic(Epic $epic): self
public function addVersion(Version $version): self
{
if (!$this->epics->contains($epic)) {
$this->epics->add($epic);
if (!$this->versions->contains($version)) {
$this->versions->add($version);
}

return $this;
}

public function removeEpic(Epic $epic): self
public function removeVersion(Version $version): self
{
$this->epics->removeElement($epic);
$this->versions->removeElement($version);

return $this;
}

/**
* @return Collection<int, Version>
* @return Collection<int, Epic>
*/
public function getVersions(): Collection
public function getEpics(): Collection
{
return $this->versions;
return $this->epics;
}

public function addVersion(Version $version): self
public function addEpic(Epic $epic): self
{
if (!$this->versions->contains($version)) {
$this->versions->add($version);
if (!$this->epics->contains($epic)) {
$this->epics->add($epic);
}

return $this;
}

public function removeVersion(Version $version): self
public function removeEpic(Epic $epic): self
{
$this->versions->removeElement($version);
$this->epics->removeElement($epic);

return $this;
}
Expand Down
Loading

0 comments on commit ccb5044

Please sign in to comment.