respect project start and end dates in quick-entry form (#3642)

This commit is contained in:
Kevin Papst
2022-11-21 19:07:33 +01:00
committed by GitHub
parent 4f698f73dc
commit d6c15bb1cb
7 changed files with 110 additions and 79 deletions

View File

@@ -12,7 +12,6 @@ namespace App\Controller;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Form\QuickEntryForm; use App\Form\QuickEntryForm;
use App\Model\QuickEntryModel;
use App\Model\QuickEntryWeek; use App\Model\QuickEntryWeek;
use App\Repository\Query\TimesheetQuery; use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
@@ -111,6 +110,11 @@ class QuickEntryController extends AbstractController
if (\array_key_exists($id, $rows)) { if (\array_key_exists($id, $rows)) {
continue; 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] = [ $rows[$id] = [
'days' => $week, 'days' => $week,
'project' => $timesheet->getProject(), 'project' => $timesheet->getProject(),
@@ -123,13 +127,13 @@ class QuickEntryController extends AbstractController
$defaultMinute = (int) $defaultBegin->format('i'); $defaultMinute = (int) $defaultBegin->format('i');
$defaultBegin->setTime($defaultHour, $defaultMinute, 0, 0); $defaultBegin->setTime($defaultHour, $defaultMinute, 0, 0);
// fill all rows and columns to make sure we do not have missing records $formModel = new QuickEntryWeek($startWeek);
/** @var QuickEntryModel[] $models */
$models = [];
foreach ($rows as $id => $row) { 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) { foreach ($row['days'] as $dayId => $day) {
if (!\array_key_exists('entry', $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 = new Timesheet();
$tmp->setUser($user); $tmp->setUser($user);
$tmp->setProject($row['project']); $tmp->setProject($row['project']);
@@ -141,11 +145,11 @@ class QuickEntryController extends AbstractController
$model->addTimesheet($day['entry']); $model->addTimesheet($day['entry']);
} }
} }
$models[] = $model;
} }
// create prototype model // create prototype model
$empty = new QuickEntryModel($user); $empty = $formModel->createRow($user);
$empty->markAsPrototype();
foreach ($week as $dayId => $day) { foreach ($week as $dayId => $day) {
$tmp = new Timesheet(); $tmp = new Timesheet();
$tmp->setUser($user); $tmp->setUser($user);
@@ -156,10 +160,10 @@ class QuickEntryController extends AbstractController
// add empty rows for simpler starting // add empty rows for simpler starting
$minRows = \intval($this->configuration->find('quick_entry.minimum_rows')); $minRows = \intval($this->configuration->find('quick_entry.minimum_rows'));
if (\count($models) < $minRows) { if ($formModel->countRows() < $minRows) {
$newRows = $minRows - \count($models); $newRows = $minRows - $formModel->countRows();
for ($a = 0; $a < $newRows; $a++) { for ($a = 0; $a < $newRows; $a++) {
$model = new QuickEntryModel(); $model = $formModel->addRow($user);
foreach ($week as $dayId => $day) { foreach ($week as $dayId => $day) {
$tmp = new Timesheet(); $tmp = new Timesheet();
$tmp->setUser($user); $tmp->setUser($user);
@@ -167,19 +171,14 @@ class QuickEntryController extends AbstractController
$tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0); $tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0);
$model->addTimesheet($tmp); $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, [ $form = $this->createForm(QuickEntryForm::class, $formModel, [
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'prototype_data' => $empty, 'prototype_data' => $empty,
'start_date' => $startWeek,
'end_date' => $endWeek,
]); ]);
$form->handleRequest($request); $form->handleRequest($request);
@@ -194,7 +193,8 @@ class QuickEntryController extends AbstractController
foreach ($data->getRows() as $tmpModel) { foreach ($data->getRows() as $tmpModel) {
foreach ($tmpModel->getTimesheets() as $timesheet) { foreach ($tmpModel->getTimesheets() as $timesheet) {
if ($timesheet->getId() !== null) { if ($timesheet->getId() !== null) {
if ($timesheet->getDuration(false) === null || $timesheet->getEnd() === null) { $duration = $timesheet->getDuration(false);
if ($duration === null || $timesheet->getEnd() === null) {
$deleteTimesheets[] = $timesheet; $deleteTimesheets[] = $timesheet;
} else { } else {
$saveTimesheets[] = $timesheet; $saveTimesheets[] = $timesheet;
@@ -235,16 +235,4 @@ class QuickEntryController extends AbstractController
'form' => $form->createView(), '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);
}
} }

View File

@@ -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, [ $builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'], 'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'], 'view_timezone' => $options['timezone'],
@@ -83,7 +76,8 @@ class QuickEntryForm extends AbstractType
'entry_options' => [ 'entry_options' => [
'label' => false, 'label' => false,
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(), '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) { 'empty_data' => function (FormInterface $form) use ($options) {
return clone $options['prototype_data']; return clone $options['prototype_data'];
}, },
@@ -103,13 +97,21 @@ class QuickEntryForm extends AbstractType
*/ */
public function configureOptions(OptionsResolver $resolver) 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([ $resolver->setDefaults([
'csrf_protection' => true, 'csrf_protection' => true,
'csrf_field_name' => '_token', 'csrf_field_name' => '_token',
'csrf_token_id' => 'timesheet_quick_edit', 'csrf_token_id' => 'timesheet_quick_edit',
'data_class' => QuickEntryWeek::class, 'data_class' => QuickEntryWeek::class,
'timezone' => date_default_timezone_get(), 'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(), 'start_date' => $start,
'end_date' => $end,
'prototype_data' => null, 'prototype_data' => null,
]); ]);
} }

View File

@@ -24,10 +24,7 @@ use Symfony\Component\Validator\Constraints\Valid;
class QuickEntryWeekType extends AbstractType class QuickEntryWeekType extends AbstractType
{ {
/** public function buildForm(FormBuilderInterface $builder, array $options): void
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$projectOptions = [ $projectOptions = [
'label' => false, 'label' => false,
@@ -35,7 +32,9 @@ class QuickEntryWeekType extends AbstractType
'join_customer' => true, 'join_customer' => true,
'query_builder_for_user' => true, 'query_builder_for_user' => true,
'placeholder' => '', '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); $builder->add('project', ProjectType::class, $projectOptions);
@@ -47,13 +46,6 @@ class QuickEntryWeekType extends AbstractType
return; 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()]; $projectOptions['projects'] = [$data->getProject()];
$event->getForm()->add('project', ProjectType::class, $projectOptions); $event->getForm()->add('project', ProjectType::class, $projectOptions);
@@ -177,10 +169,7 @@ class QuickEntryWeekType extends AbstractType
); );
} }
/** public function configureOptions(OptionsResolver $resolver): void
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => QuickEntryModel::class, 'data_class' => QuickEntryModel::class,
@@ -188,6 +177,7 @@ class QuickEntryWeekType extends AbstractType
'duration_minutes' => null, 'duration_minutes' => null,
'duration_hours' => 10, 'duration_hours' => 10,
'start_date' => new DateTime(), 'start_date' => new DateTime(),
'end_date' => new DateTime(),
'prototype_data' => null, 'prototype_data' => null,
]); ]);
} }

View File

@@ -22,6 +22,7 @@ class QuickEntryModel
private $user; private $user;
private $project; private $project;
private $activity; private $activity;
private $prototype = false;
/** /**
* @var Timesheet[] * @var Timesheet[]
*/ */
@@ -34,17 +35,14 @@ class QuickEntryModel
$this->activity = $activity; $this->activity = $activity;
} }
public function markAsPrototype(): void
{
$this->prototype = true;
}
public function isPrototype(): bool public function isPrototype(): bool
{ {
if ($this->hasExistingTimesheet()) { return $this->prototype;
return false;
}
if ($this->hasNewTimesheet()) {
return false;
}
return $this->getUser() === null && $this->getProject() === null && $this->getActivity() === null;
} }
public function getUser(): ?User public function getUser(): ?User

View File

@@ -9,27 +9,45 @@
namespace App\Model; namespace App\Model;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\User;
/** /**
* @internal * @internal
*/ */
class QuickEntryWeek class QuickEntryWeek
{ {
private $date; private $startDate;
private $rows; private $rows = [];
/** public function __construct(\DateTime $startDate)
* @param \DateTime $startDate
* @param QuickEntryModel[] $rows
*/
public function __construct(\DateTime $startDate, array $rows)
{ {
$this->date = $startDate; $this->startDate = $startDate;
$this->rows = $rows; }
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 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 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;
} }
/** /**

View File

@@ -24,7 +24,7 @@ class QuickEntryModelTest extends TestCase
public function testEmptyModel() public function testEmptyModel()
{ {
$sut = new QuickEntryModel(); $sut = new QuickEntryModel();
self::assertTrue($sut->isPrototype()); self::assertFalse($sut->isPrototype());
self::assertNull($sut->getProject()); self::assertNull($sut->getProject());
self::assertNull($sut->getActivity()); self::assertNull($sut->getActivity());
self::assertNull($sut->getUser()); self::assertNull($sut->getUser());
@@ -105,7 +105,6 @@ class QuickEntryModelTest extends TestCase
{ {
$sut = new QuickEntryModel(); $sut = new QuickEntryModel();
self::assertTrue($sut->isPrototype());
self::assertFalse($sut->hasExistingTimesheet()); self::assertFalse($sut->hasExistingTimesheet());
$mock = $this->createMock(Timesheet::class); $mock = $this->createMock(Timesheet::class);
$mock->method('getId')->willReturn(1); $mock->method('getId')->willReturn(1);
@@ -126,4 +125,18 @@ class QuickEntryModelTest extends TestCase
self::assertSame($activity, $sut->getActivity()); self::assertSame($activity, $sut->getActivity());
self::assertSame($user, $sut->getUser()); 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());
}
} }

View File

@@ -21,9 +21,8 @@ class QuickEntryWeekTest extends TestCase
public function testModel() public function testModel()
{ {
$date = new \DateTime(); $date = new \DateTime();
$rows = [];
$sut = new QuickEntryWeek($date, $rows); $sut = new QuickEntryWeek($date);
self::assertSame($date, $sut->getDate()); self::assertSame($date, $sut->getDate());
self::assertEquals([], $sut->getRows()); self::assertEquals([], $sut->getRows());