Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-328](https://github.com/itk-dev/economics/pull/328)
Added selected and total hours to the worklog selection list.
* [PR-324](https://github.com/itk-dev/economics/pull/324)
Added game center with snake
* [PR-303](https://github.com/itk-dev/economics/pull/303)
Expand Down
24 changes: 24 additions & 0 deletions assets/controllers/entry-select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class extends Controller {
"spinner",
"result",
"submitButton",
"selectedHours",
];

submitEndpoint = null;
Expand All @@ -20,6 +21,7 @@ export default class extends Controller {

connect() {
this.submitEndpoint = this.element.dataset.submitEndpoint;
this.updateSelectedHours();
}

toggleAll() {
Expand All @@ -30,11 +32,33 @@ export default class extends Controller {
});

this.selectAll = !this.selectAll;

this.updateSelectedHours();
}

checkboxClick(event) {
const entryId = event.params.id;
this.dirtyEntrys.add(entryId.toString());

this.updateSelectedHours();
}

updateSelectedHours() {
if (!this.hasSelectedHoursTarget) {
return;
}

const seconds = this.checkboxTargets.reduce((accumulator, target) => {
if (!target.checked) {
return accumulator;
}

return accumulator + Number(target.dataset.timeSpentSeconds);
}, 0);

this.selectedHoursTarget.textContent = String(
Math.round((seconds / 3600) * 100) / 100,
);
}

async submitFormRedirectWithIds(event) {
Expand Down
8 changes: 6 additions & 2 deletions assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,12 @@
@apply flex gap-3 mb-3;
}

.sticky-row {
@apply sticky bg-white text-black dark:bg-gray-900 dark:text-white border-0 bottom-0;
.sticky-actions {
@apply sticky bottom-0 flex flex-wrap items-center gap-5 px-6 py-4 bg-white text-black dark:bg-gray-900 dark:text-white border-t border-gray-300 dark:border-gray-600;
}

.sticky-actions-sums {
@apply flex flex-wrap items-center gap-5 ml-auto;
}

.comma-list span:after {
Expand Down
26 changes: 13 additions & 13 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2407,6 +2407,12 @@ parameters:
count: 1
path: tests/Integration/Controller/InvoiceFullFlowTest.php

-
message: '#^Call to an undefined method object\:\:sumSelectableTimeSpentSecondsByFilterData\(\)\.$#'
identifier: method.notFound
count: 1
path: tests/Integration/Controller/InvoiceFullFlowTest.php

-
message: '#^Cannot call method getAmount\(\) on App\\Entity\\InvoiceEntry\|null\.$#'
identifier: method.nonObject
Expand Down Expand Up @@ -2797,10 +2803,16 @@ parameters:
count: 1
path: tests/Integration/Repository/WorkerRepositoryTest.php

-
message: '#^Call to an undefined method object\:\:findBy\(\)\.$#'
identifier: method.notFound
count: 1
path: tests/Integration/Repository/WorklogRepositoryTest.php

-
message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#'
identifier: method.notFound
count: 8
count: 12
path: tests/Integration/Repository/WorklogRepositoryTest.php

-
Expand Down Expand Up @@ -2839,18 +2851,6 @@ parameters:
count: 5
path: tests/Integration/Repository/WorklogRepositoryTest.php

-
message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#'
identifier: assign.propertyType
count: 1
path: tests/Integration/Repository/WorklogRepositoryTest.php

-
message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$entityManager is never read, only written\.$#'
identifier: property.onlyWritten
count: 1
path: tests/Integration/Repository/WorklogRepositoryTest.php

-
message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$projectRepository \(App\\Repository\\ProjectRepository\) does not accept object\.$#'
identifier: assign.propertyType
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/InvoiceEntryWorklogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ public function worklogs(Request $request, Invoice $invoice, InvoiceEntry $invoi
$form->handleRequest($request);

$worklogs = $worklogRepository->findByFilterData($project, $invoiceEntry, $filterData);
$totalTimeSpentSeconds = $worklogRepository->sumSelectableTimeSpentSecondsByFilterData($project, $invoiceEntry, $filterData);

return $this->render('invoice_entry/worklogs.html.twig', [
'form' => $form->createView(),
'invoice' => $invoice,
'invoiceEntry' => $invoiceEntry,
'worklogs' => $worklogs,
'totalHours' => round($totalTimeSpentSeconds / 3600, 2),
'submitEndpoint' => $this->generateUrl('app_invoice_entry_select_worklogs', ['invoice' => $invoice->getId(), 'invoiceEntry' => $invoiceEntry->getId()]),
]);
}
Expand Down
37 changes: 36 additions & 1 deletion src/Repository/WorklogRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Enum\NonBillableVersionsEnum;
use App\Model\Invoices\InvoiceEntryWorklogsFilterData;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;

Expand Down Expand Up @@ -47,6 +48,40 @@ public function remove(Worklog $entity, bool $flush = false): void
}

public function findByFilterData(Project $project, InvoiceEntry $invoiceEntry, InvoiceEntryWorklogsFilterData $filterData): iterable
{
return $this->createFilterDataQueryBuilder($project, $invoiceEntry, $filterData)
->getQuery()
->execute();
}

/**
* Sum the time spent on the worklogs matching the given filter that can be
* added to the invoice entry.
*
* Already billed worklogs and worklogs held by another invoice entry are
* listed without a checkbox, so their time can never become part of the
* selection and must not be part of the total either.
*/
public function sumSelectableTimeSpentSecondsByFilterData(Project $project, InvoiceEntry $invoiceEntry, InvoiceEntryWorklogsFilterData $filterData): int
{
$qb = $this->createFilterDataQueryBuilder($project, $invoiceEntry, $filterData);

$sum = $qb
->select('SUM(worklog.timeSpentSeconds)')
->andWhere('worklog.isBilled = FALSE OR worklog.isBilled is NULL')
->andWhere($qb->expr()->orX(
$qb->expr()->isNull('worklog.invoiceEntry'),
$qb->expr()->eq('worklog.invoiceEntry', ':selectableInvoiceEntry')
))
->setParameter('selectableInvoiceEntry', $invoiceEntry)
->getQuery()
->getSingleScalarResult();

// SUM returns null when no worklogs match the filter.
return (int) $sum;
}

private function createFilterDataQueryBuilder(Project $project, InvoiceEntry $invoiceEntry, InvoiceEntryWorklogsFilterData $filterData): QueryBuilder
{
$qb = $this->createQueryBuilder('worklog');

Expand Down Expand Up @@ -97,7 +132,7 @@ public function findByFilterData(Project $project, InvoiceEntry $invoiceEntry, I
))->setParameter('invoiceEntry', $invoiceEntry);
}

return $qb->getQuery()->execute();
return $qb;
}

public function updateProjectByIssue(Issue $issue, Project $project): int
Expand Down
22 changes: 13 additions & 9 deletions templates/invoice_entry/worklogs.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<div {{ stimulus_controller('entry-select') }} data-submit-endpoint="{{ submitEndpoint }}">
<form id="entry-form">
<table class="table margin-bottom">
<table class="table">
<thead class="table-th">
<tr>
<th class="table-th">
Expand Down Expand Up @@ -55,6 +55,7 @@
{{ stimulus_target('entry-select', 'checkbox') }}
{{ stimulus_action('entry-select', 'checkboxClick', 'click', {id: worklog.id}) }}
data-id="{{ worklog.id }}"
data-time-spent-seconds="{{ worklog.timeSpentSeconds }}"
value="{{ worklog.id }}"
id="entry-{{ worklog.id }}"
{{ owned_by_invoice_entry ? 'checked' : '' }}
Expand Down Expand Up @@ -96,18 +97,21 @@
{% endfor %}
{% if worklogs is empty %}
<tr class="table-tr">
<td colspan="8" class="table-td">{{ 'invoices.list_no_records_found'|trans }}</td>
<td colspan="9" class="table-td">{{ 'invoices.list_no_records_found'|trans }}</td>
</tr>
{% endif %}
<tr class="table-tr sticky-row">
<td colspan="8" class="table-td">
<button type="submit" class="button mr-5" data-action="entry-select#submitForm" data-entry-select-target="submitButton">{{ 'worklog.action_save'|trans }}</button>
<span class="hidden" data-entry-select-target="spinner">loading...</span>
<span class="hidden" data-entry-select-target="result"></span>
</td>
</tr>
</tbody>
</table>

<div class="sticky-actions">
<button type="submit" class="button" data-action="entry-select#submitForm" data-entry-select-target="submitButton">{{ 'worklog.action_save'|trans }}</button>
<span class="hidden" data-entry-select-target="spinner">loading...</span>
<span class="hidden" data-entry-select-target="result"></span>
<div class="sticky-actions-sums">
<span>{{ 'worklog.selected_hours'|trans }}: <span class="font-bold" aria-live="polite" data-entry-select-target="selectedHours">0</span></span>
<span>{{ 'worklog.total_hours'|trans }}: <span class="font-bold" data-total-hours="{{ totalHours }}">{{ totalHours }}</span></span>
</div>
</div>
</form>
</div>

Expand Down
26 changes: 26 additions & 0 deletions tests/Integration/Controller/InvoiceFullFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Enum\InvoiceEntryTypeEnum;
use App\Enum\MaterialNumberEnum;
use App\Model\Invoices\ConfirmData;
use App\Model\Invoices\InvoiceEntryWorklogsFilterData;
use App\Repository\ClientRepository;
use App\Repository\InvoiceEntryRepository;
use App\Repository\InvoiceRepository;
Expand Down Expand Up @@ -172,7 +173,32 @@ public function testFullInvoiceLifecycle(): void
$this->assertSame(0.0, (float) $worklogEntry->getAmount());

// 6. Attach worklogs to the WORKLOG entry.
$worklogsCrawler = $client->request('GET', '/admin/invoices/'.$invoiceId.'/entries/'.$worklogEntryId.'/worklogs');
$this->assertResponseIsSuccessful();

$worklogRepository = static::getContainer()->get(WorklogRepository::class);

// The sums bar reports the hours that can be selected in the filtered
// list, and each checkbox carries the time the Stimulus controller sums
// for the selection. The controller renders the page with the filter
// defaults, since an unsubmitted GET form leaves the filter untouched.
$expectedTotalHours = round($worklogRepository->sumSelectableTimeSpentSecondsByFilterData(
$project,
$worklogEntry,
new InvoiceEntryWorklogsFilterData()
) / 3600, 2);
$this->assertGreaterThan(0, $expectedTotalHours);

$sums = $worklogsCrawler->filter('.sticky-actions-sums');
$this->assertCount(1, $sums);
$this->assertSame('0', trim($sums->filter('[data-entry-select-target="selectedHours"]')->text()));
$this->assertEqualsWithDelta(
$expectedTotalHours,
(float) $sums->filter('[data-total-hours]')->attr('data-total-hours'),
0.001
);
$this->assertGreaterThan(0, $worklogsCrawler->filter('input[data-time-spent-seconds]')->count());

$unbilled = $worklogRepository->findBy(
['project' => $project, 'isBilled' => false],
['id' => 'ASC'],
Expand Down
Loading
Loading