weekly quick-entry form (#2793)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
89
src/Form/QuickEntryForm.php
Normal file
89
src/Form/QuickEntryForm.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
108
src/Form/Type/QuickEntryTimesheetType.php
Normal file
108
src/Form/Type/QuickEntryTimesheetType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
180
src/Form/Type/QuickEntryWeekType.php
Normal file
180
src/Form/Type/QuickEntryWeekType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user