weekly quick-entry form (#2793)

This commit is contained in:
Kevin Papst
2021-10-18 11:12:46 +02:00
committed by GitHub
parent 64893e0a95
commit 9ab098af86
105 changed files with 3356 additions and 778 deletions

View File

@@ -65,7 +65,15 @@ class DurationStringToSecondsTransformer implements DataTransformerInterface
}
try {
return $this->formatter->parseDurationString($formatToInt);
$seconds = $this->formatter->parseDurationString($formatToInt);
// DateTime throws if a duration with too many seconds is passed and an amount of so
// many seconds is likely not required in a time-tracking application ;-)
if ($seconds > 315360000000000) {
throw new TransformationFailedException('Maximum duration exceeded.');
}
return $seconds;
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage());
}

View File

@@ -68,13 +68,23 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
$apiData['route_params'] = [];
}
$formPrefix = $form->getParent()->getName();
if (!empty($formPrefix)) {
$formPrefix .= '_';
$formPrefixes = [];
$parent = $form->getParent();
do {
$formPrefixes[] = $parent->getName();
} while (($parent = $parent->getParent()) !== null);
$formPrefix = implode('_', array_reverse($formPrefixes));
$formField = $formPrefix;
if (!empty($formField)) {
$formField .= '_';
}
$formField .= $apiData['select'];
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-related-select' => $formPrefix . $apiData['select'],
'data-form-prefix' => $formPrefix,
'data-related-select' => $formField,
'data-api-url' => $this->router->generate($apiData['route'], $apiData['route_params']),
]);

View File

@@ -17,11 +17,7 @@ use App\Form\Type\CustomerType;
use App\Form\Type\DescriptionType;
use App\Form\Type\ProjectType;
use App\Form\Type\TagsType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
@@ -29,18 +25,16 @@ use Symfony\Component\Form\FormEvents;
/**
* Helper functions to manage dependent customer-project-activity fields.
*
* If you always want to show the list of all available projects/activities, use the form types directly.
*/
trait FormTrait
{
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null)
{
$builder->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'query_builder_for_user' => true,
'customers' => $customer,
'data' => $customer ? $customer : '',
'required' => false,
'placeholder' => '',
@@ -49,30 +43,29 @@ trait FormTrait
]);
}
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null)
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null, array $options = [])
{
$builder->add('project', ProjectType::class, [
$options = array_merge([
'placeholder' => '',
'activity_enabled' => true,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
'query_builder_for_user' => true,
'join_customer' => true
], $options);
return $repo->getQueryBuilderForFormType($query);
},
]);
$builder->add('project', ProjectType::class, array_merge($options, [
'projects' => $project,
'customers' => $customer,
]));
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer, $isNew) {
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options) {
$data = $event->getData();
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
$event->getForm()->add('project', ProjectType::class, [
'placeholder' => '',
'activity_enabled' => true,
$event->getForm()->add('project', ProjectType::class, array_merge($options, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
// is there a better wa to prevent starting a record with a hidden project ?
@@ -90,44 +83,36 @@ trait FormTrait
}
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setWithCustomer(true);
return $repo->getQueryBuilderForFormType($query);
},
]);
]));
}
);
}
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null)
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null, array $options = [])
{
$builder->add('activity', ActivityType::class, [
'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) {
$query = new ActivityFormTypeQuery($activity, $project);
$query->setUser($builder->getOption('user'));
$options = array_merge(['placeholder' => '', 'query_builder_for_user' => true], $options);
return $repo->getQueryBuilderForFormType($query);
},
]);
$options['projects'] = $project;
$options['activities'] = $activity;
$builder->add('activity', ActivityType::class, $options);
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $activity) {
function (FormEvent $event) use ($options) {
$data = $event->getData();
if (!isset($data['project']) || empty($data['project'])) {
return;
}
$event->getForm()->add('activity', ActivityType::class, [
'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($builder, $data, $activity) {
$query = new ActivityFormTypeQuery($activity, $data['project']);
$query->setUser($builder->getOption('user'));
$options['projects'] = $data['project'];
return $repo->getQueryBuilderForFormType($query);
},
]);
$event->getForm()->add('activity', ActivityType::class, $options);
}
);
}

View File

