Release 2.0.11 (#3932)
- added "today" as selector in date-range dropdown - added feature to prevent auto-select of dropdowns with only one entry - added hint that no changes were detected in batch update - added negative invoice sums are possible (e.g. for credit notes) - fix project list is expanded after submission - fix invalid date parsing causes 500 - fix: prevent auto-select of activities in export and invoice form (in case only one global activity exists) - fix team assignments for customer and project were not saved (using API now) - fix form fieldset with legend styling (e.g. team project assignment) - fix required meta-field were forced to have a value in batch update - fix tomselect meta-field was not disabled in batch update - fix unset internal rate is shown as 0 - fix one minute rounding problem in duration-only mode with "now" being default time - fix column width and label for duration-only mode - tech debt: cleanup invoice template (remove invoice layout) - tech debt: reorder for simpler comparison with invoice form - possible BC for devs: remove unused methods from form trait - bump composer packages (includes new translations for auth screens)
This commit is contained in:
@@ -15,6 +15,9 @@ use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Form\API\TeamApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TeamRepository;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
@@ -230,15 +233,14 @@ final class TeamController extends BaseApiController
|
||||
#[Rest\Post(path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function postCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
|
||||
public function postCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
if ($team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Team has already access to customer');
|
||||
}
|
||||
|
||||
$team->addCustomer($customer);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$customerRepository->saveCustomer($customer);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -256,15 +258,14 @@ final class TeamController extends BaseApiController
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/customers/{customerId}', name: 'delete_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
|
||||
public function deleteCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
|
||||
public function deleteCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
if (!$team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Customer is not assigned to the team');
|
||||
}
|
||||
|
||||
$team->removeCustomer($customer);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$customerRepository->saveCustomer($customer);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -282,15 +283,14 @@ final class TeamController extends BaseApiController
|
||||
#[Rest\Post(path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function postProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
|
||||
public function postProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
if ($team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Team has already access to project');
|
||||
}
|
||||
|
||||
$team->addProject($project);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$projectRepository->saveProject($project);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -308,15 +308,14 @@ final class TeamController extends BaseApiController
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/projects/{projectId}', name: 'delete_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
|
||||
public function deleteProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
|
||||
public function deleteProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
if (!$team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Project is not assigned to the team');
|
||||
}
|
||||
|
||||
$team->removeProject($project);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$projectRepository->saveProject($project);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -334,15 +333,14 @@ final class TeamController extends BaseApiController
|
||||
#[Rest\Post(path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
public function postActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
|
||||
public function postActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity, ActivityRepository $activityRepository): Response
|
||||
{
|
||||
if ($team->hasActivity($activity)) {
|
||||
throw new BadRequestHttpException('Team has already access to activity');
|
||||
}
|
||||
|
||||
$team->addActivity($activity);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$activityRepository->saveActivity($activity);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -360,15 +358,14 @@ final class TeamController extends BaseApiController
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[Rest\Delete(path: '/{id}/activities/{activityId}', name: 'delete_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
|
||||
public function deleteActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
|
||||
public function deleteActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity, ActivityRepository $activityRepository): Response
|
||||
{
|
||||
if (!$team->hasActivity($activity)) {
|
||||
throw new BadRequestHttpException('Activity is not assigned to the team');
|
||||
}
|
||||
|
||||
$team->removeActivity($activity);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$activityRepository->saveActivity($activity);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.10';
|
||||
public const VERSION = '2.0.11';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20010;
|
||||
public const VERSION_ID = 20011;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -68,7 +68,7 @@ final class CalendarController extends AbstractController
|
||||
$defaultStart = null;
|
||||
if ($this->configuration->getTimesheetDefaultBeginTime() !== 'now') {
|
||||
$defaultStart = $factory->createDateTime($this->configuration->getTimesheetDefaultBeginTime());
|
||||
$defaultStart = $defaultStart->format('h:i:s');
|
||||
$defaultStart = $defaultStart->format('H:i:s');
|
||||
}
|
||||
|
||||
$config = $this->calendarService->getConfiguration();
|
||||
|
||||
@@ -36,10 +36,17 @@ final class QuickEntryController extends AbstractController
|
||||
public function quickEntry(Request $request, ?string $begin = null)
|
||||
{
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
if ($begin !== null) {
|
||||
try {
|
||||
$begin = $factory->createDateTime($begin);
|
||||
} catch (\Exception $ex) {
|
||||
$begin = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($begin === null) {
|
||||
$begin = $factory->createDateTime();
|
||||
} else {
|
||||
$begin = $factory->createDateTime($begin);
|
||||
}
|
||||
|
||||
$startWeek = $factory->getStartOfWeek($begin);
|
||||
|
||||
@@ -77,7 +77,7 @@ final class TagController extends AbstractController
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'tags_edit', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function editAction(Tag $tag, TagRepository $repository, Request $request)
|
||||
public function editAction(Tag $tag, TagRepository $repository, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createForm(TagEditForm::class, $tag, [
|
||||
'action' => $this->generateUrl('tags_edit', ['id' => $tag->getId()]),
|
||||
@@ -109,7 +109,7 @@ final class TagController extends AbstractController
|
||||
|
||||
#[Route(path: '/create', name: 'tags_create', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function createAction(TagRepository $repository, Request $request)
|
||||
public function createAction(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
$tag = new Tag();
|
||||
|
||||
@@ -143,7 +143,7 @@ final class TagController extends AbstractController
|
||||
|
||||
#[Route(path: '/multi-delete', name: 'tags_multi_delete', methods: ['POST'])]
|
||||
#[IsGranted('delete_tag')]
|
||||
public function multiDelete(TagRepository $repository, Request $request)
|
||||
public function multiDelete(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -10,14 +10,15 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\TeamCustomerForm;
|
||||
use App\Form\TeamEditForm;
|
||||
use App\Form\TeamProjectForm;
|
||||
use App\Form\Toolbar\TeamToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -90,7 +91,7 @@ final class TeamController extends AbstractController
|
||||
#[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function duplicateTeam(Team $team, Request $request)
|
||||
public function duplicateTeam(Team $team, Request $request): Response
|
||||
{
|
||||
$newTeam = clone $team;
|
||||
|
||||
@@ -105,14 +106,14 @@ final class TeamController extends AbstractController
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_team_edit', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function editAction(Team $team, Request $request)
|
||||
public function editAction(Team $team, Request $request): Response
|
||||
{
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit_member', name: 'admin_team_member', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('edit', 'team')]
|
||||
public function editMemberAction(Team $team, Request $request)
|
||||
public function editMemberAction(Team $team, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
'action' => $this->generateUrl('admin_team_member', ['id' => $team->getId()]),
|
||||
@@ -176,37 +177,21 @@ final class TeamController extends AbstractController
|
||||
}
|
||||
|
||||
if (null !== $team->getId()) {
|
||||
$customerForm = $this->createForm(TeamCustomerForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
$customerForm->handleRequest($request);
|
||||
$customerForm = $this->createFormWithName('team_customer_form', FormType::class, $team)
|
||||
->add('customers', CustomerType::class, [
|
||||
'label' => false,
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'query_builder_for_user' => false,
|
||||
]);
|
||||
|
||||
if ($customerForm->isSubmitted() && $customerForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
$projectForm = $this->createForm(TeamProjectForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
$projectForm->handleRequest($request);
|
||||
|
||||
if ($projectForm->isSubmitted() && $projectForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
$projectForm = $this->createFormWithName('team_project_form', FormType::class, $team)
|
||||
->add('projects', ProjectType::class, [
|
||||
'label' => false,
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'query_builder_for_user' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
|
||||
@@ -280,15 +280,19 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function multiUpdate(Request $request)
|
||||
protected function multiUpdate(Request $request): Response
|
||||
{
|
||||
$dto = new TimesheetMultiUpdateDTO();
|
||||
|
||||
// initial request from the listing posts a different form
|
||||
$form = $this->getMultiUpdateActionForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$dto->setEntities($form->getData()->getEntities());
|
||||
$data = $form->getData();
|
||||
if ($data instanceof MultiUpdateTableDTO) {
|
||||
$dto->setEntities($data->getEntities());
|
||||
}
|
||||
}
|
||||
|
||||
// using a new timesheet to make sure we ONLY use meta-fields which are registered via events
|
||||
@@ -321,14 +325,14 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
$dto->setEntities($timesheets);
|
||||
|
||||
if (\count($dto->getEntities()) === 0) {
|
||||
if (\count($timesheets) === 0) {
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var Timesheet $timesheet */
|
||||
$execute = false;
|
||||
foreach ($dto->getEntities() as $timesheet) {
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
if ($dto->isReplaceTags()) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$timesheet->removeTag($tag);
|
||||
@@ -391,13 +395,17 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
if ($execute) {
|
||||
try {
|
||||
$this->service->updateMultipleTimesheets($dto->getEntities());
|
||||
$this->service->updateMultipleTimesheets($timesheets);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
} else {
|
||||
$this->flashSuccess(sprintf('No changes for %s entries detected.', \count($timesheets)));
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ use Doctrine\Common\Collections\Collection;
|
||||
class MultiUpdateTableDTO
|
||||
{
|
||||
/**
|
||||
* @var iterable<object>|Collection<object>
|
||||
* @var array<object>|Collection<object>
|
||||
*/
|
||||
private iterable|Collection $entities = [];
|
||||
private array|Collection $entities = [];
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
@@ -26,16 +26,16 @@ class MultiUpdateTableDTO
|
||||
/**
|
||||
* @return object[]
|
||||
*/
|
||||
public function getEntities(): iterable|Collection
|
||||
public function getEntities(): array|Collection
|
||||
{
|
||||
return $this->entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<object>|Collection<object> $entities
|
||||
* @param array<object>|Collection<object> $entities
|
||||
* @return MultiUpdateTableDTO
|
||||
*/
|
||||
public function setEntities(iterable|Collection $entities): MultiUpdateTableDTO
|
||||
public function setEntities(array|Collection $entities): MultiUpdateTableDTO
|
||||
{
|
||||
$this->entities = $entities;
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ final class TimesheetMultiUpdate extends AbstractType
|
||||
|
||||
// meta fields only if at least one exists
|
||||
if ($entry !== null && $entry->getMetaFields()->count() > 0) {
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class, ['fields_required' => false]);
|
||||
|
||||
$choices = [];
|
||||
foreach ($entry->getMetaFields() as $field) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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\Entity\Team;
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class TeamCustomerForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('customers', CustomerType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_customer',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?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\Entity\Team;
|
||||
use App\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class TeamProjectForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('projects', ProjectType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'by_reference' => false,
|
||||
'attr' => ['size' => '20'],
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_project',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -28,15 +28,15 @@ final class ExportToolbarForm extends AbstractType
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
|
||||
$this->addActivityMultiChoice($builder, [], true);
|
||||
$this->addActivitySelect($builder, [], true, true, false);
|
||||
$this->addTagInputField($builder);
|
||||
if ($options['include_user']) {
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addTeamsChoice($builder);
|
||||
}
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addBillableChoice($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$builder->add('renderer', HiddenType::class, []);
|
||||
if ($options['include_export']) {
|
||||
$builder->add('markAsExported', HiddenType::class, [
|
||||
|
||||
@@ -25,21 +25,21 @@ final class InvoiceToolbarForm extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
|
||||
$this->addActivitySelect($builder, [], true, true, false);
|
||||
$this->addTagInputField($builder);
|
||||
if ($options['include_user']) {
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addTeamsChoice($builder);
|
||||
}
|
||||
$this->addActivityMultiChoice($builder, $options, true);
|
||||
$this->addTagInputField($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$builder->add('invoiceDate', DatePickerType::class, [
|
||||
'required' => true,
|
||||
]);
|
||||
$this->addTemplateChoice($builder);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder): void
|
||||
|
||||
@@ -45,14 +45,6 @@ use Symfony\Component\Form\FormEvents;
|
||||
*/
|
||||
trait ToolbarFormTrait
|
||||
{
|
||||
protected function addUserChoice(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
'label' => 'user',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addUsersChoice(FormBuilderInterface $builder, string $field = 'users', array $options = []): void
|
||||
{
|
||||
$builder->add($field, UserType::class, array_merge([
|
||||
@@ -67,14 +59,6 @@ trait ToolbarFormTrait
|
||||
], $options));
|
||||
}
|
||||
|
||||
protected function addTeamChoice(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('team', TeamType::class, [
|
||||
'label' => 'team',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTeamsChoice(FormBuilderInterface $builder, string $field = 'teams', array $options = []): void
|
||||
{
|
||||
$builder->add($field, TeamType::class, array_merge([
|
||||
@@ -89,11 +73,6 @@ trait ToolbarFormTrait
|
||||
], $options));
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
|
||||
{
|
||||
$this->addCustomerSelect($builder, $options, false, $multiProject);
|
||||
}
|
||||
|
||||
protected function addCustomerMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
|
||||
{
|
||||
$this->addCustomerSelect($builder, $options, true, $multiProject);
|
||||
@@ -190,16 +169,6 @@ trait ToolbarFormTrait
|
||||
$builder->add('daterange', DateRangeType::class, $params);
|
||||
}
|
||||
|
||||
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true, $required = false): void
|
||||
{
|
||||
$this->addDateRange($builder, [], $allowEmpty, $required);
|
||||
}
|
||||
|
||||
protected function addProjectChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
|
||||
{
|
||||
$this->addProjectSelect($builder, $options, false, $multiCustomer, $multiActivity);
|
||||
}
|
||||
|
||||
protected function addProjectMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
|
||||
{
|
||||
$this->addProjectSelect($builder, $options, true, $multiCustomer, $multiActivity);
|
||||
@@ -271,46 +240,45 @@ trait ToolbarFormTrait
|
||||
);
|
||||
}
|
||||
|
||||
protected function addActivityChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
|
||||
{
|
||||
$this->addActivitySelect($builder, $options, false, $multiProject);
|
||||
}
|
||||
|
||||
protected function addActivityMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
|
||||
{
|
||||
$this->addActivitySelect($builder, $options, true, $multiProject);
|
||||
}
|
||||
|
||||
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false): void
|
||||
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false, bool $autoFill = true): void
|
||||
{
|
||||
$name = 'activity';
|
||||
if ($multiActivity) {
|
||||
$name = 'activities';
|
||||
}
|
||||
$name = $multiActivity ? 'activities' : 'activity';
|
||||
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
$builder->add($name, ActivityType::class, [
|
||||
$activityOptions = [
|
||||
'required' => false,
|
||||
'documentation' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'integer', 'description' => 'Activity ID'],
|
||||
'description' => 'Array of activity IDs',
|
||||
],
|
||||
'choices' => [],
|
||||
'multiple' => $multiActivity,
|
||||
]);
|
||||
];
|
||||
|
||||
if (!$autoFill) {
|
||||
$activityOptions['attr'] = [
|
||||
'data-autoselect' => 'false'
|
||||
];
|
||||
}
|
||||
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
$builder->add($name, ActivityType::class, array_merge($activityOptions, [
|
||||
'choices' => [],
|
||||
]));
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($name, $multiActivity, $multiProject) {
|
||||
function (FormEvent $event) use ($name, $multiProject, $activityOptions) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add($name, ActivityType::class, [
|
||||
'multiple' => $multiActivity,
|
||||
'required' => false,
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data, $multiActivity, $multiProject) {
|
||||
$event->getForm()->add($name, ActivityType::class, array_merge($activityOptions, [
|
||||
'query_builder' => function (ActivityRepository $repo) use ($name, $data, $multiProject) {
|
||||
$query = new ActivityFormTypeQuery();
|
||||
|
||||
$name = $multiActivity ? 'activities' : 'activity';
|
||||
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
|
||||
// we need to pre-fetch the activities to see if they are global, see ActivityFormTypeQuery::isGlobalsOnly()
|
||||
$activities = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
|
||||
@@ -323,9 +291,9 @@ trait ToolbarFormTrait
|
||||
}
|
||||
}
|
||||
|
||||
$name = $multiProject ? 'projects' : 'project';
|
||||
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
|
||||
$projects = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
|
||||
$projectName = $multiProject ? 'projects' : 'project';
|
||||
if (\array_key_exists($projectName, $data) && $data[$projectName] !== null && $data[$projectName] !== '') {
|
||||
$projects = \is_array($data[$projectName]) ? $data[$projectName] : [$data[$projectName]];
|
||||
foreach ($projects as $project) {
|
||||
$project = \is_string($project) ? (int) $project : $project;
|
||||
if (!\is_int($project) && !($project instanceof Project)) {
|
||||
@@ -337,7 +305,7 @@ trait ToolbarFormTrait
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ final class DateRangeType extends AbstractType
|
||||
$factory = DateTimeFactory::createByUser($user);
|
||||
|
||||
$view->vars['ranges'] = [
|
||||
'today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
|
||||
'yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
|
||||
'thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
|
||||
'lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
|
||||
|
||||
@@ -27,7 +27,7 @@ final class MetaFieldsCollectionType extends AbstractType
|
||||
{
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($options) {
|
||||
/** @var ArrayCollection<MetaTableTypeInterface> $collection */
|
||||
$collection = $event->getData();
|
||||
foreach ($collection as $collectionItem) {
|
||||
@@ -42,6 +42,11 @@ final class MetaFieldsCollectionType extends AbstractType
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($options['fields_required'] !== null) {
|
||||
// TODO required select-fields can receive an empty value
|
||||
$collectionItem->setIsRequired((bool) $options['fields_required']);
|
||||
}
|
||||
|
||||
$collection->set($collectionItem->getName(), $collectionItem);
|
||||
}
|
||||
},
|
||||
@@ -57,8 +62,11 @@ final class MetaFieldsCollectionType extends AbstractType
|
||||
'entry_options' => ['label' => false],
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
'fields_required' => null,
|
||||
'label' => false,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('fields_required', ['null', 'bool']);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
|
||||
@@ -532,10 +532,6 @@ final class ServiceInvoice
|
||||
$model->addEntries($settings['entries']);
|
||||
$this->prepareModelQueryDates($model);
|
||||
|
||||
if ($model->getCalculator()->getTotal() < 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$models[] = $model;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,12 @@ final class DurationFixedBeginMode implements TrackingModeInterface
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
|
||||
$newBegin = clone $timesheet->getBegin();
|
||||
/** @var DateTime $newBegin */
|
||||
$newBegin = clone $timesheet->getBegin(); // @phpstan-ignore-line
|
||||
|
||||
// this prevents the problem that "now" is being ignored in modify()
|
||||
$beginTime = (new DateTime($this->configuration->getTimesheetDefaultBeginTime(), $newBegin->getTimezone()))->format('H:i:s');
|
||||
$newBegin->modify($beginTime);
|
||||
$beginTime = new DateTime($this->configuration->getTimesheetDefaultBeginTime(), $newBegin->getTimezone());
|
||||
$newBegin->setTime((int) $beginTime->format('H'), (int) $beginTime->format('i'), 0, 0);
|
||||
|
||||
$timesheet->setBegin($newBegin);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user