added drag and drop for new records via calendar (#1962)
This commit is contained in:
66
src/API/Serializer/ValidationFailedExceptionErrorHandler.php
Normal file
66
src/API/Serializer/ValidationFailedExceptionErrorHandler.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\API\Serializer;
|
||||
|
||||
use App\Validator\ValidationFailedException;
|
||||
use JMS\Serializer\GraphNavigatorInterface;
|
||||
use JMS\Serializer\Handler\SubscribingHandlerInterface;
|
||||
use JMS\Serializer\Visitor\SerializationVisitorInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ValidationFailedExceptionErrorHandler implements SubscribingHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public static function getSubscribingMethods()
|
||||
{
|
||||
return [[
|
||||
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
|
||||
'type' => ValidationFailedException::class,
|
||||
'format' => 'json',
|
||||
'method' => 'serializeExceptionToJson',
|
||||
]];
|
||||
}
|
||||
|
||||
public function serializeExceptionToJson(SerializationVisitorInterface $visitor, ValidationFailedException $exception, array $type)
|
||||
{
|
||||
$errors = [];
|
||||
/** @var ConstraintViolationInterface $error */
|
||||
foreach (iterator_to_array($exception->getViolations()) as $error) {
|
||||
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => '400',
|
||||
'message' => $this->translator->trans($exception->getMessage(), [], 'validators'),
|
||||
'errors' => [
|
||||
'children' => $errors
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getErrorMessage(ConstraintViolationInterface $error): string
|
||||
{
|
||||
if (null !== $error->getPlural()) {
|
||||
return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators');
|
||||
}
|
||||
|
||||
return $this->translator->trans($error->getMessageTemplate(), $error->getParameters(), 'validators');
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ use App\Form\API\TimesheetApiEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\RoundingService;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
@@ -39,7 +38,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @RouteResource("Timesheet")
|
||||
@@ -50,6 +48,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Not_Expanded'];
|
||||
public const GROUPS_ENTITY_FULL = ['Default', 'Entity', 'Timesheet', 'Timesheet_Entity', 'Expanded'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'Timesheet', 'Not_Expanded'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Timesheet', 'Not_Expanded'];
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
|
||||
@@ -78,10 +77,6 @@ class TimesheetController extends BaseApiController
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var RoundingService
|
||||
*/
|
||||
private $roundingService;
|
||||
/**
|
||||
* @var TimesheetService
|
||||
*/
|
||||
@@ -94,7 +89,6 @@ class TimesheetController extends BaseApiController
|
||||
TagRepository $tagRepository,
|
||||
TrackingModeService $trackingModeService,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
RoundingService $roundingService,
|
||||
TimesheetService $service
|
||||
) {
|
||||
$this->viewHandler = $viewHandler;
|
||||
@@ -103,7 +97,6 @@ class TimesheetController extends BaseApiController
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->trackingModeService = $trackingModeService;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->roundingService = $roundingService;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
@@ -330,12 +323,14 @@ class TimesheetController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEditForm")
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="full", requirements="true", strict=true, nullable=true, description="Allows to fetch fully serialized objects including subresources (TimesheetEntityExpanded). Allowed values: true (default: false)")
|
||||
*
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postAction(Request $request): Response
|
||||
public function postAction(Request $request, ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
@@ -367,7 +362,12 @@ class TimesheetController extends BaseApiController
|
||||
}
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
if ('true' === $paramFetcher->get('full')) {
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY_FULL);
|
||||
} else {
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
}
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
@@ -626,11 +626,12 @@ class TimesheetController extends BaseApiController
|
||||
* )
|
||||
*
|
||||
* @Rest\RequestParam(name="copy", requirements="all|tags|rates|meta|description", strict=true, nullable=true, description="Whether data should be copied to the new entry. Allowed values: all, tags, rates, description, meta (default: nothing is copied)")
|
||||
* @Rest\RequestParam(name="begin", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Changes the restart date to the given one (default: now)")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function restartAction(int $id, ParamFetcherInterface $paramFetcher, ValidatorInterface $validator): Response
|
||||
public function restartAction(int $id, ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
@@ -647,12 +648,19 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
$copyTimesheet = $this->service->createNewTimesheet($user);
|
||||
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
$begin = $factory->createDateTime();
|
||||
if (null !== ($beginTmp = $paramFetcher->get('begin'))) {
|
||||
$begin = $factory->createDateTime($beginTmp);
|
||||
}
|
||||
|
||||
$copyTimesheet
|
||||
->setBegin($this->getDateTimeFactory()->createDateTime())
|
||||
->setBegin($begin)
|
||||
->setActivity($timesheet->getActivity())
|
||||
->setProject($timesheet->getProject())
|
||||
;
|
||||
$this->roundingService->roundBegin($copyTimesheet);
|
||||
$this->service->prepareNewTimesheet($copyTimesheet);
|
||||
|
||||
if (null !== ($copy = $paramFetcher->get('copy'))) {
|
||||
if (\in_array($copy, ['rates', 'all'])) {
|
||||
@@ -678,13 +686,7 @@ class TimesheetController extends BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
$errors = $validator->validate($copyTimesheet);
|
||||
|
||||
if (\count($errors) > 0) {
|
||||
throw new BadRequestHttpException($errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage());
|
||||
}
|
||||
|
||||
$this->service->saveNewTimesheet($copyTimesheet);
|
||||
$this->service->restartTimesheet($copyTimesheet, $timesheet);
|
||||
|
||||
$view = new View($copyTimesheet, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
41
src/Calendar/DragAndDropEntry.php
Normal file
41
src/Calendar/DragAndDropEntry.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Calendar;
|
||||
|
||||
interface DragAndDropEntry
|
||||
{
|
||||
/**
|
||||
* Data to be passed to the API call.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getData(): array;
|
||||
|
||||
/**
|
||||
* Returns the title for this entry.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Returns the color for this entry.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): string;
|
||||
|
||||
/**
|
||||
* The block to use for rendering the entry.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBlockName(): ?string;
|
||||
}
|
||||
50
src/Calendar/DragAndDropSource.php
Normal file
50
src/Calendar/DragAndDropSource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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\Calendar;
|
||||
|
||||
interface DragAndDropSource
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteReplacer(): array;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod(): string;
|
||||
|
||||
/**
|
||||
* @return DragAndDropEntry[]
|
||||
*/
|
||||
public function getEntries(): array;
|
||||
|
||||
/**
|
||||
* If you want to customize the item rendering, you have to return a path to your include here.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBlockInclude(): ?string;
|
||||
}
|
||||
@@ -9,39 +9,36 @@
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class Google
|
||||
final class Google
|
||||
{
|
||||
/**
|
||||
* @var Source[]
|
||||
* @var GoogleSource[]
|
||||
*/
|
||||
protected $sources = [];
|
||||
private $sources;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $apiKey = null;
|
||||
private $apiKey;
|
||||
|
||||
/**
|
||||
* @param string $apiKey
|
||||
* @param Source[] $sources
|
||||
* @param GoogleSource[] $sources
|
||||
*/
|
||||
public function __construct($apiKey, $sources = [])
|
||||
public function __construct(string $apiKey, array $sources = [])
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
$this->sources = $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
* @return GoogleSource[]
|
||||
*/
|
||||
public function getSources()
|
||||
public function getSources(): array
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiKey()
|
||||
public function getApiKey(): string
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
48
src/Calendar/GoogleSource.php
Normal file
48
src/Calendar/GoogleSource.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Calendar;
|
||||
|
||||
final class GoogleSource
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $uri;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $color;
|
||||
|
||||
public function __construct(string $id, string $uri, ?string $color = null)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->uri = $uri;
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
}
|
||||
70
src/Calendar/RecentActivitiesSource.php
Normal file
70
src/Calendar/RecentActivitiesSource.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\Calendar;
|
||||
|
||||
class RecentActivitiesSource implements DragAndDropSource
|
||||
{
|
||||
/**
|
||||
* @var DragAndDropEntry[]
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* @param DragAndDropEntry[] $entries
|
||||
*/
|
||||
public function __construct(array $entries)
|
||||
{
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'recent.activities';
|
||||
}
|
||||
|
||||
public function getRoute(): string
|
||||
{
|
||||
return 'post_timesheet';
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return 'POST';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return ['full' => 'true'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteReplacer(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DragAndDropEntry[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
public function getBlockInclude(): ?string
|
||||
{
|
||||
return 'calendar/drag-drop.html.twig';
|
||||
}
|
||||
}
|
||||
@@ -1,83 +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\Calendar;
|
||||
|
||||
class Source
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uri;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $color;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return Source
|
||||
*/
|
||||
public function setId(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Source
|
||||
*/
|
||||
public function setUri(string $uri)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return Source
|
||||
*/
|
||||
public function setColor(string $color)
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
75
src/Calendar/TimesheetEntry.php
Normal file
75
src/Calendar/TimesheetEntry.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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\Calendar;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class TimesheetEntry implements DragAndDropEntry
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
private $timesheet;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $color;
|
||||
|
||||
public function __construct(Timesheet $timesheet, string $color)
|
||||
{
|
||||
$this->timesheet = $timesheet;
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return [
|
||||
'description' => $this->timesheet->getDescription(),
|
||||
'activity' => $this->timesheet->getActivity() !== null ? $this->timesheet->getActivity()->getId() : null,
|
||||
'project' => $this->timesheet->getProject() !== null ? $this->timesheet->getProject()->getId() : null,
|
||||
'tags' => implode(',', $this->timesheet->getTagsAsArray()),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
if ($this->timesheet->getActivity() !== null && $this->timesheet->getActivity()->getName() !== null) {
|
||||
return $this->timesheet->getActivity()->getName();
|
||||
}
|
||||
|
||||
if (null !== $this->timesheet->getProject() && $this->timesheet->getProject()->getName() !== null) {
|
||||
return $this->timesheet->getProject()->getName();
|
||||
}
|
||||
|
||||
return $this->timesheet->getDescription() ?? '';
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function getBlockName(): ?string
|
||||
{
|
||||
return 'dd_timesheet';
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->timesheet->getActivity();
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->timesheet->getProject();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @deprecated since 1.11 - use SystemConfiguration instead
|
||||
*/
|
||||
class CalendarConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @internal will be deprecated soon, use SystemConfiguration instead
|
||||
*/
|
||||
class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
|
||||
@@ -22,4 +22,64 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $repository->getConfiguration();
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultBeginTime(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.default_begin');
|
||||
}
|
||||
|
||||
public function getCalendarBusinessDays(): array
|
||||
{
|
||||
return (array) $this->find('calendar.businessHours.days');
|
||||
}
|
||||
|
||||
public function getCalendarBusinessTimeBegin(): string
|
||||
{
|
||||
return (string) $this->find('calendar.businessHours.begin');
|
||||
}
|
||||
|
||||
public function getCalendarBusinessTimeEnd(): string
|
||||
{
|
||||
return (string) $this->find('calendar.businessHours.end');
|
||||
}
|
||||
|
||||
public function getCalendarTimeframeBegin(): string
|
||||
{
|
||||
return (string) $this->find('calendar.visibleHours.begin');
|
||||
}
|
||||
|
||||
public function getCalendarTimeframeEnd(): string
|
||||
{
|
||||
return (string) $this->find('calendar.visibleHours.end');
|
||||
}
|
||||
|
||||
public function getCalendarDayLimit(): int
|
||||
{
|
||||
return (int) $this->find('calendar.day_limit');
|
||||
}
|
||||
|
||||
public function isCalendarShowWeekNumbers(): bool
|
||||
{
|
||||
return (bool) $this->find('calendar.week_numbers');
|
||||
}
|
||||
|
||||
public function isCalendarShowWeekends(): bool
|
||||
{
|
||||
return (bool) $this->find('calendar.weekends');
|
||||
}
|
||||
|
||||
public function getCalendarGoogleApiKey(): ?string
|
||||
{
|
||||
return $this->find('calendar.google.api_key');
|
||||
}
|
||||
|
||||
public function getCalendarGoogleSources(): ?array
|
||||
{
|
||||
return $this->find('calendar.google.sources');
|
||||
}
|
||||
|
||||
public function getCalendarSlotDuration(): string
|
||||
{
|
||||
return (string) $this->find('calendar.slot_duration');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @internal will be deprecated soon, use SystemConfiguration instead
|
||||
*/
|
||||
class ThemeConfiguration implements SystemBundleConfiguration, \ArrayAccess
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @internal will be deprecated soon, use SystemConfiguration instead
|
||||
*/
|
||||
class TimesheetConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
|
||||
@@ -9,12 +9,20 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Calendar\DragAndDropSource;
|
||||
use App\Calendar\Google;
|
||||
use App\Calendar\Source;
|
||||
use App\Configuration\CalendarConfiguration;
|
||||
use App\Calendar\GoogleSource;
|
||||
use App\Calendar\RecentActivitiesSource;
|
||||
use App\Calendar\TimesheetEntry;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Event\CalendarDragAndDropSourceEvent;
|
||||
use App\Event\CalendarGoogleSourceEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
use App\Utils\Color;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Controller used to display calendars.
|
||||
@@ -24,37 +32,109 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
*/
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="calendar", methods={"GET"})
|
||||
*/
|
||||
public function userCalendar(CalendarConfiguration $configuration, TrackingModeService $service)
|
||||
public function userCalendar(SystemConfiguration $configuration, TrackingModeService $service, TimesheetRepository $repository)
|
||||
{
|
||||
$mode = $service->getActiveMode();
|
||||
$factory = $this->getDateTimeFactory();
|
||||
$defaultStart = $factory->createDateTime($configuration->getTimesheetDefaultBeginTime());
|
||||
|
||||
$config = [
|
||||
'dayLimit' => $configuration->getCalendarDayLimit(),
|
||||
'showWeekNumbers' => $configuration->isCalendarShowWeekNumbers(),
|
||||
'showWeekends' => $configuration->isCalendarShowWeekends(),
|
||||
'businessDays' => $configuration->getCalendarBusinessDays(),
|
||||
'businessTimeBegin' => $configuration->getCalendarBusinessTimeBegin(),
|
||||
'businessTimeEnd' => $configuration->getCalendarBusinessTimeEnd(),
|
||||
'slotDuration' => $configuration->getCalendarSlotDuration(),
|
||||
'timeframeBegin' => $configuration->getCalendarTimeframeBegin(),
|
||||
'timeframeEnd' => $configuration->getCalendarTimeframeEnd(),
|
||||
];
|
||||
|
||||
$isPunchMode = !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd();
|
||||
$dragAndDrop = [];
|
||||
|
||||
if ($mode->canEditBegin()) {
|
||||
$dragAndDrop = $this->getDragAndDropResources($repository);
|
||||
}
|
||||
|
||||
return $this->render('calendar/user.html.twig', [
|
||||
'config' => $configuration,
|
||||
'config' => $config,
|
||||
'dragAndDrop' => $dragAndDrop,
|
||||
'google' => $this->getGoogleSources($configuration),
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
'is_punch_mode' => !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd()
|
||||
'now' => $factory->createDateTime(),
|
||||
'defaultStartTime' => $defaultStart->format('h:i:s'),
|
||||
'is_punch_mode' => $isPunchMode,
|
||||
'can_edit_begin' => $mode->canEditBegin(),
|
||||
'can_edit_end' => $mode->canEditBegin(),
|
||||
'can_edit_duration' => $mode->canEditDuration(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Google
|
||||
* @return DragAndDropSource[]
|
||||
*/
|
||||
protected function getGoogleSources(CalendarConfiguration $configuration)
|
||||
private function getDragAndDropResources(TimesheetRepository $repository): array
|
||||
{
|
||||
$apiKey = $configuration->getGoogleApiKey() ?? null;
|
||||
$sources = [];
|
||||
|
||||
foreach ($configuration->getGoogleSources() as $name => $config) {
|
||||
$source = new Source();
|
||||
$source
|
||||
->setColor($config['color'])
|
||||
->setUri($config['id'])
|
||||
->setId($name)
|
||||
;
|
||||
try {
|
||||
$data = $repository->getRecentActivities(
|
||||
$this->getUser(),
|
||||
$this->getDateTimeFactory()->createDateTime('-1 year'),
|
||||
10
|
||||
);
|
||||
|
||||
$entries = [];
|
||||
$colorHelper = new Color();
|
||||
foreach ($data as $timesheet) {
|
||||
$entries[] = new TimesheetEntry($timesheet, $colorHelper->getTimesheetColor($timesheet));
|
||||
}
|
||||
|
||||
$sources[] = new RecentActivitiesSource($entries);
|
||||
} catch (\Exception $ex) {
|
||||
$this->logException($ex);
|
||||
}
|
||||
|
||||
$event = new CalendarDragAndDropSourceEvent($this->getUser());
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
foreach ($event->getSources() as $source) {
|
||||
$sources[] = $source;
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
private function getGoogleSources(SystemConfiguration $configuration): ?Google
|
||||
{
|
||||
$apiKey = $configuration->getCalendarGoogleApiKey();
|
||||
if ($apiKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sources = [];
|
||||
|
||||
foreach ($configuration->getCalendarGoogleSources() as $name => $config) {
|
||||
$sources[] = new GoogleSource($name, $config['id'], $config['color']);
|
||||
}
|
||||
|
||||
$event = new CalendarGoogleSourceEvent($this->getUser());
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
foreach ($event->getSources() as $source) {
|
||||
$sources[] = $source;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
@@ -386,12 +387,18 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayNode('chart')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('background_color')->defaultValue('rgba(0,115,183,0.7)')->end()
|
||||
->scalarNode('background_color')->defaultValue('#3c8dbc')->end() // rgba(0,115,183,0.7) = #0073b7 = Constants::DEFAULT_COLOR
|
||||
->scalarNode('border_color')->defaultValue('#3b8bba')->end()
|
||||
->scalarNode('grid_color')->defaultValue('rgba(0,0,0,.05)')->end()
|
||||
->scalarNode('height')->defaultValue('200')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('calendar')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('background_color')->defaultValue(Constants::DEFAULT_COLOR)->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('branding')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
|
||||
@@ -44,6 +44,11 @@ trait ColorTrait
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function hasColor(): bool
|
||||
{
|
||||
return null !== $this->color && $this->color !== Constants::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return self
|
||||
|
||||
51
src/Event/CalendarDragAndDropSourceEvent.php
Normal file
51
src/Event/CalendarDragAndDropSourceEvent.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Calendar\DragAndDropSource;
|
||||
use App\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class CalendarDragAndDropSourceEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var DragAndDropSource[]
|
||||
*/
|
||||
private $sources = [];
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function addSource(DragAndDropSource $source): CalendarDragAndDropSourceEvent
|
||||
{
|
||||
$this->sources[] = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DragAndDropSource[]
|
||||
*/
|
||||
public function getSources(): array
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
}
|
||||
51
src/Event/CalendarGoogleSourceEvent.php
Normal file
51
src/Event/CalendarGoogleSourceEvent.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Calendar\GoogleSource;
|
||||
use App\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class CalendarGoogleSourceEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var GoogleSource[]
|
||||
*/
|
||||
private $sources = [];
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function addSource(GoogleSource $source): CalendarGoogleSourceEvent
|
||||
{
|
||||
$this->sources[] = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GoogleSource[]
|
||||
*/
|
||||
public function getSources(): array
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
}
|
||||
31
src/Event/TimesheetRestartPostEvent.php
Normal file
31
src/Event/TimesheetRestartPostEvent.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class TimesheetRestartPostEvent extends AbstractTimesheetEvent
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
private $original;
|
||||
|
||||
public function __construct(Timesheet $new, Timesheet $original)
|
||||
{
|
||||
parent::__construct($new);
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
public function getOriginalTimesheet(): Timesheet
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
}
|
||||
31
src/Event/TimesheetRestartPreEvent.php
Normal file
31
src/Event/TimesheetRestartPreEvent.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class TimesheetRestartPreEvent extends AbstractTimesheetEvent
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
private $original;
|
||||
|
||||
public function __construct(Timesheet $new, Timesheet $original)
|
||||
{
|
||||
parent::__construct($new);
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
public function getOriginalTimesheet(): Timesheet
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
}
|
||||
44
src/Form/Type/APIDateTimeType.php
Normal file
44
src/Form/Type/APIDateTimeType.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\API\BaseApiController;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class APIDateTimeType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'documentation' => [
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'example' => (new \DateTime())->format(BaseApiController::DATE_FORMAT_PHP),
|
||||
],
|
||||
'widget' => 'single_text',
|
||||
'html5' => true,
|
||||
'model_timezone' => date_default_timezone_get(),
|
||||
'view_timezone' => date_default_timezone_get(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateTimeType::class;
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,6 @@ class DateTimePickerType extends AbstractType
|
||||
*/
|
||||
protected $dateTime;
|
||||
|
||||
/**
|
||||
* @param LocaleSettings $localeSettings
|
||||
*/
|
||||
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
|
||||
{
|
||||
$this->localeSettings = $localeSettings;
|
||||
|
||||
@@ -23,10 +23,14 @@ use App\Repository\Loader\TimesheetLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<Timesheet>
|
||||
@@ -74,7 +78,7 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteMultiple(iterable $timesheets): void
|
||||
{
|
||||
@@ -87,12 +91,15 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.11 use TimesheetService::stopTimesheet() instead
|
||||
*/
|
||||
public function add(Timesheet $timesheet, int $maxRunningEntries)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
@@ -106,7 +113,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$em->persist($timesheet);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
@@ -126,7 +133,7 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveMultiple(array $timesheets): void
|
||||
{
|
||||
@@ -139,7 +146,7 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
@@ -152,6 +159,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @throws RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @deprecated since 1.11 use TimesheetService::stopTimesheet() instead
|
||||
*/
|
||||
public function stopRecording(Timesheet $entry, bool $flush = true)
|
||||
{
|
||||
@@ -161,7 +169,7 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
// seems to be necessary so Doctrine will recognize a changed timestamp
|
||||
$begin = clone $entry->getBegin();
|
||||
$end = new \DateTime('now', $begin->getTimezone());
|
||||
$end = new DateTime('now', $begin->getTimezone());
|
||||
|
||||
$entry->setBegin($begin);
|
||||
$entry->setEnd($end);
|
||||
@@ -208,7 +216,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$what = 'COUNT(t.id)';
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid query type: ' . $type);
|
||||
throw new InvalidArgumentException('Invalid query type: ' . $type);
|
||||
}
|
||||
|
||||
return $this->queryTimeRange($what, $begin, $end, $user);
|
||||
@@ -401,7 +409,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$results = [];
|
||||
/** @var Timesheet $result */
|
||||
foreach ($timesheets as $result) {
|
||||
/** @var \DateTime $beginTmp */
|
||||
/** @var DateTime $beginTmp */
|
||||
$beginTmp = $result->getBegin();
|
||||
/** @var DateTime $endTmp */
|
||||
$endTmp = $result->getEnd();
|
||||
@@ -412,7 +420,7 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
if ($dateKey !== $dateKeyEnd) {
|
||||
$newDateBegin = clone $beginTmp;
|
||||
$newDateBegin->add(new \DateInterval('P1D'));
|
||||
$newDateBegin->add(new DateInterval('P1D'));
|
||||
// overlapping records should always start at midnight
|
||||
$newDateBegin->setTime(0, 0, 0);
|
||||
} else {
|
||||
@@ -490,7 +498,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @return Day[]
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getDailyStats(?User $user, DateTime $begin, DateTime $end): array
|
||||
{
|
||||
@@ -557,6 +565,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @throws RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @deprecated since 1.11 use TimesheetService::stopTimesheet() instead
|
||||
*/
|
||||
public function stopActiveEntries(User $user, int $hardLimit, bool $flush = true)
|
||||
{
|
||||
@@ -573,7 +582,7 @@ class TimesheetRepository extends EntityRepository
|
||||
foreach ($activeEntries as $activeEntry) {
|
||||
if ($i > $limit) {
|
||||
if ($hardLimit > 1) {
|
||||
throw new \Exception('timesheet.start.exceeded_limit');
|
||||
throw new Exception('timesheet.start.exceeded_limit');
|
||||
}
|
||||
|
||||
$this->stopRecording($activeEntry, $flush);
|
||||
@@ -773,15 +782,15 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if ($query->isExported()) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, \PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, PDO::PARAM_BOOL);
|
||||
} elseif ($query->isNotExported()) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, \PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, PDO::PARAM_BOOL);
|
||||
}
|
||||
|
||||
if ($query->isBillable()) {
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, \PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, PDO::PARAM_BOOL);
|
||||
} elseif ($query->isNotBillable()) {
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, \PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, PDO::PARAM_BOOL);
|
||||
}
|
||||
|
||||
if (null !== $query->getModifiedAfter()) {
|
||||
@@ -848,7 +857,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @return array|mixed
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function getRecentActivities(User $user = null, \DateTime $startFrom = null, $limit = 10)
|
||||
public function getRecentActivities(User $user = null, DateTime $startFrom = null, $limit = 10)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
@@ -865,7 +874,7 @@ class TimesheetRepository extends EntityRepository
|
||||
->groupBy('a.id', 'p.id')
|
||||
->orderBy('maxid', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setParameter('visible', true, \PDO::PARAM_BOOL)
|
||||
->setParameter('visible', true, PDO::PARAM_BOOL)
|
||||
;
|
||||
|
||||
if (null !== $user) {
|
||||
@@ -909,7 +918,7 @@ class TimesheetRepository extends EntityRepository
|
||||
->update(Timesheet::class, 't')
|
||||
->set('t.exported', ':exported')
|
||||
->where($qb->expr()->in('t.id', ':ids'))
|
||||
->setParameter('exported', true, \PDO::PARAM_BOOL)
|
||||
->setParameter('exported', true, PDO::PARAM_BOOL)
|
||||
->setParameter('ids', $timesheets)
|
||||
->getQuery()
|
||||
->execute();
|
||||
@@ -1008,21 +1017,35 @@ class TimesheetRepository extends EntityRepository
|
||||
$or->add($qb->expr()->between(':end', 't.begin', 't.end'));
|
||||
$or->add($qb->expr()->between('t.begin', ':begin', ':end'));
|
||||
$or->add($qb->expr()->between('t.end', ':begin', ':end'));
|
||||
$qb->setParameter('end', $timesheet->getEnd());
|
||||
$end = clone $timesheet->getEnd();
|
||||
$end->sub(new DateInterval('PT1S'));
|
||||
$qb->setParameter('end', $end);
|
||||
}
|
||||
|
||||
// one second is added, because people normally either use the calendar / times which are rounded to the full minute
|
||||
// for an existing entry like 12:45-13:00 it is impossible to add a new one from 13:00-13:15 as the between() query find the first one
|
||||
// by adding one second the between() select will not match any longer
|
||||
|
||||
$begin = clone $timesheet->getBegin();
|
||||
$begin->add(new DateInterval('PT1S'));
|
||||
|
||||
$qb->select($qb->expr()->count('t.id'))
|
||||
->from(Timesheet::class, 't')
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($or)
|
||||
->setParameter('begin', $timesheet->getBegin())
|
||||
->setParameter('user', $timesheet->getUser())
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('user', $timesheet->getUser()->getId())
|
||||
;
|
||||
|
||||
// if we edit an existing entry, make sure we do not find "the same entry" when only updating eg. the description
|
||||
if ($timesheet->getId() !== null) {
|
||||
$qb->andWhere($qb->expr()->neq('t.id', $timesheet->getId()));
|
||||
}
|
||||
|
||||
try {
|
||||
$result = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ use App\Event\TimesheetCreatePreEvent;
|
||||
use App\Event\TimesheetDeleteMultiplePreEvent;
|
||||
use App\Event\TimesheetDeletePreEvent;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
use App\Event\TimesheetRestartPostEvent;
|
||||
use App\Event\TimesheetRestartPreEvent;
|
||||
use App\Event\TimesheetStopPostEvent;
|
||||
use App\Event\TimesheetStopPreEvent;
|
||||
use App\Event\TimesheetUpdateMultiplePostEvent;
|
||||
@@ -24,10 +26,14 @@ use App\Event\TimesheetUpdateMultiplePreEvent;
|
||||
use App\Event\TimesheetUpdatePostEvent;
|
||||
use App\Event\TimesheetUpdatePreEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Validator\ValidationException;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class TimesheetService
|
||||
{
|
||||
@@ -51,19 +57,25 @@ final class TimesheetService
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
private $auth;
|
||||
/**
|
||||
* @var ValidatorInterface
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
TimesheetConfiguration $configuration,
|
||||
TimesheetRepository $repository,
|
||||
TrackingModeService $service,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
AuthorizationCheckerInterface $security
|
||||
AuthorizationCheckerInterface $security,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->configuration = $configuration;
|
||||
$this->repository = $repository;
|
||||
$this->trackingModeService = $service;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->auth = $security;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +100,7 @@ final class TimesheetService
|
||||
public function prepareNewTimesheet(Timesheet $timesheet, ?Request $request = null): Timesheet
|
||||
{
|
||||
if (null !== $timesheet->getId()) {
|
||||
throw new \InvalidArgumentException('Cannot prepare timesheet, already persisted');
|
||||
throw new InvalidArgumentException('Cannot prepare timesheet, already persisted');
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
||||
@@ -100,25 +112,73 @@ final class TimesheetService
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param Timesheet $copyFrom
|
||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||
* @throws InvalidArgumentException for already persisted timesheets
|
||||
* @throws AccessDeniedHttpException if user is not allowed to start timesheet
|
||||
*/
|
||||
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
|
||||
{
|
||||
$this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom));
|
||||
$this->saveNewTimesheet($timesheet);
|
||||
$this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom));
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return Timesheet
|
||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||
* @throws InvalidArgumentException for already persisted timesheets
|
||||
* @throws AccessDeniedHttpException if user is not allowed to start timesheet
|
||||
*/
|
||||
public function saveNewTimesheet(Timesheet $timesheet): Timesheet
|
||||
{
|
||||
if (null !== $timesheet->getId()) {
|
||||
throw new \InvalidArgumentException('Cannot create timesheet, already persisted');
|
||||
throw new InvalidArgumentException('Cannot create timesheet, already persisted');
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd() && !$this->auth->isGranted('start', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to start this timesheet record');
|
||||
}
|
||||
|
||||
$this->validateTimesheet($timesheet);
|
||||
|
||||
try {
|
||||
$this->stopActiveEntries($timesheet);
|
||||
} catch (ValidationFailedException $vex) {
|
||||
// could happen for timesheets that were started in the future (end before begin)
|
||||
throw new ValidationFailedException($vex->getViolations(), 'Cannot stop running timesheet');
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetCreatePreEvent($timesheet));
|
||||
$this->repository->add($timesheet, $this->configuration->getActiveEntriesHardLimit());
|
||||
$this->repository->save($timesheet);
|
||||
$this->dispatcher->dispatch(new TimesheetCreatePostEvent($timesheet));
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does NOT validate the given timesheet!
|
||||
*
|
||||
* @param Timesheet $timesheet
|
||||
* @return Timesheet
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateTimesheet(Timesheet $timesheet): Timesheet
|
||||
{
|
||||
// there is at least one edge case which leads to a problem:
|
||||
// if you do not allow overlapping entries, you cannot restart a timesheet by removing the
|
||||
// end date if another timesheet is running, because the check for existing timesheets will always trigger
|
||||
/*
|
||||
if ($timesheet->getEnd() === null) {
|
||||
$this->stopActiveEntries($timesheet);
|
||||
}
|
||||
*/
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetUpdatePreEvent($timesheet));
|
||||
$this->repository->save($timesheet);
|
||||
$this->dispatcher->dispatch(new TimesheetUpdatePostEvent($timesheet));
|
||||
@@ -126,6 +186,13 @@ final class TimesheetService
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does NOT validate the given timesheet!
|
||||
*
|
||||
* @param array $timesheets
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateMultipleTimesheets(array $timesheets): array
|
||||
{
|
||||
$this->dispatcher->dispatch(new TimesheetUpdateMultiplePreEvent($timesheets));
|
||||
@@ -135,10 +202,30 @@ final class TimesheetService
|
||||
return $timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given timesheet, especially important for not setting a wrong end date.
|
||||
* But also to check that all required data is set.
|
||||
*
|
||||
* @param Timesheet $timesheet
|
||||
* @throws ValidationException for already stopped timesheets
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
public function stopTimesheet(Timesheet $timesheet): void
|
||||
{
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
throw new ValidationException('Timesheet entry already stopped');
|
||||
}
|
||||
|
||||
$begin = clone $timesheet->getBegin();
|
||||
$now = new \DateTime('now', $begin->getTimezone());
|
||||
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($now);
|
||||
|
||||
$this->validateTimesheet($timesheet);
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetStopPreEvent($timesheet));
|
||||
$this->repository->stopRecording($timesheet);
|
||||
$this->repository->save($timesheet);
|
||||
$this->dispatcher->dispatch(new TimesheetStopPostEvent($timesheet));
|
||||
}
|
||||
|
||||
@@ -153,4 +240,56 @@ final class TimesheetService
|
||||
$this->dispatcher->dispatch(new TimesheetDeleteMultiplePreEvent($timesheets));
|
||||
$this->repository->deleteMultiple($timesheets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param string[] $groups
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function validateTimesheet(Timesheet $timesheet, array $groups = []): void
|
||||
{
|
||||
$errors = $this->validator->validate($timesheet, null, $groups);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all active records for the current user, besides the given $timesheet.
|
||||
*
|
||||
* @param Timesheet $timesheet
|
||||
* @return int
|
||||
* @throws ValidationException
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function stopActiveEntries(Timesheet $timesheet): int
|
||||
{
|
||||
$user = $timesheet->getUser();
|
||||
$hardLimit = $this->configuration->getActiveEntriesHardLimit();
|
||||
$activeEntries = $this->repository->getActiveEntries($user);
|
||||
$counter = 0;
|
||||
|
||||
// reduce limit by one:
|
||||
// this method is only called when a new entry is started
|
||||
// -> all entries, including the new one must not exceed the $limit
|
||||
$limit = $hardLimit - 1;
|
||||
|
||||
if (\count($activeEntries) > $limit) {
|
||||
$i = 1;
|
||||
foreach ($activeEntries as $activeEntry) {
|
||||
if ($i > $limit && $timesheet->getId() !== $activeEntry->getId()) {
|
||||
if ($hardLimit > 1) {
|
||||
throw new ValidationException('timesheet.start.exceeded_limit');
|
||||
}
|
||||
|
||||
$this->stopTimesheet($activeEntry);
|
||||
$counter++;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\EntityWithMetaFields;
|
||||
use App\Entity\Project;
|
||||
use App\Utils\Color;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -32,6 +30,7 @@ class Extensions extends AbstractExtension
|
||||
new TwigFilter('docu_link', [$this, 'documentationLink']),
|
||||
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
|
||||
new TwigFilter('color', [$this, 'color']),
|
||||
new TwigFilter('font_contrast', [$this, 'calculateFontContrastColor']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -60,32 +59,20 @@ class Extensions extends AbstractExtension
|
||||
return ++$key;
|
||||
}
|
||||
|
||||
public function color(EntityWithMetaFields $entity): ?string
|
||||
/**
|
||||
* Returns null instead of the default color if $defaultColor is not set to true.
|
||||
*
|
||||
* @param EntityWithMetaFields $entity
|
||||
* @return string|null
|
||||
*/
|
||||
public function color(EntityWithMetaFields $entity, bool $defaultColor = false): ?string
|
||||
{
|
||||
if ($entity instanceof Activity) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
return (new Color())->getColor($entity, $defaultColor);
|
||||
}
|
||||
|
||||
if (null !== $entity->getProject()) {
|
||||
$entity = $entity->getProject();
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity instanceof Project) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
$entity = $entity->getCustomer();
|
||||
}
|
||||
|
||||
if ($entity instanceof Customer) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
public function calculateFontContrastColor(string $color): string
|
||||
{
|
||||
return (new Color())->getFontContrastColor($color);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
98
src/Utils/Color.php
Normal file
98
src/Utils/Color.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?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\Utils;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\EntityWithMetaFields;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class Color
|
||||
{
|
||||
public function getTimesheetColor(Timesheet $timesheet): string
|
||||
{
|
||||
$activity = $timesheet->getActivity();
|
||||
if (null !== $activity && $activity->hasColor()) {
|
||||
return $activity->getColor();
|
||||
}
|
||||
|
||||
$project = $timesheet->getProject();
|
||||
if (null !== $project) {
|
||||
if ($project->hasColor()) {
|
||||
return $project->getColor();
|
||||
}
|
||||
$customer = $project->getCustomer();
|
||||
if ($customer->hasColor()) {
|
||||
return $customer->getColor();
|
||||
}
|
||||
}
|
||||
|
||||
return Constants::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
public function getColor(EntityWithMetaFields $entity, bool $defaultColor = false): ?string
|
||||
{
|
||||
if ($entity instanceof Timesheet) {
|
||||
$color = $this->getTimesheetColor($entity);
|
||||
if ($color === Constants::DEFAULT_COLOR && !$defaultColor) {
|
||||
$color = null;
|
||||
}
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
if ($entity instanceof Activity) {
|
||||
if ($entity->hasColor()) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
|
||||
if (null !== $entity->getProject()) {
|
||||
$entity = $entity->getProject();
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity instanceof Project) {
|
||||
if ($entity->hasColor()) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
$entity = $entity->getCustomer();
|
||||
}
|
||||
|
||||
if ($entity instanceof Customer) {
|
||||
if ($entity->hasColor()) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
}
|
||||
|
||||
return $defaultColor ? Constants::DEFAULT_COLOR : null;
|
||||
}
|
||||
|
||||
public function getFontContrastColor(string $color): string
|
||||
{
|
||||
if ($color[0] !== '#') {
|
||||
throw new \InvalidArgumentException('Invalid color code given, only #hexadecimal is supported.');
|
||||
}
|
||||
|
||||
$color = substr($color, 1);
|
||||
|
||||
if (\strlen($color) === 3) {
|
||||
$color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
|
||||
}
|
||||
|
||||
$r = hexdec(substr($color, 0, 2));
|
||||
$g = hexdec(substr($color, 2, 2));
|
||||
$b = hexdec(substr($color, 4, 2));
|
||||
$yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
return ($yiq >= 128) ? '#000000' : '#ffffff';
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,11 @@ final class TimesheetFutureTimesValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
$now = new \DateTime('now', $timesheet->getBegin()->getTimezone());
|
||||
|
||||
// allow configured default rounding time + 1 minute - see #1295
|
||||
$allowedDiff = ($this->configuration->getDefaultRoundingBegin() * 60) + 60;
|
||||
if ((time() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
|
||||
if (($now->getTimestamp() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
|
||||
$this->context->buildViolation('The begin date cannot be in the future.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
|
||||
@@ -47,6 +47,14 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
// this case is handled in TimesheetValidator and should not raise a second validation
|
||||
if ($begin !== null && $end !== null && $begin > $end) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configuration->isAllowOverlappingRecords()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,10 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
*/
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
if (null === $begin) {
|
||||
$context->buildViolation('You must submit a begin date.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
@@ -82,7 +85,7 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getBegin() && null !== $timesheet->getEnd() && $timesheet->getEnd()->getTimestamp() < $timesheet->getBegin()->getTimestamp()) {
|
||||
if (null !== $end && $begin > $end) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
|
||||
21
src/Validator/ValidationException.php
Normal file
21
src/Validator/ValidationException.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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\Validator;
|
||||
|
||||
class ValidationException extends \RuntimeException
|
||||
{
|
||||
public function __construct(string $message = null)
|
||||
{
|
||||
if ($message === null) {
|
||||
$message = 'Validation failed';
|
||||
}
|
||||
parent::__construct($message, 400);
|
||||
}
|
||||
}
|
||||
35
src/Validator/ValidationFailedException.php
Normal file
35
src/Validator/ValidationFailedException.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Validator;
|
||||
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
class ValidationFailedException extends \RuntimeException
|
||||
{
|
||||
/**
|
||||
* @var ConstraintViolationListInterface
|
||||
*/
|
||||
private $violations;
|
||||
|
||||
public function __construct(ConstraintViolationListInterface $violations, ?string $message = null)
|
||||
{
|
||||
if ($message === null) {
|
||||
$message = 'Validation failed';
|
||||
}
|
||||
parent::__construct($message, 400);
|
||||
|
||||
$this->violations = $violations;
|
||||
}
|
||||
|
||||
public function getViolations(): ConstraintViolationListInterface
|
||||
{
|
||||
return $this->violations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user