@@ -18,12 +18,7 @@ use App\Form\Type\ProjectType;
use App\Form\Type\TagsType;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Repository\TimesheetRepository;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Form\AbstractType;
@@ -83,12 +78,8 @@ class TimesheetMultiUpdate extends AbstractType
$builder
->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'query_builder_for_user' => true,
'customers' => $customer,
'data' => $customer ? $customer : '',
'required' => false,
'placeholder' => '',
@@ -103,28 +94,20 @@ class TimesheetMultiUpdate extends AbstractType
$projectOptions['group_by'] = null;
}
$builder
->add(
'project',
ProjectType::class,
array_merge($projectOptions, [
'required' => false,
'placeholder' => '',
'activity_enabled' => true,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
])
);
$builder->add('project', ProjectType::class, array_merge($projectOptions, [
'required' => false,
'placeholder' => '',
'activity_enabled' => true,
'customers' => $customer,
'projects' => $project,
'query_builder_for_user' => true,
]));
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
// TODO replace me with FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer) {
function (FormEvent $event) use ($project, $customer) {
$data = $event->getData();
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
@@ -134,45 +117,37 @@ class TimesheetMultiUpdate extends AbstractType
'placeholder' => '',
'activity_enabled' => true,
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'customers' => $customer,
'projects' => $project,
'query_builder_for_user' => true,
]);
}
);
$builder
->add('activity', ActivityType::class, [
'required' => false,
'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
// TODO respect user (team permission)
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $project));
},
])
;
$activityOptions = [
'required' => false,
'placeholder' => '',
'activities' => $activity,
'query_builder_for_user' => true,
];
$builder->add('activity', ActivityType::class, array_merge($activityOptions, [
'projects' => $project,
]));
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
// TODO replace me with FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($activity) {
function (FormEvent $event) use ($activityOptions) {
$data = $event->getData();
if (!isset($data['project']) || empty($data['project'])) {
return;
}
$event->getForm()->add('activity', ActivityType::class, [
'required' => false,
'placeholder' => '',
'query_builder' => function (ActivityRepository $repo) use ($data, $activity) {
// TODO respect user (team permission)
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $data['project']));
},
]);
$event->getForm()->add('activity', ActivityType::class, array_merge($activityOptions, [
'projects' => $data['project'],
]));
}
);

View File

@@ -13,8 +13,6 @@ use App\Entity\Customer;
use App\Entity\Project;
use App\Form\Type\CustomerType;
use App\Form\Type\DateTimePickerType;
use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -90,12 +88,8 @@ class ProjectEditForm extends AbstractType
]))
->add('customer', CustomerType::class, [
'placeholder' => (null === $id && null === $customer) ? '' : false,
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'customers' => $customer,
'query_builder_for_user' => true,
]);
$this->addCommonFields($builder, $options);

View File

@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Configuration\SystemConfiguration;
use App\Form\Type\QuickEntryWeekType;
use App\Form\Type\WeekPickerType;
use App\Model\QuickEntryWeek;
use App\Validator\Constraints\QuickEntryModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Valid;
class QuickEntryForm extends AbstractType
{
private $configuration;
public function __construct(SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$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'],
'start_date' => $options['start_date'],
'label' => false,
]);
$builder->add('rows', CollectionType::class, [
'label' => false,
'entry_type' => QuickEntryWeekType::class,
'entry_options' => [
'label' => false,
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
'start_date' => $startDate,
'empty_data' => function (FormInterface $form) use ($options) {
return clone $options['prototype_data'];
},
'prototype_data' => clone $options['prototype_data'],
],
'prototype_data' => $options['prototype_data'],
'allow_add' => true,
'constraints' => [
new Valid(),
new All(['constraints' => [new QuickEntryModel()]])
],
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$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(),
'prototype_data' => null,
]);
}
}

View File

