respect project start and end dates in quick-entry form (#3642)
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user