From d6c15bb1cb931ae87a35fd2e9cd9ad9922cf7cab Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 21 Nov 2022 19:07:33 +0100 Subject: [PATCH] respect project start and end dates in quick-entry form (#3642) --- src/Controller/QuickEntryController.php | 48 +++++++------------ src/Form/QuickEntryForm.php | 20 ++++---- src/Form/Type/QuickEntryWeekType.php | 22 +++------ src/Model/QuickEntryModel.php | 16 +++---- src/Model/QuickEntryWeek.php | 63 ++++++++++++++++++++----- tests/Model/QuickEntryModelTest.php | 17 ++++++- tests/Model/QuickEntryWeekTest.php | 3 +- 7 files changed, 110 insertions(+), 79 deletions(-) diff --git a/src/Controller/QuickEntryController.php b/src/Controller/QuickEntryController.php index 50e57183..ac99280f 100644 --- a/src/Controller/QuickEntryController.php +++ b/src/Controller/QuickEntryController.php @@ -12,7 +12,6 @@ namespace App\Controller; use App\Configuration\SystemConfiguration; use App\Entity\Timesheet; use App\Form\QuickEntryForm; -use App\Model\QuickEntryModel; use App\Model\QuickEntryWeek; use App\Repository\Query\TimesheetQuery; use App\Repository\TimesheetRepository; @@ -111,6 +110,11 @@ class QuickEntryController extends AbstractController if (\array_key_exists($id, $rows)) { continue; } + // there is an edge case possible with a project that starts and ends between the start and end date + // user could still select it from the dropdown, but it is better to hide a row than displaying already ended projects + if (!$timesheet->getProject()->isVisibleAtDate($startWeek) && !$timesheet->getProject()->isVisibleAtDate($endWeek)) { + continue; + } $rows[$id] = [ 'days' => $week, 'project' => $timesheet->getProject(), @@ -123,13 +127,13 @@ class QuickEntryController extends AbstractController $defaultMinute = (int) $defaultBegin->format('i'); $defaultBegin->setTime($defaultHour, $defaultMinute, 0, 0); - // fill all rows and columns to make sure we do not have missing records - /** @var QuickEntryModel[] $models */ - $models = []; + $formModel = new QuickEntryWeek($startWeek); + foreach ($rows as $id => $row) { - $model = new QuickEntryModel($user, $row['project'], $row['activity']); + $model = $formModel->addRow($user, $row['project'], $row['activity']); foreach ($row['days'] as $dayId => $day) { if (!\array_key_exists('entry', $day)) { + // fill all rows and columns to make sure we do not have missing records $tmp = new Timesheet(); $tmp->setUser($user); $tmp->setProject($row['project']); @@ -141,11 +145,11 @@ class QuickEntryController extends AbstractController $model->addTimesheet($day['entry']); } } - $models[] = $model; } // create prototype model - $empty = new QuickEntryModel($user); + $empty = $formModel->createRow($user); + $empty->markAsPrototype(); foreach ($week as $dayId => $day) { $tmp = new Timesheet(); $tmp->setUser($user); @@ -156,10 +160,10 @@ class QuickEntryController extends AbstractController // add empty rows for simpler starting $minRows = \intval($this->configuration->find('quick_entry.minimum_rows')); - if (\count($models) < $minRows) { - $newRows = $minRows - \count($models); + if ($formModel->countRows() < $minRows) { + $newRows = $minRows - $formModel->countRows(); for ($a = 0; $a < $newRows; $a++) { - $model = new QuickEntryModel(); + $model = $formModel->addRow($user); foreach ($week as $dayId => $day) { $tmp = new Timesheet(); $tmp->setUser($user); @@ -167,19 +171,14 @@ class QuickEntryController extends AbstractController $tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0); $model->addTimesheet($tmp); } - - $models[] = $model; } } - // sort rows by projects - make it configurable in the future - uasort($models, [$this, 'sortByProjectName']); - - $formModel = new QuickEntryWeek($startWeek, $models); - $form = $this->createForm(QuickEntryForm::class, $formModel, [ 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'prototype_data' => $empty, + 'start_date' => $startWeek, + 'end_date' => $endWeek, ]); $form->handleRequest($request); @@ -194,7 +193,8 @@ class QuickEntryController extends AbstractController foreach ($data->getRows() as $tmpModel) { foreach ($tmpModel->getTimesheets() as $timesheet) { if ($timesheet->getId() !== null) { - if ($timesheet->getDuration(false) === null || $timesheet->getEnd() === null) { + $duration = $timesheet->getDuration(false); + if ($duration === null || $timesheet->getEnd() === null) { $deleteTimesheets[] = $timesheet; } else { $saveTimesheets[] = $timesheet; @@ -235,16 +235,4 @@ class QuickEntryController extends AbstractController 'form' => $form->createView(), ]); } - - private function sortByProjectName(QuickEntryModel $a, QuickEntryModel $b): int - { - $aName = $a->getProject() !== null ? $a->getProject()->getName() : null; - $bName = $b->getProject() !== null ? $b->getProject()->getName() : null; - - if ($aName === null || $bName === null) { - return -1; - } - - return strcmp($aName, $bName); - } } diff --git a/src/Form/QuickEntryForm.php b/src/Form/QuickEntryForm.php index f36dff78..9dce23bc 100644 --- a/src/Form/QuickEntryForm.php +++ b/src/Form/QuickEntryForm.php @@ -63,13 +63,6 @@ class QuickEntryForm extends AbstractType } )); - $startDate = new \DateTime(); - if ($builder->getData() !== null) { - /** @var QuickEntryWeek $data */ - $data = $builder->getData(); - $startDate = $data->getDate(); - } - $builder->add('date', WeekPickerType::class, [ 'model_timezone' => $options['timezone'], 'view_timezone' => $options['timezone'], @@ -83,7 +76,8 @@ class QuickEntryForm extends AbstractType 'entry_options' => [ 'label' => false, 'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(), - 'start_date' => $startDate, + 'start_date' => $options['start_date'], + 'end_date' => $options['end_date'], 'empty_data' => function (FormInterface $form) use ($options) { return clone $options['prototype_data']; }, @@ -103,13 +97,21 @@ class QuickEntryForm extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $start = new \DateTime(); + $start->setTime(0, 0, 0); + + $end = clone $start; + $end->add(new \DateInterval('P1W')); + $end->setTime(23, 59, 59); + $resolver->setDefaults([ 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_token_id' => 'timesheet_quick_edit', 'data_class' => QuickEntryWeek::class, 'timezone' => date_default_timezone_get(), - 'start_date' => new \DateTime(), + 'start_date' => $start, + 'end_date' => $end, 'prototype_data' => null, ]); } diff --git a/src/Form/Type/QuickEntryWeekType.php b/src/Form/Type/QuickEntryWeekType.php index 788bfce9..e944cec9 100644 --- a/src/Form/Type/QuickEntryWeekType.php +++ b/src/Form/Type/QuickEntryWeekType.php @@ -24,10 +24,7 @@ use Symfony\Component\Validator\Constraints\Valid; class QuickEntryWeekType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $projectOptions = [ 'label' => false, @@ -35,7 +32,9 @@ class QuickEntryWeekType extends AbstractType 'join_customer' => true, 'query_builder_for_user' => true, 'placeholder' => '', - 'activity_enabled' => true + 'activity_enabled' => true, + 'project_date_start' => $options['start_date'], + 'project_date_end' => $options['end_date'], ]; $builder->add('project', ProjectType::class, $projectOptions); @@ -47,13 +46,6 @@ class QuickEntryWeekType extends AbstractType return; } - $begin = clone $data->getFirstEntry()->getBegin(); - $begin->setTime(0, 0, 0); - $projectOptions['project_date_start'] = $begin; - - $end = clone $data->getLatestEntry()->getBegin(); - $end->setTime(23, 59, 59); - $projectOptions['project_date_end'] = $begin; $projectOptions['projects'] = [$data->getProject()]; $event->getForm()->add('project', ProjectType::class, $projectOptions); @@ -177,10 +169,7 @@ class QuickEntryWeekType extends AbstractType ); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => QuickEntryModel::class, @@ -188,6 +177,7 @@ class QuickEntryWeekType extends AbstractType 'duration_minutes' => null, 'duration_hours' => 10, 'start_date' => new DateTime(), + 'end_date' => new DateTime(), 'prototype_data' => null, ]); } diff --git a/src/Model/QuickEntryModel.php b/src/Model/QuickEntryModel.php index 0e8d3e0e..887d7104 100644 --- a/src/Model/QuickEntryModel.php +++ b/src/Model/QuickEntryModel.php @@ -22,6 +22,7 @@ class QuickEntryModel private $user; private $project; private $activity; + private $prototype = false; /** * @var Timesheet[] */ @@ -34,17 +35,14 @@ class QuickEntryModel $this->activity = $activity; } + public function markAsPrototype(): void + { + $this->prototype = true; + } + public function isPrototype(): bool { - if ($this->hasExistingTimesheet()) { - return false; - } - - if ($this->hasNewTimesheet()) { - return false; - } - - return $this->getUser() === null && $this->getProject() === null && $this->getActivity() === null; + return $this->prototype; } public function getUser(): ?User diff --git a/src/Model/QuickEntryWeek.php b/src/Model/QuickEntryWeek.php index dc20864a..b99519b6 100644 --- a/src/Model/QuickEntryWeek.php +++ b/src/Model/QuickEntryWeek.php @@ -9,27 +9,45 @@ namespace App\Model; +use App\Entity\Activity; +use App\Entity\Project; +use App\Entity\User; + /** * @internal */ class QuickEntryWeek { - private $date; - private $rows; + private $startDate; + private $rows = []; - /** - * @param \DateTime $startDate - * @param QuickEntryModel[] $rows - */ - public function __construct(\DateTime $startDate, array $rows) + public function __construct(\DateTime $startDate) { - $this->date = $startDate; - $this->rows = $rows; + $this->startDate = $startDate; + } + + public function addRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel + { + $model = $this->createRow($user, $project, $activity); + + $this->rows[] = $model; + + return $model; + } + + public function createRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel + { + return new QuickEntryModel($user, $project, $activity); } public function getDate(): \DateTime { - return $this->date; + return $this->startDate; + } + + public function countRows(): int + { + return \count($this->rows); } /** @@ -37,7 +55,30 @@ class QuickEntryWeek */ public function getRows(): array { - return $this->rows; + $rows = $this->rows; + + // sort rows by projects - make it configurable in the future + uasort($rows, [$this, 'sortByProjectName']); + + return $rows; + } + + private function sortByProjectName(QuickEntryModel $a, QuickEntryModel $b): int + { + $aName = $a->getProject() !== null ? $a->getProject()->getName() : null; + $bName = $b->getProject() !== null ? $b->getProject()->getName() : null; + + if ($aName === null && $bName === null) { + $result = 0; + } elseif ($aName === null && $bName !== null) { + $result = 1; + } elseif ($aName !== null && $bName === null) { + $result = -1; + } else { + $result = strcmp($aName, $bName); + } + + return $result < 0 ? -1 : 1; } /** diff --git a/tests/Model/QuickEntryModelTest.php b/tests/Model/QuickEntryModelTest.php index e52e1e24..657ab544 100644 --- a/tests/Model/QuickEntryModelTest.php +++ b/tests/Model/QuickEntryModelTest.php @@ -24,7 +24,7 @@ class QuickEntryModelTest extends TestCase public function testEmptyModel() { $sut = new QuickEntryModel(); - self::assertTrue($sut->isPrototype()); + self::assertFalse($sut->isPrototype()); self::assertNull($sut->getProject()); self::assertNull($sut->getActivity()); self::assertNull($sut->getUser()); @@ -105,7 +105,6 @@ class QuickEntryModelTest extends TestCase { $sut = new QuickEntryModel(); - self::assertTrue($sut->isPrototype()); self::assertFalse($sut->hasExistingTimesheet()); $mock = $this->createMock(Timesheet::class); $mock->method('getId')->willReturn(1); @@ -126,4 +125,18 @@ class QuickEntryModelTest extends TestCase self::assertSame($activity, $sut->getActivity()); self::assertSame($user, $sut->getUser()); } + + public function testPrototype() + { + $sut = new QuickEntryModel(); + $sut->markAsPrototype(); + self::assertTrue($sut->isPrototype()); + + $sut = new QuickEntryModel(); + $sut->markAsPrototype(); + $mock = $this->createMock(Timesheet::class); + $mock->method('getId')->willReturn(1); + $sut->addTimesheet($mock); + self::assertTrue($sut->isPrototype()); + } } diff --git a/tests/Model/QuickEntryWeekTest.php b/tests/Model/QuickEntryWeekTest.php index feef0c2d..1faa1654 100644 --- a/tests/Model/QuickEntryWeekTest.php +++ b/tests/Model/QuickEntryWeekTest.php @@ -21,9 +21,8 @@ class QuickEntryWeekTest extends TestCase public function testModel() { $date = new \DateTime(); - $rows = []; - $sut = new QuickEntryWeek($date, $rows); + $sut = new QuickEntryWeek($date); self::assertSame($date, $sut->getDate()); self::assertEquals([], $sut->getRows());