@@ -14,6 +14,7 @@ use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -78,10 +79,30 @@ class ActivityType extends AbstractType
'choice_label' => [$this, 'choiceLabel'],
'group_by' => [$this, 'groupBy'],
'choice_attr' => [$this, 'choiceAttr'],
'query_builder' => function (ActivityRepository $repo) {
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery());
},
'query_builder_for_user' => true,
// @var Project|Project[]|int|int[]|null
'projects' => null,
// @var Activity|Activity[]|int|int[]|null
'activities' => null,
// @var Activity|null
'ignore_activity' => null,
]);
$resolver->setDefault('query_builder', function (Options $options) {
return function (ActivityRepository $repo) use ($options) {
$query = new ActivityFormTypeQuery($options['activities'], $options['projects']);
if (true === $options['query_builder_for_user']) {
$query->setUser($options['user']);
}
if (null !== $options['ignore_activity']) {
$query->setActivityToIgnore($options['ignore_activity']);
}
return $repo->getQueryBuilderForFormType($query);
};
});
}
/**

View File

@@ -44,15 +44,23 @@ class CustomerType extends AbstractType
'end_date_param' => '%end%',
'ignore_date' => false,
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
// @var Customer|null
'ignore_customer' => null,
// @var Customer|Customer[]|null
'customers' => null,
]);
$resolver->setDefault('query_builder', function (Options $options) {
return function (CustomerRepository $repo) use ($options) {
$query = new CustomerFormTypeQuery();
$query = new CustomerFormTypeQuery($options['customers']);
if (true === $options['query_builder_for_user']) {
$query->setUser($options['user']);
}
if (null !== $options['ignore_customer']) {
$query->setCustomerToIgnore($options['ignore_customer']);
}
return $repo->getQueryBuilderForFormType($query);
};
});

View File

@@ -38,6 +38,12 @@ class DurationType extends AbstractType
public function buildView(FormView $view, FormInterface $form, array $options)
{
$class = 'duration-input';
if (isset($view->vars['attr']['class'])) {
$class .= ' ' . $view->vars['attr']['class'];
}
$view->vars['attr']['class'] = $class;
if ($options['preset_hours'] === null || $options['preset_minutes'] === null) {
return;
}

View File

@@ -29,6 +29,7 @@ class InitialViewType extends AbstractType
'dashboard' => 'menu.homepage',
'timesheet' => 'menu.timesheet',
'calendar' => 'calendar.title',
'quick_entry' => 'quick_entry.title',
'my_profile' => 'profile.title',
'admin_timesheet' => 'menu.admin_timesheet',
'invoice' => 'menu.invoice',
@@ -49,6 +50,7 @@ class InitialViewType extends AbstractType
'admin_customer' => 'view_customer',
'admin_project' => 'view_project',
'admin_activity' => 'view_activity',
'quick_entry' => 'view_own_timesheet',
];
private $voter;

View File

@@ -68,21 +68,44 @@ class ProjectType extends AbstractType
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
'ignore_date' => false,
'join_customer' => false,
// @var Project|null
'ignore_project' => null,
// @var Customer|Customer[]|int|int[]|null
'customers' => null,
// @var Project|Project[]|int|int[]|null
'projects' => null,
// @var DateTime|null
'project_date_start' => null,
// @var DateTime|null
'project_date_end' => null,
]);
$resolver->setDefault('query_builder', function (Options $options) {
return function (ProjectRepository $repo) use ($options) {
$query = new ProjectFormTypeQuery();
$query = new ProjectFormTypeQuery($options['projects'], $options['customers']);
if (true === $options['query_builder_for_user']) {
$query->setUser($options['user']);
}
if (true === $options['ignore_date']) {
$query->setIgnoreDate(true);
} else {
if ($options['project_date_start'] !== null) {
$query->setProjectStart($options['project_date_start']);
}
if ($options['project_date_end'] !== null) {
$query->setProjectEnd($options['project_date_end']);
}
}
if (true === $options['join_customer']) {
$query->setWithCustomer(true);
}
if (null !== $options['ignore_project']) {
$query->setProjectToIgnore($options['ignore_project']);
}
return $repo->getQueryBuilderForFormType($query);
};
});

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Entity\Timesheet;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
class QuickEntryTimesheetType extends AbstractType
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$durationOptions = [
'label' => false,
'required' => false,
'attr' => [
'placeholder' => '0:00',
],
];
$duration = $options['duration_minutes'];
if ($duration !== null && (int) $duration > 0) {
$durationOptions = array_merge($durationOptions, [
'preset_minutes' => $duration
]);
}
$duration = $options['duration_hours'];
if ($duration !== null && (int) $duration > 0) {
$durationOptions = array_merge($durationOptions, [
'preset_hours' => $duration,
]);
}
$builder->add('duration', DurationType::class, $durationOptions);
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) use ($durationOptions) {
/** @var Timesheet|null $data */
$data = $event->getData();
if (null === $data || null === $data->getEnd()) {
$event->getForm()->get('duration')->setData(null);
}
if (null !== $data && !$this->security->isGranted('edit', $data)) {
$event->getForm()->add('duration', DurationType::class, array_merge(['disabled' => true], $durationOptions));
}
}
);
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
/** @var Timesheet $data */
$data = $event->getData();
$duration = $data->getDuration(false);
try {
if (null !== $duration) {
$end = clone $data->getBegin();
$end->modify('+ ' . $duration . ' seconds');
$data->setEnd($end);
} else {
$data->setDuration(null);
}
} catch (\Exception $e) {
$event->getForm()->addError(new FormError($e->getMessage()));
}
}
);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Timesheet::class,
'timezone' => date_default_timezone_get(),
'duration_minutes' => null,
'duration_hours' => 10,
]);
}
}

View File

@@ -0,0 +1,180 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Model\QuickEntryModel;
use App\Validator\Constraints\QuickEntryTimesheet;
use DateTime;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Valid;
class QuickEntryWeekType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$projectOptions = [
'label' => false,
'required' => false,
'join_customer' => true,
'query_builder_for_user' => true,
'placeholder' => '',
'activity_enabled' => true
];
$builder->add('project', ProjectType::class, $projectOptions);
$projectFunction = function (FormEvent $event) use ($projectOptions) {
/** @var QuickEntryModel|null $data */
$data = $event->getData();
if ($data === null || $data->getProject() === null) {
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);
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, $projectFunction);
$activityOptions = [
'label' => false,
'required' => false,
'placeholder' => '',
'query_builder_for_user' => true,
];
$builder->add('activity', ActivityType::class, $activityOptions);
$activityFunction = function (FormEvent $event) use ($activityOptions) {
/** @var QuickEntryModel|null $data */
$data = $event->getData();
if ($data === null || $data->getActivity() === null) {
return;
}
$activityOptions['activities'] = [$data->getActivity()];
$activityOptions['projects'] = [$data->getProject()];
$event->getForm()->add('activity', ActivityType::class, $activityOptions);
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, $activityFunction);
$builder->add('timesheets', CollectionType::class, [
'entry_type' => QuickEntryTimesheetType::class,
'label' => false,
'entry_options' => [
'label' => false,
'compound' => true,
'timezone' => $options['timezone'],
'duration_minutes' => $options['duration_minutes'],
'duration_hours' => $options['duration_hours'],
],
'allow_add' => true,
'constraints' => [
new Valid(),
new All(['constraints' => [new QuickEntryTimesheet()]])
],
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
if ($event->getData() === null) {
$event->setData(clone $options['prototype_data']);
}
});
$builder->addModelTransformer(new CallbackTransformer(
function ($transformValue) use ($options) {
/** @var QuickEntryModel|null $transformValue */
if ($transformValue === null || $transformValue->isPrototype()) {
return $transformValue;
}
$project = $transformValue->getProject();
$activity = $transformValue->getActivity();
// this case needs to be handled by the validator
if ($project === null || $activity === null) {
return $transformValue;
}
foreach ($transformValue->getTimesheets() as $timesheet) {
$timesheet->setUser($transformValue->getUser() ?? $options['user']);
$timesheet->setProject($project);
$timesheet->setActivity($activity);
}
return $transformValue;
},
function ($reverseTransformValue) {
return $reverseTransformValue;
}
));
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
/** @var QuickEntryModel $data */
$data = $event->getData();
$newRecords = $data->getNewTimesheet();
$user = $data->getUser();
$project = $data->getProject();
$activity = $data->getActivity();
foreach ($newRecords as $record) {
if ($user !== null) {
$record->setUser($user);
}
if ($project !== null) {
$record->setProject($project);
}
if ($activity !== null) {
$record->setActivity($activity);
}
}
}
);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => QuickEntryModel::class,
'timezone' => date_default_timezone_get(),
'duration_minutes' => null,
'duration_hours' => 10,
'start_date' => new DateTime(),
'prototype_data' => null,
]);
}
}