Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -10,30 +10,18 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/about")
|
||||
*/
|
||||
class AboutController extends AbstractController
|
||||
#[Route(path: '/about')]
|
||||
final class AboutController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $projectDirectory;
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
public function __construct(private string $projectDirectory)
|
||||
{
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="about", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '', name: 'about', methods: ['GET'])]
|
||||
public function license(): Response
|
||||
{
|
||||
$filename = $this->projectDirectory . '/LICENSE';
|
||||
@@ -47,10 +35,11 @@ class AboutController extends AbstractController
|
||||
|
||||
if (false === $license) {
|
||||
$license = 'Failed reading license file: ' . $filename . '. ' .
|
||||
'Check this instead: ' . Constants::GITHUB . 'blob/master/LICENSE';
|
||||
'Check this instead: ' . Constants::GITHUB . 'blob/main/LICENSE';
|
||||
}
|
||||
|
||||
return $this->render('about/license.html.twig', [
|
||||
'page_setup' => new PageSetup('about.title'),
|
||||
'license' => $license
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,86 +9,112 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\User;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\LocaleFormats;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* The abstract base controller.
|
||||
* @method null|User getUser()
|
||||
*/
|
||||
abstract class AbstractController extends BaseAbstractController implements ServiceSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @deprecated since 1.6, will be removed with 2.0
|
||||
*/
|
||||
public const ROLE_ADMIN = User::ROLE_ADMIN;
|
||||
protected function getUser(): User
|
||||
{
|
||||
$user = parent::getUser();
|
||||
if ($user === null) {
|
||||
throw $this->createAccessDeniedException('Missing user');
|
||||
}
|
||||
|
||||
protected function getTranslator(): TranslatorInterface
|
||||
if (!($user instanceof User)) {
|
||||
throw $this->createAccessDeniedException('Expected Kimai user, received unknown type');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function getTranslator(): TranslatorInterface
|
||||
{
|
||||
return $this->container->get('translator');
|
||||
}
|
||||
|
||||
public function createFormForGetRequest(string $type = FormType::class, $data = null, array $options = []): FormInterface
|
||||
protected function createSearchForm(string $type = FormType::class, $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->createFormForGetRequest($type, $data, $options);
|
||||
}
|
||||
|
||||
protected function createFormForGetRequest(string $type = FormType::class, $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container
|
||||
->get('form.factory')
|
||||
->createNamed('', $type, $data, $options);
|
||||
->createNamed('', $type, $data, array_merge(['method' => 'GET'], $options));
|
||||
}
|
||||
|
||||
private function getLogger(): LoggerInterface
|
||||
protected function createFormWithName(string $name, string $type, mixed $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container->get('logger');
|
||||
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RedirectResponse to the given route with the given parameters.
|
||||
*
|
||||
* This needs to be a 201 code and NOT 302 (as usual for redirects) because 302 cannot be handled on
|
||||
* javascript side, as the fetch() API will auto-redirect these responses without access to the header.
|
||||
*/
|
||||
protected function redirectToRouteAfterCreate(string $route, array $parameters = []): RedirectResponse
|
||||
{
|
||||
$url = $this->generateUrl($route, $parameters);
|
||||
$response = new RedirectResponse($url, 201);
|
||||
$response->headers->set('x-modal-redirect', $url);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "successful" flash message to the stack.
|
||||
*
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashSuccess(string $translationKey, array $parameter = []): void
|
||||
protected function flashSuccess(string $translationKey): void
|
||||
{
|
||||
$this->addFlashTranslated('success', $translationKey, $parameter);
|
||||
$this->addFlashTranslated('success', $translationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "warning" flash message to the stack.
|
||||
*
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashWarning(string $translationKey, array $parameter = []): void
|
||||
protected function flashWarning(string $translationKey): void
|
||||
{
|
||||
$this->addFlashTranslated('warning', $translationKey, $parameter);
|
||||
$this->addFlashTranslated('warning', $translationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "error" flash message to the stack.
|
||||
* Adds an "error" flash message to the stack.
|
||||
*
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
* @param array<string, string>|string $reason passing an array is deprecated
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function flashError(string $translationKey, array $parameter = []): void
|
||||
protected function flashError(string $translationKey, array|string $reason = ''): void
|
||||
{
|
||||
$this->addFlashTranslated('error', $translationKey, $parameter);
|
||||
if (\is_array($reason)) {
|
||||
@trigger_error('Calling "flashError" with an array $reason is deprecated and will be removed soon. Refactor and pass a string instead.', E_USER_DEPRECATED);
|
||||
$reason = \array_key_exists('%reason%', $reason) ? $reason['%reason%'] : '';
|
||||
}
|
||||
|
||||
$this->addFlashTranslated('error', $translationKey, ['%reason%' => $reason]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an exception flash message for failed update/create actions.
|
||||
*
|
||||
* @param \Exception $exception
|
||||
*/
|
||||
protected function flashUpdateException(\Exception $exception): void
|
||||
{
|
||||
@@ -97,8 +123,6 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
|
||||
/**
|
||||
* Adds an exception flash message for failed delete actions.
|
||||
*
|
||||
* @param \Exception $exception
|
||||
*/
|
||||
protected function flashDeleteException(\Exception $exception): void
|
||||
{
|
||||
@@ -106,21 +130,13 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "error" flash message and logs the Exception.
|
||||
*
|
||||
* @param \Exception $exception
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
* Adds an "error" flash message and logs the Exception.
|
||||
*/
|
||||
protected function flashException(\Exception $exception, string $translationKey, array $parameter = []): void
|
||||
protected function flashException(\Exception $exception, string $translationKey): void
|
||||
{
|
||||
$this->logException($exception);
|
||||
|
||||
if (!\array_key_exists('%reason%', $parameter)) {
|
||||
$parameter['%reason%'] = $exception->getMessage();
|
||||
}
|
||||
|
||||
$this->addFlashTranslated('error', $translationKey, $parameter);
|
||||
$this->addFlashTranslated('error', $translationKey, ['%reason%' => $exception->getMessage()]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,9 +144,11 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $message
|
||||
* @param array $parameter
|
||||
* @param array<string, string> $parameter
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addFlashTranslated(string $type, string $message, array $parameter = []): void
|
||||
private function addFlashTranslated(string $type, string $message, array $parameter = []): void
|
||||
{
|
||||
if (!empty($parameter)) {
|
||||
foreach ($parameter as $key => $value) {
|
||||
@@ -146,17 +164,39 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
$this->addFlash($type, $message);
|
||||
}
|
||||
|
||||
protected function logException(\Exception $ex): void
|
||||
/**
|
||||
* Handles exception flash messages for failed update/create actions.
|
||||
*/
|
||||
protected function handleFormUpdateException(\Exception $exception, FormInterface $form): void
|
||||
{
|
||||
$this->getLogger()->critical($ex->getMessage());
|
||||
if (!($exception instanceof ValidationFailedException)) {
|
||||
$this->flashUpdateException($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = $this->getTranslator()->trans($exception->getMessage(), [], 'validators');
|
||||
if ($exception->getViolations()->count() > 0) {
|
||||
for ($i = 0; $i < $exception->getViolations()->count(); $i++) {
|
||||
$violation = $exception->getViolations()->get($i);
|
||||
$form->addError(new FormError($violation->getMessage()));
|
||||
}
|
||||
} else {
|
||||
$form->addError(new FormError($msg));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedServices()
|
||||
protected function logException(\Exception $ex): void
|
||||
{
|
||||
$this->container->get('logger')->critical($ex->getMessage());
|
||||
}
|
||||
|
||||
public static function getSubscribedServices(): array
|
||||
{
|
||||
return array_merge(parent::getSubscribedServices(), [
|
||||
'translator' => TranslatorInterface::class,
|
||||
'logger' => LoggerInterface::class,
|
||||
LanguageFormattings::class => LanguageFormattings::class,
|
||||
BookmarkRepository::class => BookmarkRepository::class,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -169,28 +209,30 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
return DateTimeFactory::createByUser($user);
|
||||
}
|
||||
|
||||
protected function getLocaleFormats(string $locale): LocaleFormats
|
||||
// ================================ SEARCH AND BOOKMARKS ================================
|
||||
|
||||
private function getBookmark(): BookmarkRepository
|
||||
{
|
||||
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
|
||||
return $this->container->get(BookmarkRepository::class);
|
||||
}
|
||||
|
||||
private function getLastSearch(BaseQuery $query): ?array
|
||||
private function getLastSearch(SessionInterface $session, BaseQuery $query): ?array
|
||||
{
|
||||
$name = 'search_' . $this->getSearchName($query);
|
||||
|
||||
if (!$this->get('session')->has($name)) {
|
||||
if (!$session->has($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->get('session')->get($name);
|
||||
return $session->get($name);
|
||||
}
|
||||
|
||||
private function removeLastSearch(BaseQuery $query): void
|
||||
private function removeLastSearch(SessionInterface $session, BaseQuery $query): void
|
||||
{
|
||||
$name = 'search_' . $this->getSearchName($query);
|
||||
|
||||
if ($this->get('session')->has($name)) {
|
||||
$this->get('session')->remove($name);
|
||||
if ($session->has($name)) {
|
||||
$session->remove($name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,15 +241,6 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
return substr($query->getName(), 0, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @internal
|
||||
*/
|
||||
protected function ignorePersistedSearch(Request $request): void
|
||||
{
|
||||
$request->query->set('performSearch', true);
|
||||
}
|
||||
|
||||
protected function handleSearch(FormInterface $form, Request $request): bool
|
||||
{
|
||||
$data = $form->getData();
|
||||
@@ -230,21 +263,26 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
|
||||
if ($request->query->has('resetSearchFilter')) {
|
||||
$data->resetFilter();
|
||||
$this->removeLastSearch($data);
|
||||
$this->removeLastSearch($request->getSession(), $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$submitData = $request->query->all();
|
||||
// allow to use forms with block-prefix
|
||||
$queryKey = null;
|
||||
if (!empty($formName = $form->getConfig()->getName()) && $request->request->has($formName)) {
|
||||
$submitData = $request->request->get($formName);
|
||||
// allow using forms with block-prefix
|
||||
$queryKey = $formName;
|
||||
}
|
||||
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
$submitData = $request->request->all($queryKey);
|
||||
} else {
|
||||
$submitData = $request->query->all($queryKey);
|
||||
}
|
||||
$searchName = $this->getSearchName($data);
|
||||
|
||||
/** @var BookmarkRepository $bookmarkRepo */
|
||||
$bookmarkRepo = $this->getDoctrine()->getRepository(Bookmark::class);
|
||||
$bookmarkRepo = $this->getBookmark();
|
||||
$bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $searchName);
|
||||
|
||||
if ($bookmark !== null) {
|
||||
@@ -260,12 +298,23 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
|
||||
// apply persisted search data ONLY if search form was not submitted manually
|
||||
if (!$request->query->has('performSearch')) {
|
||||
$sessionSearch = $this->getLastSearch($data);
|
||||
$sessionSearch = $this->getLastSearch($request->getSession(), $data);
|
||||
if ($sessionSearch !== null) {
|
||||
$submitData = array_merge($sessionSearch, $submitData);
|
||||
} elseif ($bookmark !== null && !$request->query->has('setDefaultQuery')) {
|
||||
$submitData = array_merge($bookmark->getContent(), $submitData);
|
||||
$data->flagAsBookmarkSearch();
|
||||
$bookContent = $bookmark->getContent();
|
||||
$isBookmarkSearch = true;
|
||||
foreach ($submitData as $key => $value) {
|
||||
if (!\array_key_exists($key, $bookContent) || $value !== $bookContent[$key]) {
|
||||
$isBookmarkSearch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($isBookmarkSearch) {
|
||||
$data->flagAsBookmarkSearch();
|
||||
}
|
||||
|
||||
$submitData = array_merge($bookContent, $submitData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +347,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
}
|
||||
|
||||
if ($request->query->has('performSearch')) {
|
||||
$this->get('session')->set('search_' . $searchName, $params);
|
||||
$request->getSession()->set('search_' . $searchName, $params);
|
||||
}
|
||||
|
||||
// filter stuff, that does not belong in a bookmark
|
||||
@@ -310,7 +359,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
}
|
||||
|
||||
if ($request->query->has('setDefaultQuery')) {
|
||||
$this->removeLastSearch($data);
|
||||
$this->removeLastSearch($request->getSession(), $data);
|
||||
if ($bookmark === null) {
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setType(Bookmark::SEARCH_DEFAULT);
|
||||
|
||||
@@ -32,50 +32,29 @@ use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')")
|
||||
* Controller used to manage activities.
|
||||
*/
|
||||
#[Route(path: '/admin/activity')]
|
||||
#[Security("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')")]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var ActivityService
|
||||
*/
|
||||
private $activityService;
|
||||
|
||||
public function __construct(ActivityRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher, ActivityService $activityService)
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->activityService = $activityService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
@@ -88,12 +67,43 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
$metaColumns = $this->findMetaColumns($query);
|
||||
|
||||
$table = new DataTable('activity_admin', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_activity_paginated');
|
||||
$table->setReloadEvents('kimai.activityUpdate kimai.activityDelete kimai.activityTeamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('project', ['class' => 'd-none']);
|
||||
$table->addColumn('comment', ['class' => 'd-none', 'title' => 'description']);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_money', 'activity')) {
|
||||
$table->addColumn('budget', ['class' => 'd-none text-end w-min', 'title' => 'budget']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_time', 'activity')) {
|
||||
$table->addColumn('timeBudget', ['class' => 'd-none text-end w-min', 'title' => 'timeBudget']);
|
||||
}
|
||||
|
||||
$table->addColumn('billable', ['class' => 'd-none text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('team', ['class' => 'text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('visible', ['class' => 'd-none text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setDataTable($table);
|
||||
$page->setActionName('activities');
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'metaColumns' => $this->findMetaColumns($query),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'metaColumns' => $metaColumns,
|
||||
'defaultCurrency' => $this->configuration->getCustomerDefaultCurrency(),
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
@@ -103,7 +113,7 @@ final class ActivityController extends AbstractController
|
||||
* @param ActivityQuery $query
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
protected function findMetaColumns(ActivityQuery $query): array
|
||||
private function findMetaColumns(ActivityQuery $query): array
|
||||
{
|
||||
$event = new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::ACTIVITY);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -111,10 +121,8 @@ final class ActivityController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="activity_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', activity)")
|
||||
*/
|
||||
#[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', activity)")]
|
||||
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService)
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
@@ -146,7 +154,13 @@ final class ActivityController extends AbstractController
|
||||
$this->dispatcher->dispatch($event);
|
||||
$boxes = $event->getController();
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setActionName('activity');
|
||||
$page->setActionView('activity_details');
|
||||
$page->setActionPayload(['activity' => $activity]);
|
||||
|
||||
return $this->render('activity/details.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'rates' => $rates,
|
||||
@@ -157,17 +171,27 @@ final class ActivityController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_activity_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository)
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_activity_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
public function editRateAction(Activity $activity, ActivityRate $rate, Request $request, ActivityRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($activity, $rate, $request, $repository, $this->generateUrl('admin_activity_rate_edit', ['id' => $activity->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_activity_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository): Response
|
||||
{
|
||||
$rate = new ActivityRate();
|
||||
$rate->setActivity($activity);
|
||||
|
||||
return $this->rateFormAction($activity, $rate, $request, $repository, $this->generateUrl('admin_activity_rate_add', ['id' => $activity->getId()]));
|
||||
}
|
||||
|
||||
private function rateFormAction(Activity $activity, ActivityRate $rate, Request $request, ActivityRateRepository $repository, string $formUrl): Response
|
||||
{
|
||||
$form = $this->createForm(ActivityRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_activity_rate_add', ['id' => $activity->getId()]),
|
||||
'action' => $formUrl,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
@@ -185,17 +209,27 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('activity/rates.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'activity' => $activity,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{project}", name="admin_activity_create_with_project", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_activity')")
|
||||
*/
|
||||
public function createAction(Request $request, ?Project $project = null)
|
||||
#[Route(path: '/create/{project}', name: 'admin_activity_create_with_project', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_activity')")]
|
||||
public function createWithProjectAction(Request $request, Project $project): Response
|
||||
{
|
||||
return $this->createActivity($request, $project);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_activity_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_activity')")]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->createActivity($request, null);
|
||||
}
|
||||
|
||||
private function createActivity(Request $request, ?Project $project = null): Response
|
||||
{
|
||||
$activity = $this->activityService->createNewActivity($project);
|
||||
|
||||
@@ -210,23 +244,22 @@ final class ActivityController extends AbstractController
|
||||
$this->activityService->saveNewActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
return $this->redirectToRouteAfterCreate('activity_details', ['id' => $activity->getId()]);
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('activity/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'activity' => $activity,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_activity_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', activity)")
|
||||
*/
|
||||
public function teamPermissionsAction(Activity $activity, Request $request)
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_activity_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', activity)")]
|
||||
public function teamPermissionsAction(Activity $activity, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(ActivityTeamPermissionForm::class, $activity, [
|
||||
'action' => $this->generateUrl('admin_activity_permissions', ['id' => $activity->getId()]),
|
||||
@@ -251,26 +284,24 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('activity/permissions.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'activity' => $activity,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/create_team", name="activity_team_create", methods={"GET"})
|
||||
* @Security("is_granted('create_team') and is_granted('permissions', activity)")
|
||||
*/
|
||||
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository)
|
||||
#[Route(path: '/{id}/create_team', name: 'activity_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', activity)")]
|
||||
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
$defaultTeam = new Team();
|
||||
$defaultTeam->setName($activity->getName());
|
||||
$defaultTeam = new Team($activity->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addActivity($activity);
|
||||
|
||||
@@ -283,11 +314,9 @@ final class ActivityController extends AbstractController
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*/
|
||||
public function editAction(Activity $activity, Request $request)
|
||||
#[Route(path: '/{id}/edit', name: 'admin_activity_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', activity)")]
|
||||
public function editAction(Activity $activity, Request $request): Response
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -307,16 +336,15 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('activity/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'activity' => $activity,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request, ActivityStatisticService $statisticService)
|
||||
#[Route(path: '/{id}/delete', name: 'admin_activity_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', activity)")]
|
||||
public function deleteAction(Activity $activity, Request $request, ActivityStatisticService $statisticService): Response
|
||||
{
|
||||
$stats = $statisticService->getActivityStatistics($activity);
|
||||
|
||||
@@ -352,20 +380,16 @@ final class ActivityController extends AbstractController
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'activity/delete.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]
|
||||
);
|
||||
return $this->render('activity/delete.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="activity_export", methods={"GET"})
|
||||
*/
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
#[Route(path: '/export', name: 'activity_export', methods: ['GET'])]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -390,25 +414,16 @@ final class ActivityController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ActivityQuery $query)
|
||||
private function getToolbarForm(ActivityQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ActivityToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(ActivityToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_activity', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
private function createEditForm(Activity $activity): FormInterface
|
||||
{
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
@@ -428,4 +443,12 @@ final class ActivityController extends AbstractController
|
||||
'include_time' => $this->isGranted('time', $activity),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPageSetup(): PageSetup
|
||||
{
|
||||
$page = new PageSetup('activities');
|
||||
$page->setHelp('activity.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,23 +17,14 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @Route(path="/saml")
|
||||
*/
|
||||
#[Route(path: '/saml')]
|
||||
final class SamlController extends AbstractController
|
||||
{
|
||||
private $authFactory;
|
||||
private $samlConfiguration;
|
||||
|
||||
public function __construct(SamlAuthFactory $authFactory, SamlConfigurationInterface $samlConfiguration)
|
||||
public function __construct(private SamlAuthFactory $authFactory, private SamlConfigurationInterface $samlConfiguration)
|
||||
{
|
||||
$this->authFactory = $authFactory;
|
||||
$this->samlConfiguration = $samlConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login", name="saml_login")
|
||||
*/
|
||||
#[Route(path: '/login', name: 'saml_login')]
|
||||
public function loginAction(Request $request)
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
@@ -47,7 +38,7 @@ final class SamlController extends AbstractController
|
||||
|
||||
if ($request->attributes->has($authErrorKey)) {
|
||||
$error = $request->attributes->get($authErrorKey);
|
||||
} elseif (null !== $session && $session->has($authErrorKey)) {
|
||||
} elseif ($session->has($authErrorKey)) {
|
||||
$error = $session->get($authErrorKey);
|
||||
$session->remove($authErrorKey);
|
||||
}
|
||||
@@ -62,9 +53,7 @@ final class SamlController extends AbstractController
|
||||
$this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/metadata", name="saml_metadata")
|
||||
*/
|
||||
#[Route(path: '/metadata', name: 'saml_metadata')]
|
||||
public function metadataAction()
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
@@ -79,9 +68,7 @@ final class SamlController extends AbstractController
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/acs", name="saml_acs")
|
||||
*/
|
||||
#[Route(path: '/acs', name: 'saml_acs')]
|
||||
public function assertionConsumerServiceAction()
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
@@ -91,9 +78,7 @@ final class SamlController extends AbstractController
|
||||
throw new \RuntimeException('You must configure the check path in your firewall.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/logout", name="saml_logout")
|
||||
*/
|
||||
#[Route(path: '/logout', name: 'saml_logout')]
|
||||
public function logoutAction()
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
|
||||
140
src/Controller/BookmarkController.php
Normal file
140
src/Controller/BookmarkController.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\Bookmark;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Utils\ProfileManager;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\RuntimeException;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
/**
|
||||
* This does not go into the API, because it is ONLY related to the Web UI.
|
||||
*/
|
||||
#[Route(path: '/bookmark')]
|
||||
final class BookmarkController extends AbstractController
|
||||
{
|
||||
public const DATATABLE_TOKEN = 'datatable_update';
|
||||
public const PARAM_TOKEN_NAME = 'datatable_token';
|
||||
public const PARAM_DATATABLE = 'datatable_name';
|
||||
public const PARAM_PROFILE = 'datatable_profile';
|
||||
|
||||
public function __construct(private BookmarkRepository $bookmarkRepository, private ProfileManager $profileManager)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/datatable/profile', name: 'bookmark_profile', methods: ['POST'])]
|
||||
public function datatableProfile(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$request->request->has(self::PARAM_TOKEN_NAME) || !$request->request->has(self::PARAM_PROFILE)) {
|
||||
throw $this->createNotFoundException('Missing CSRF Token');
|
||||
}
|
||||
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken(self::DATATABLE_TOKEN, $request->request->get(self::PARAM_TOKEN_NAME)))) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF Token');
|
||||
}
|
||||
|
||||
$profile = $request->request->get(self::PARAM_PROFILE);
|
||||
if (!$this->profileManager->isValidProfile($profile)) {
|
||||
throw $this->createNotFoundException('Invalid profile given');
|
||||
}
|
||||
|
||||
$this->profileManager->setProfile($request->getSession(), $profile);
|
||||
$csrfTokenManager->refreshToken(self::DATATABLE_TOKEN);
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
#[Route(path: '/datatable/save', name: 'bookmark_save_datatable', methods: ['POST'])]
|
||||
public function datatableSave(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$request->request->has(self::PARAM_TOKEN_NAME) || !$request->request->has(self::PARAM_DATATABLE) || !$request->request->has(self::PARAM_PROFILE)) {
|
||||
throw $this->createNotFoundException('Missing data: csrf token, datatable name or profile');
|
||||
}
|
||||
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken(self::DATATABLE_TOKEN, $request->request->get(self::PARAM_TOKEN_NAME)))) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF Token');
|
||||
}
|
||||
|
||||
$profile = $request->request->get(self::PARAM_PROFILE);
|
||||
if (!$this->profileManager->isValidProfile($profile)) {
|
||||
throw $this->createNotFoundException('Invalid profile given');
|
||||
}
|
||||
|
||||
$datatableName = $request->request->get(self::PARAM_DATATABLE);
|
||||
$datatableName = $this->profileManager->getDatatableName($datatableName, $profile);
|
||||
|
||||
if (empty($datatableName) || mb_strlen($datatableName) > 50) {
|
||||
throw new RuntimeException('Invalid datatable name');
|
||||
}
|
||||
|
||||
$enabled = [];
|
||||
foreach ($request->request->all() as $name => $value) {
|
||||
if ($value !== 'on' || mb_strlen($name) > 30) {
|
||||
continue;
|
||||
}
|
||||
$enabled[$name] = true;
|
||||
}
|
||||
|
||||
if (\count($enabled) > 50) {
|
||||
throw new RuntimeException(sprintf('Too many columns provided, expected maximum 50, received %s.', \count($enabled)));
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$bookmark = $this->bookmarkRepository->findBookmark($user, Bookmark::COLUMN_VISIBILITY, $datatableName);
|
||||
if ($bookmark === null) {
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setUser($user);
|
||||
$bookmark->setType(Bookmark::COLUMN_VISIBILITY);
|
||||
$bookmark->setName($datatableName);
|
||||
}
|
||||
$bookmark->setContent($enabled);
|
||||
|
||||
$this->bookmarkRepository->saveBookmark($bookmark);
|
||||
$this->profileManager->setProfile($request->getSession(), $profile);
|
||||
$csrfTokenManager->refreshToken(self::DATATABLE_TOKEN);
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
#[Route(path: '/datatable/delete', name: 'bookmark_delete', methods: ['POST'])]
|
||||
public function datatableDelete(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$request->request->has(self::PARAM_TOKEN_NAME) || !$request->request->has(self::PARAM_DATATABLE) || !$request->request->has(self::PARAM_PROFILE)) {
|
||||
throw $this->createNotFoundException('Missing data: csrf token, datatable name or profile');
|
||||
}
|
||||
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken(self::DATATABLE_TOKEN, $request->request->get(self::PARAM_TOKEN_NAME)))) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF Token');
|
||||
}
|
||||
|
||||
$profile = $request->request->get(self::PARAM_PROFILE);
|
||||
if (!$this->profileManager->isValidProfile($profile)) {
|
||||
throw $this->createNotFoundException('Invalid profile given');
|
||||
}
|
||||
|
||||
$datatableName = $request->request->get(self::PARAM_DATATABLE);
|
||||
$datatableName = $this->profileManager->getDatatableName($datatableName, $profile);
|
||||
|
||||
$bookmark = $this->bookmarkRepository->findBookmark($this->getUser(), Bookmark::COLUMN_VISIBILITY, $datatableName);
|
||||
if ($bookmark !== null) {
|
||||
$this->bookmarkRepository->deleteBookmark($bookmark);
|
||||
}
|
||||
|
||||
$csrfTokenManager->refreshToken(self::DATATABLE_TOKEN);
|
||||
|
||||
return new Response();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Form\CalendarForm;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -21,27 +22,17 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to display calendars.
|
||||
*
|
||||
* @Route(path="/calendar")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class CalendarController extends AbstractController
|
||||
#[Route(path: '/calendar')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
final class CalendarController extends AbstractController
|
||||
{
|
||||
private $calendarService;
|
||||
private $configuration;
|
||||
private $service;
|
||||
|
||||
public function __construct(CalendarService $calendarService, SystemConfiguration $configuration, TrackingModeService $service)
|
||||
public function __construct(private CalendarService $calendarService, private SystemConfiguration $configuration, private TrackingModeService $service)
|
||||
{
|
||||
$this->calendarService = $calendarService;
|
||||
$this->configuration = $configuration;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="calendar", methods={"GET"})
|
||||
* @Route(path="/{profile}", name="calendar_user", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', name: 'calendar', methods: ['GET'])]
|
||||
#[Route(path: '/{profile}', name: 'calendar_user', methods: ['GET'])]
|
||||
public function userCalendar(Request $request): Response
|
||||
{
|
||||
$form = null;
|
||||
@@ -72,7 +63,13 @@ class CalendarController extends AbstractController
|
||||
|
||||
$mode = $this->service->getActiveMode();
|
||||
$factory = $this->getDateTimeFactory();
|
||||
$defaultStart = $factory->createDateTime($this->configuration->getTimesheetDefaultBeginTime());
|
||||
|
||||
// if now is default time, we do not pass it on, so it can be re-calculated for each new entry
|
||||
$defaultStart = null;
|
||||
if ($this->configuration->getTimesheetDefaultBeginTime() !== 'now') {
|
||||
$defaultStart = $factory->createDateTime($this->configuration->getTimesheetDefaultBeginTime());
|
||||
$defaultStart = $defaultStart->format('h:i:s');
|
||||
}
|
||||
|
||||
$config = $this->calendarService->getConfiguration();
|
||||
|
||||
@@ -87,14 +84,18 @@ class CalendarController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('calendar');
|
||||
$page->setHelp('calendar.html');
|
||||
|
||||
return $this->render('calendar/user.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'form' => $form,
|
||||
'user' => $profile,
|
||||
'config' => $config,
|
||||
'dragAndDrop' => $dragAndDrop,
|
||||
'google' => $this->calendarService->getGoogleSources($profile),
|
||||
'now' => $factory->createDateTime(),
|
||||
'defaultStartTime' => $defaultStart->format('h:i:s'),
|
||||
'defaultStartTime' => $defaultStart,
|
||||
'is_punch_mode' => $isPunchMode,
|
||||
'can_edit_begin' => $mode->canEditBegin(),
|
||||
'can_edit_end' => $mode->canEditBegin(),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Customer\CustomerService;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
@@ -34,44 +34,34 @@ use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\FileHelper;
|
||||
use App\Utils\PageSetup;
|
||||
use JeroenDesloovere\VCard\VCard;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\Intl\Countries;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')")
|
||||
*/
|
||||
#[Route(path: '/admin/customer')]
|
||||
#[Security("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')")]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(CustomerRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -84,12 +74,54 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
$metaColumns = $this->findMetaColumns($query);
|
||||
|
||||
$table = new DataTable('customer_admin', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_customer_paginated');
|
||||
$table->setReloadEvents('kimai.customerUpdate kimai.customerDelete kimai.customerTeamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('comment', ['class' => 'd-none', 'title' => 'description']);
|
||||
$table->addColumn('number', ['class' => 'd-none w-min']);
|
||||
$table->addColumn('company', ['class' => 'd-none']);
|
||||
$table->addColumn('vat_id', ['class' => 'd-none w-min']);
|
||||
$table->addColumn('contact', ['class' => 'd-none']);
|
||||
$table->addColumn('address', ['class' => 'd-none']);
|
||||
$table->addColumn('country', ['class' => 'd-none w-min']);
|
||||
$table->addColumn('currency', ['class' => 'd-none w-min']);
|
||||
$table->addColumn('phone', ['class' => 'd-none']);
|
||||
$table->addColumn('fax', ['class' => 'd-none']);
|
||||
$table->addColumn('mobile', ['class' => 'd-none']);
|
||||
$table->addColumn('email', ['class' => 'd-none']);
|
||||
$table->addColumn('homepage', ['class' => 'd-none']);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_money', 'customer')) {
|
||||
$table->addColumn('budget', ['class' => 'd-none text-end w-min', 'title' => 'budget']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_time', 'customer')) {
|
||||
$table->addColumn('timeBudget', ['class' => 'd-none text-end w-min', 'title' => 'timeBudget']);
|
||||
}
|
||||
|
||||
$table->addColumn('billable', ['class' => 'd-none text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('team', ['class' => 'text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('visible', ['class' => 'd-none text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setDataTable($table);
|
||||
$page->setActionName('customers');
|
||||
|
||||
return $this->render('customer/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'metaColumns' => $this->findMetaColumns($query),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'metaColumns' => $metaColumns,
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
}
|
||||
@@ -106,29 +138,17 @@ final class CustomerController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*/
|
||||
public function createAction(Request $request, SystemConfiguration $configuration)
|
||||
#[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_customer')")]
|
||||
public function createAction(Request $request, CustomerService $customerService)
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
$customer = $customerService->createNewCustomer('');
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setCountry($configuration->getCustomerDefaultCountry());
|
||||
$customer->setCurrency($configuration->getCustomerDefaultCurrency());
|
||||
$customer->setTimezone($timezone);
|
||||
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
return $this->renderCustomerForm($customer, $request, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_customer_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', customer)")]
|
||||
public function teamPermissionsAction(Customer $customer, Request $request)
|
||||
{
|
||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||
@@ -154,15 +174,14 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('customer/permissions.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_delete/{token}", name="customer_comment_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")]
|
||||
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$customerId = $comment->getCustomer()->getId();
|
||||
@@ -184,14 +203,12 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customerId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_add", name="customer_comment_add", methods={"POST"})
|
||||
* @Security("is_granted('comments_create', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])]
|
||||
#[Security("is_granted('comments', customer)")]
|
||||
public function addCommentAction(Customer $customer, Request $request)
|
||||
{
|
||||
$comment = new CustomerComment();
|
||||
$form = $this->getCommentForm($customer, $comment);
|
||||
$comment = new CustomerComment($customer);
|
||||
$form = $this->getCommentForm($comment);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -206,10 +223,8 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_pin/{token}", name="customer_comment_pin", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")]
|
||||
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$customerId = $comment->getCustomer()->getId();
|
||||
@@ -232,21 +247,18 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customerId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/create_team", name="customer_team_create", methods={"GET"})
|
||||
* @Security("is_granted('create_team') and is_granted('permissions', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', customer)")]
|
||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository)
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
$defaultTeam = new Team();
|
||||
$defaultTeam->setName($customer->getName());
|
||||
$defaultTeam = new Team($customer->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addCustomer($customer);
|
||||
|
||||
@@ -259,10 +271,8 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/projects/{page}", defaults={"page": 1}, name="customer_projects", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
@@ -274,7 +284,6 @@ final class CustomerController extends AbstractController
|
||||
$query->addOrderGroup('visible', ProjectQuery::ORDER_DESC);
|
||||
$query->addOrderGroup('name', ProjectQuery::ORDER_ASC);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $projectRepository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('customer/embed_projects.html.twig', [
|
||||
@@ -285,10 +294,8 @@ final class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="customer_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
@@ -321,10 +328,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
if ($this->isGranted('comments', $customer)) {
|
||||
$comments = $this->repository->getComments($customer);
|
||||
}
|
||||
|
||||
if ($this->isGranted('comments_create', $customer)) {
|
||||
$commentForm = $this->getCommentForm($customer, new CustomerComment())->createView();
|
||||
$commentForm = $this->getCommentForm(new CustomerComment($customer))->createView();
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
||||
@@ -336,7 +340,13 @@ final class CustomerController extends AbstractController
|
||||
$this->dispatcher->dispatch($event);
|
||||
$boxes = $event->getController();
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setActionName('customer');
|
||||
$page->setActionView('customer_details');
|
||||
$page->setActionPayload(['customer' => $customer]);
|
||||
|
||||
return $this->render('customer/details.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'customer' => $customer,
|
||||
'comments' => $comments,
|
||||
'commentForm' => $commentForm,
|
||||
@@ -351,17 +361,80 @@ final class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_customer_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository)
|
||||
#[Route(path: '/{id}/vcard', name: 'customer_vcard', methods: ['GET'])]
|
||||
#[Security("is_granted('view', customer)")]
|
||||
public function downloadVCard(Customer $customer): Response
|
||||
{
|
||||
$vcard = new VCard();
|
||||
|
||||
$contact = $customer->getContact() ?? $customer->getName();
|
||||
$contact = explode(' ', $contact);
|
||||
$lastname = array_pop($contact);
|
||||
$firstname = \count($contact) > 0 ? $contact[0] : $lastname;
|
||||
$note = $customer->getComment();
|
||||
if ($note !== null) {
|
||||
$note .= PHP_EOL;
|
||||
}
|
||||
|
||||
$vcard->addName($lastname, $firstname);
|
||||
$vcard->addNote($note . $customer->getAddress());
|
||||
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($customer->getCountry()));
|
||||
|
||||
$vcard->addCompany($customer->getCompany() ?? $customer->getName());
|
||||
$vcard->addEmail($customer->getEmail());
|
||||
|
||||
$hasPref = false;
|
||||
|
||||
if ($customer->getPhone() !== null) {
|
||||
$hasPref = true;
|
||||
$vcard->addPhoneNumber($customer->getPhone(), 'PREF;WORK');
|
||||
}
|
||||
|
||||
if ($customer->getMobile() !== null) {
|
||||
$type = $hasPref ? 'CELL' : 'PREF;CELL';
|
||||
$vcard->addPhoneNumber($customer->getMobile(), $type);
|
||||
}
|
||||
|
||||
if ($customer->getFax() !== null) {
|
||||
$vcard->addPhoneNumber($customer->getFax(), 'FAX');
|
||||
}
|
||||
|
||||
if ($customer->getHomepage() !== null) {
|
||||
$vcard->addURL($customer->getHomepage(), 'WORK');
|
||||
}
|
||||
|
||||
$response = new Response($vcard->getOutput());
|
||||
|
||||
$disposition = $response->headers->makeDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
FileHelper::convertToAsciiFilename($customer->getName()) . '.vcf'
|
||||
);
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_customer_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
public function editRateAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($customer, $rate, $request, $repository, $this->generateUrl('admin_customer_rate_edit', ['id' => $customer->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_customer_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository): Response
|
||||
{
|
||||
$rate = new CustomerRate();
|
||||
$rate->setCustomer($customer);
|
||||
|
||||
return $this->rateFormAction($customer, $rate, $request, $repository, $this->generateUrl('admin_customer_rate_add', ['id' => $customer->getId()]));
|
||||
}
|
||||
|
||||
private function rateFormAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository, string $formUrl): Response
|
||||
{
|
||||
$form = $this->createForm(CustomerRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_customer_rate_add', ['id' => $customer->getId()]),
|
||||
'action' => $formUrl,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
@@ -379,24 +452,21 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('customer/rates.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', customer)")]
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*/
|
||||
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', customer)")]
|
||||
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService)
|
||||
{
|
||||
$stats = $statisticService->getCustomerStatistics($customer);
|
||||
@@ -431,15 +501,14 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('customer/delete.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'customer' => $customer,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="customer_export", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -465,12 +534,7 @@ final class CustomerController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
private function renderCustomerForm(Customer $customer, Request $request)
|
||||
private function renderCustomerForm(Customer $customer, Request $request, bool $create = false): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
@@ -481,13 +545,18 @@ final class CustomerController extends AbstractController
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($create) {
|
||||
return $this->redirectToRouteAfterCreate('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
@@ -495,23 +564,21 @@ final class CustomerController extends AbstractController
|
||||
|
||||
private function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(CustomerToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(CustomerToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
private function getCommentForm(Customer $customer, CustomerComment $comment): FormInterface
|
||||
private function getCommentForm(CustomerComment $comment): FormInterface
|
||||
{
|
||||
if (null === $comment->getId()) {
|
||||
$comment->setCustomer($customer);
|
||||
$comment->setCreatedBy($this->getUser());
|
||||
}
|
||||
|
||||
return $this->createForm(CustomerCommentForm::class, $comment, [
|
||||
'action' => $this->generateUrl('customer_comment_add', ['id' => $customer->getId()]),
|
||||
'action' => $this->generateUrl('customer_comment_add', ['id' => $comment->getCustomer()->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
@@ -534,4 +601,12 @@ final class CustomerController extends AbstractController
|
||||
'include_time' => $this->isGranted('time', $customer),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPageSetup(): PageSetup
|
||||
{
|
||||
$page = new PageSetup('customers');
|
||||
$page->setHelp('customer.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,145 +9,292 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\AuthorizedWidget;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\UserWidget;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Dashboard controller for the admin area.
|
||||
*
|
||||
* @Route(path="/dashboard")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class DashboardController extends AbstractController
|
||||
#[Route(path: '/dashboard')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
final class DashboardController extends AbstractController
|
||||
{
|
||||
public const BOOKMARK_TYPE = 'dashboard';
|
||||
public const BOOKMARK_NAME = 'default';
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
* @var WidgetInterface[]|null
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var WidgetService
|
||||
*/
|
||||
private $widgets;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $dashboard;
|
||||
private ?array $widgets = null;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param WidgetService $service
|
||||
* @param array $dashboard
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, WidgetService $service, array $dashboard)
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private WidgetService $service, private BookmarkRepository $repository)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->widgets = $service;
|
||||
$this->dashboard = $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={}, name="dashboard", methods={"GET"})
|
||||
* @param User $user
|
||||
* @return array<WidgetInterface>
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction()
|
||||
private function getAllAvailableWidgets(User $user): array
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if ($this->widgets === null) {
|
||||
$all = [];
|
||||
foreach ($this->service->getAllWidgets() as $widget) {
|
||||
$widget->setUser($user);
|
||||
|
||||
$event = new DashboardEvent($user);
|
||||
foreach ($this->dashboard as $widgetRow) {
|
||||
if (empty($widgetRow['widgets'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $widgetRow['permission'] && !$this->isGranted($widgetRow['permission'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($widgetRow['type'])) {
|
||||
$widgetRow['type'] = CompoundRow::class;
|
||||
}
|
||||
|
||||
if (!class_exists($widgetRow['type'])) {
|
||||
throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetRow['type']));
|
||||
}
|
||||
|
||||
$row = new $widgetRow['type']();
|
||||
if (!($row instanceof AbstractContainer)) {
|
||||
throw new WidgetException(
|
||||
sprintf(
|
||||
'Expected widget type to be an instanceof "%s", but found "%s"',
|
||||
AbstractContainer::class,
|
||||
$widgetRow['type']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$row->setTitle($widgetRow['title'] ?? '');
|
||||
$row->setOrder($widgetRow['order']);
|
||||
|
||||
foreach ($widgetRow['widgets'] as $widgetName) {
|
||||
if (!$this->widgets->hasWidget($widgetName)) {
|
||||
throw new \Exception(sprintf('Unknown widget "%s"', $widgetName));
|
||||
}
|
||||
|
||||
$widget = $this->widgets->getWidget($widgetName);
|
||||
|
||||
$add = true;
|
||||
if ($widget instanceof AuthorizedWidget) {
|
||||
$tmp = false;
|
||||
foreach ($widget->getPermissions() as $perm) {
|
||||
$permissions = $widget->getPermissions();
|
||||
if (\count($permissions) > 0) {
|
||||
$add = false;
|
||||
foreach ($permissions as $perm) {
|
||||
if ($this->isGranted($perm)) {
|
||||
$tmp = true;
|
||||
$add = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$add = $tmp;
|
||||
}
|
||||
|
||||
if ($widget instanceof UserWidget) {
|
||||
$widget->setUser($user);
|
||||
}
|
||||
|
||||
if ($add) {
|
||||
$row->addWidget($widget);
|
||||
if (!$add) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$all[] = $widget;
|
||||
}
|
||||
$this->widgets = $all;
|
||||
}
|
||||
|
||||
$event->addSection($row);
|
||||
return $this->widgets;
|
||||
}
|
||||
|
||||
private function getBookmark(User $user): ?Bookmark
|
||||
{
|
||||
return $this->repository->findBookmark($user, self::BOOKMARK_TYPE, self::BOOKMARK_NAME);
|
||||
}
|
||||
|
||||
private function getDefaultConfig(): array
|
||||
{
|
||||
$event = new DashboardEvent($this->getUser());
|
||||
|
||||
// default widgets
|
||||
$dashboard = [
|
||||
'PaginatedWorkingTimeChart',
|
||||
//'UserAmountToday',
|
||||
//'UserAmountWeek',
|
||||
//'UserAmountMonth',
|
||||
//'UserAmountYear',
|
||||
//'UserTeams',
|
||||
//'UserTeamProjects',
|
||||
'DurationToday',
|
||||
'DurationWeek',
|
||||
'DurationMonth',
|
||||
'DurationYear',
|
||||
//'ActiveUsersToday',
|
||||
//'ActiveUsersWeek',
|
||||
//'ActiveUsersMonth',
|
||||
//'ActiveUsersYear',
|
||||
//'AmountToday',
|
||||
//'AmountWeek',
|
||||
//'AmountMonth',
|
||||
//'AmountYear',
|
||||
//'TotalsUser',
|
||||
//'TotalsCustomer',
|
||||
//'TotalsProject',
|
||||
//'TotalsActivity',
|
||||
];
|
||||
|
||||
foreach ($dashboard as $widgetName) {
|
||||
$event->addWidget($widgetName);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$sections = $event->getSections();
|
||||
$clearedSections = [];
|
||||
/** @var WidgetContainerInterface $section */
|
||||
foreach ($sections as $key => $section) {
|
||||
if (!empty($section->getWidgets())) {
|
||||
$clearedSections[] = $section;
|
||||
return $event->getWidgets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of widgets names and options for a user.
|
||||
*
|
||||
* @param User $user
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function getUserConfig(User $user): array
|
||||
{
|
||||
$bookmark = $this->getBookmark($user);
|
||||
if ($bookmark !== null) {
|
||||
return $bookmark->getContent();
|
||||
}
|
||||
|
||||
$widgets = [];
|
||||
|
||||
foreach ($this->getDefaultConfig() as $name) {
|
||||
$widgets[] = ['id' => $name, 'options' => []];
|
||||
}
|
||||
|
||||
return $widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<WidgetInterface> $widgets
|
||||
* @param User $user
|
||||
* @return array<WidgetInterface>
|
||||
*/
|
||||
private function filterWidgets(array $widgets, User $user): array
|
||||
{
|
||||
$filteredWidgets = [];
|
||||
|
||||
foreach ($this->getUserConfig($user) as $setting) {
|
||||
$id = $setting['id'];
|
||||
$options = $setting['options'];
|
||||
foreach ($widgets as $widget) {
|
||||
if ($widget->getId() === $id) {
|
||||
$tmpWidget = clone $widget;
|
||||
foreach ($options as $key => $value) {
|
||||
$tmpWidget->setOption($key, $value);
|
||||
}
|
||||
$filteredWidgets[] = $tmpWidget;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uasort(
|
||||
$clearedSections,
|
||||
function (WidgetContainerInterface $a, WidgetContainerInterface $b) {
|
||||
if ($a->getOrder() == $b->getOrder()) {
|
||||
return 0;
|
||||
}
|
||||
return $filteredWidgets;
|
||||
}
|
||||
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
}
|
||||
);
|
||||
#[Route(path: '/', defaults: [], name: 'dashboard', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$available = $this->getAllAvailableWidgets($user);
|
||||
$widgets = $this->filterWidgets($available, $user);
|
||||
|
||||
$page = new PageSetup('dashboard.title');
|
||||
$page->setHelp('dashboard.html');
|
||||
$page->setActionName('dashboard');
|
||||
$page->setActionPayload(['widgets' => $widgets, 'available' => $available]);
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'widgets' => $clearedSections
|
||||
'page_setup' => $page,
|
||||
'widgets' => $widgets,
|
||||
'available' => $available,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/reset/', defaults: [], name: 'dashboard_reset', methods: ['GET', 'POST'])]
|
||||
public function reset(): RedirectResponse
|
||||
{
|
||||
$bookmark = $this->getBookmark($this->getUser());
|
||||
if ($bookmark !== null) {
|
||||
$this->repository->deleteBookmark($bookmark);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('dashboard');
|
||||
}
|
||||
|
||||
#[Route(path: '/add-widget/{widget}', defaults: [], name: 'dashboard_add', methods: ['GET'])]
|
||||
public function add(string $widget): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$widgets = $this->getUserConfig($user);
|
||||
|
||||
// prevent to add the same widget multiple times
|
||||
foreach ($widgets as $id => $setting) {
|
||||
if ($setting['id'] === $widget) {
|
||||
return $this->redirectToRoute('dashboard_edit');
|
||||
}
|
||||
}
|
||||
|
||||
$widgets[] = ['id' => $widget, 'options' => []];
|
||||
|
||||
$this->saveBookmark($user, $widgets);
|
||||
|
||||
return $this->redirectToRoute('dashboard_edit');
|
||||
}
|
||||
|
||||
private function saveBookmark(User $user, array $widgets): void
|
||||
{
|
||||
$bookmark = $this->getBookmark($user);
|
||||
if ($bookmark === null) {
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setUser($user);
|
||||
$bookmark->setType(self::BOOKMARK_TYPE);
|
||||
$bookmark->setName(self::BOOKMARK_NAME);
|
||||
}
|
||||
$bookmark->setContent($widgets);
|
||||
|
||||
$this->repository->saveBookmark($bookmark);
|
||||
}
|
||||
|
||||
#[Route(path: '/edit/', defaults: [], name: 'dashboard_edit', methods: ['GET', 'POST'])]
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$available = $this->getAllAvailableWidgets($user);
|
||||
$widgets = $this->filterWidgets($available, $user);
|
||||
|
||||
$choices = [];
|
||||
|
||||
foreach ($available as $widget) {
|
||||
if (empty($widget->getTitle())) {
|
||||
continue;
|
||||
}
|
||||
$choices[$widget->getId()] = $widget->getId();
|
||||
}
|
||||
|
||||
$form = $this->createFormBuilder(null, [])
|
||||
->add('widgets', ChoiceType::class, ['choices' => $choices, 'multiple' => true])
|
||||
->setAction($this->generateUrl('dashboard_edit'))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$userWidgets = $this->getUserConfig($user);
|
||||
$saveWidgets = [];
|
||||
foreach ($form->getData()['widgets'] as $widgetId) {
|
||||
$options = [];
|
||||
foreach ($userWidgets as $setting) {
|
||||
if ($setting['id'] === $widgetId) {
|
||||
$options = $setting['options'];
|
||||
}
|
||||
}
|
||||
$saveWidgets[] = ['id' => $widgetId, 'options' => $options];
|
||||
}
|
||||
|
||||
$this->saveBookmark($user, $saveWidgets);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('dashboard');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('dashboard.title');
|
||||
$page->setHelp('dashboard.html');
|
||||
$page->setActionName('dashboard');
|
||||
$page->setActionView('edit');
|
||||
$page->setActionPayload(['widgets' => $widgets, 'available' => $available]);
|
||||
|
||||
return $this->render('dashboard/grid.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'widgets' => $widgets,
|
||||
'available' => $available,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,26 +10,25 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Utils\FileHelper;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Utils\ReleaseVersion;
|
||||
use Composer\InstalledVersions;
|
||||
use PackageVersions\Versions;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/doctor")
|
||||
* @Security("is_granted('system_information')")
|
||||
*/
|
||||
class DoctorController extends AbstractController
|
||||
#[Route(path: '/doctor')]
|
||||
#[Security("is_granted('system_information')")]
|
||||
final class DoctorController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* PHP extensions which Kimai needs for runtime.
|
||||
* Some are not a hard requiremenet, but some functions might not work as expected.
|
||||
* Required PHP extensions for Kimai.
|
||||
*/
|
||||
public const REQUIRED_EXTENSIONS = [
|
||||
'gd',
|
||||
'intl',
|
||||
'json',
|
||||
'mbstring',
|
||||
@@ -47,21 +46,12 @@ class DoctorController extends AbstractController
|
||||
'var/log/',
|
||||
];
|
||||
|
||||
private $projectDirectory;
|
||||
private $environment;
|
||||
private $fileHelper;
|
||||
|
||||
public function __construct(string $projectDirectory, string $kernelEnvironment, FileHelper $fileHelper)
|
||||
public function __construct(private string $projectDirectory, private string $kernelEnvironment, private FileHelper $fileHelper, private CacheInterface $cache)
|
||||
{
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
$this->environment = $kernelEnvironment;
|
||||
$this->fileHelper = $fileHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/flush-log/{token}", name="doctor_flush_log", methods={"GET"})
|
||||
* @Security("is_granted('system_configuration')")
|
||||
*/
|
||||
#[Route(path: '/flush-log/{token}', name: 'doctor_flush_log', methods: ['GET'])]
|
||||
#[Security("is_granted('system_configuration')")]
|
||||
public function deleteLogfileAction(string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('doctor.flush_log', $token))) {
|
||||
@@ -76,10 +66,10 @@ class DoctorController extends AbstractController
|
||||
|
||||
if (file_exists($logfile)) {
|
||||
if (!is_writable($logfile)) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Logfile cannot be written']);
|
||||
$this->flashError('action.delete.error', 'Logfile cannot be written');
|
||||
} else {
|
||||
if (false === file_put_contents($logfile, '')) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => 'Failed writing to logfile']);
|
||||
$this->flashError('action.delete.error', 'Failed writing to logfile');
|
||||
} else {
|
||||
$this->flashSuccess('action.delete.success');
|
||||
}
|
||||
@@ -89,32 +79,34 @@ class DoctorController extends AbstractController
|
||||
return $this->redirectToRoute('doctor');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="doctor", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '', name: 'doctor', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$logLines = 100;
|
||||
|
||||
$canDeleteLogfile = $this->isGranted('system_configuration') && is_writable($this->getLogFilename());
|
||||
$page = new PageSetup('Doctor');
|
||||
$page->setHelp('doctor.html');
|
||||
|
||||
return $this->render('doctor/index.html.twig', array_merge(
|
||||
[
|
||||
'modules' => get_loaded_extensions(),
|
||||
'environment' => $this->environment,
|
||||
'info' => $this->getPhpInfo(),
|
||||
'settings' => $this->getIniSettings(),
|
||||
'extensions' => $this->getLoadedExtensions(),
|
||||
'directories' => $this->getFilePermissions(),
|
||||
'log_delete' => $canDeleteLogfile,
|
||||
'logs' => $this->getLog(),
|
||||
'logLines' => $logLines,
|
||||
'logSize' => $this->getLogSize(),
|
||||
'composer' => $this->getComposerPackages(),
|
||||
]
|
||||
));
|
||||
return $this->render('doctor/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'modules' => get_loaded_extensions(),
|
||||
'environment' => $this->kernelEnvironment,
|
||||
'info' => $this->getPhpInfo(),
|
||||
'settings' => $this->getIniSettings(),
|
||||
'extensions' => $this->getLoadedExtensions(),
|
||||
'directories' => $this->getFilePermissions(),
|
||||
'log_delete' => $canDeleteLogfile,
|
||||
'logs' => $this->getLog(),
|
||||
'logLines' => $logLines,
|
||||
'logSize' => $this->getLogSize(),
|
||||
'composer' => $this->getComposerPackages(),
|
||||
'release' => $this->getNextUpdateVersion()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getComposerPackages(): array
|
||||
{
|
||||
$versions = [];
|
||||
@@ -124,35 +116,30 @@ class DoctorController extends AbstractController
|
||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
}
|
||||
} else {
|
||||
@trigger_error('Please upgrade your Composer to 2.x', E_USER_DEPRECATED);
|
||||
|
||||
// @deprecated since 1.14, will be removed with 2.0
|
||||
$rootPackage = Versions::rootPackageName();
|
||||
foreach (Versions::VERSIONS as $name => $version) {
|
||||
$versions[$name] = explode('@', $version)[0];
|
||||
}
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
ksort($versions);
|
||||
}
|
||||
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage) {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
ksort($versions);
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
private function getLoadedExtensions()
|
||||
/**
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
private function getLoadedExtensions(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
@@ -175,7 +162,7 @@ class DoctorController extends AbstractController
|
||||
|
||||
private function getLogFilename(): string
|
||||
{
|
||||
$logfileName = 'var/log/' . $this->environment . '.log';
|
||||
$logfileName = 'var/log/' . $this->kernelEnvironment . '.log';
|
||||
|
||||
return $this->projectDirectory . '/' . $logfileName;
|
||||
}
|
||||
@@ -218,7 +205,7 @@ class DoctorController extends AbstractController
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getFilePermissions()
|
||||
private function getFilePermissions(): array
|
||||
{
|
||||
$testPaths = [];
|
||||
$baseDir = $this->projectDirectory . DIRECTORY_SEPARATOR;
|
||||
@@ -241,7 +228,7 @@ class DoctorController extends AbstractController
|
||||
foreach ($testPaths as $fullUri) {
|
||||
$fullUri = rtrim($fullUri, DIRECTORY_SEPARATOR);
|
||||
$tmp = str_replace($baseDir, '', $fullUri) . DIRECTORY_SEPARATOR;
|
||||
if ($fullUri !== false && is_readable($fullUri) && is_writable($fullUri)) {
|
||||
if (is_readable($fullUri) && is_writable($fullUri)) {
|
||||
$results[$tmp] = true;
|
||||
} else {
|
||||
$results[$tmp] = false;
|
||||
@@ -251,9 +238,13 @@ class DoctorController extends AbstractController
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function getIniSettings()
|
||||
private function getIniSettings(): array
|
||||
{
|
||||
$ini = [
|
||||
'memory_limit',
|
||||
'session.gc_maxlifetime',
|
||||
'max_execution_time',
|
||||
'date.timezone',
|
||||
'allow_url_fopen',
|
||||
'allow_url_include',
|
||||
'default_charset',
|
||||
@@ -262,8 +253,6 @@ class DoctorController extends AbstractController
|
||||
'error_log',
|
||||
'error_reporting',
|
||||
'log_errors',
|
||||
'max_execution_time',
|
||||
'memory_limit',
|
||||
'open_basedir',
|
||||
'post_max_size',
|
||||
'sys_temp_dir',
|
||||
@@ -274,7 +263,7 @@ class DoctorController extends AbstractController
|
||||
$settings = [];
|
||||
foreach ($ini as $name) {
|
||||
try {
|
||||
$settings[$name] = ini_get($name);
|
||||
$settings[$name] = \ini_get($name);
|
||||
} catch (\Exception $ex) {
|
||||
$settings[$name] = "Couldn't load ini setting: " . $ex->getMessage();
|
||||
}
|
||||
@@ -287,9 +276,9 @@ class DoctorController extends AbstractController
|
||||
* @author https://php.net/manual/en/function.phpinfo.php#117961
|
||||
* @return array
|
||||
*/
|
||||
private function getPhpInfo()
|
||||
private function getPhpInfo(): array
|
||||
{
|
||||
$plainText = function ($input) {
|
||||
$plainText = function ($input): string {
|
||||
return trim(html_entity_decode(strip_tags($input)));
|
||||
};
|
||||
|
||||
@@ -323,4 +312,24 @@ class DoctorController extends AbstractController
|
||||
|
||||
return $phpInfo;
|
||||
}
|
||||
|
||||
private function getNextUpdateVersion(): ?array
|
||||
{
|
||||
return $this->cache->get('kimai.update_release', function (ItemInterface $item) {
|
||||
// we cache the result, no matter if the call failed: at the end, this is "just"
|
||||
// an update note but an expensive call
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
try {
|
||||
$version = new ReleaseVersion();
|
||||
|
||||
return $version->getLatestReleaseFromGithub(true);
|
||||
} catch (\Exception $ex) {
|
||||
// something failed, retry tomorrow
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Export\Base\DispositionInlineInterface;
|
||||
use App\Export\ExportItemInterface;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Export\TooManyItemsExportException;
|
||||
use App\Form\Toolbar\ExportToolbarForm;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -23,25 +24,16 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to export timesheet data.
|
||||
*
|
||||
* @Route(path="/export")
|
||||
* @Security("is_granted('create_export')")
|
||||
*/
|
||||
class ExportController extends AbstractController
|
||||
#[Route(path: '/export')]
|
||||
#[Security("is_granted('create_export')")]
|
||||
final class ExportController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ServiceExport
|
||||
*/
|
||||
private $export;
|
||||
|
||||
public function __construct(ServiceExport $export)
|
||||
public function __construct(private ServiceExport $export)
|
||||
{
|
||||
$this->export = $export;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="export", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', name: 'export', methods: ['GET'])]
|
||||
public function indexAction(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
@@ -84,7 +76,11 @@ class ExportController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('export');
|
||||
$page->setHelp('export.html');
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'too_many' => $tooManyResults,
|
||||
'by_customer' => $byCustomer,
|
||||
'query' => $query,
|
||||
@@ -97,9 +93,7 @@ class ExportController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/data", name="export_data", methods={"POST"})
|
||||
*/
|
||||
#[Route(path: '/data', name: 'export_data', methods: ['POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
@@ -133,7 +127,7 @@ class ExportController extends AbstractController
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function getDefaultQuery(): ExportQuery
|
||||
private function getDefaultQuery(): ExportQuery
|
||||
{
|
||||
$begin = $this->getDateTimeFactory()->getStartOfMonth();
|
||||
$end = $this->getDateTimeFactory()->getEndOfMonth();
|
||||
@@ -148,10 +142,10 @@ class ExportController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param ExportQuery $query
|
||||
* @return ExportItemInterface[]
|
||||
* @return ExportableItem[]
|
||||
* @throws TooManyItemsExportException
|
||||
*/
|
||||
protected function getEntries(ExportQuery $query): array
|
||||
private function getEntries(ExportQuery $query): array
|
||||
{
|
||||
if (null !== $query->getBegin()) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
@@ -163,9 +157,9 @@ class ExportController extends AbstractController
|
||||
return $this->export->getExportItems($query);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(ExportQuery $query, string $method): FormInterface
|
||||
private function getToolbarForm(ExportQuery $query, string $method): FormInterface
|
||||
{
|
||||
return $this->createForm(ExportToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(ExportToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('export', []),
|
||||
'include_user' => $this->isGranted('view_other_timesheet'),
|
||||
'include_export' => $this->isGranted('edit_export_other_timesheet'),
|
||||
|
||||
46
src/Controller/FavoriteController.php
Normal file
46
src/Controller/FavoriteController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\FavoriteRecordService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route(path: '/favorite')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
final class FavoriteController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function favoriteAction(): Response
|
||||
{
|
||||
return $this->render('partials/recent-activities.html.twig');
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||
{
|
||||
$favoriteRecordService->addFavorite($timesheet);
|
||||
|
||||
return $this->render('partials/recent-activities.html.twig');
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||
{
|
||||
$favoriteRecordService->removeFavorite($timesheet);
|
||||
|
||||
return $this->render('partials/recent-activities.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\InitialViewType;
|
||||
use App\Utils\LanguageService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -19,20 +18,19 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Homepage controller is a redirect controller with user specific logic.
|
||||
*
|
||||
* @Route(path="/homepage")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class HomepageController extends AbstractController
|
||||
#[Route(path: '/homepage')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
final class HomepageController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="", defaults={}, name="homepage", methods={"GET"})
|
||||
*/
|
||||
public function indexAction(Request $request, LanguageService $service): Response
|
||||
public const DEFAULT_ROUTE = 'timesheet';
|
||||
|
||||
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
|
||||
public function indexAction(Request $request, LocaleService $service): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$userRoute = $user->getPreferenceValue('login.initial_view', InitialViewType::DEFAULT_VIEW, false);
|
||||
$userRoute = $user->getPreferenceValue('login_initial_view', self::DEFAULT_ROUTE, false);
|
||||
$userLanguage = $user->getLanguage();
|
||||
$requestLanguage = $request->getLocale();
|
||||
|
||||
@@ -46,16 +44,16 @@ class HomepageController extends AbstractController
|
||||
|
||||
// if a user somehow managed to get a wrong locale into hos account (eg. an imported user from Kimai 1)
|
||||
// make sure that he will still see a beautiful page and not a 404
|
||||
if (!$service->isKnownLanguage($userLanguage)) {
|
||||
$userLanguage = $service->getDefaultLanguage();
|
||||
if (!$service->isKnownLocale($userLanguage)) {
|
||||
$userLanguage = $service->getDefaultLocale();
|
||||
}
|
||||
|
||||
$routes = [
|
||||
[$userRoute, $userLanguage],
|
||||
[$userRoute, $requestLanguage],
|
||||
[$userRoute, User::DEFAULT_LANGUAGE],
|
||||
[InitialViewType::DEFAULT_VIEW, $userLanguage],
|
||||
[InitialViewType::DEFAULT_VIEW, $requestLanguage],
|
||||
[self::DEFAULT_ROUTE, $userLanguage],
|
||||
[self::DEFAULT_ROUTE, $requestLanguage],
|
||||
];
|
||||
|
||||
foreach ($routes as $routeSettings) {
|
||||
@@ -69,6 +67,6 @@ class HomepageController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute(InitialViewType::DEFAULT_VIEW, ['_locale' => User::DEFAULT_LANGUAGE]);
|
||||
return $this->redirectToRoute(self::DEFAULT_ROUTE, ['_locale' => User::DEFAULT_LANGUAGE]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Event\InvoiceCreatedMultipleEvent;
|
||||
use App\Event\InvoiceDocumentsEvent;
|
||||
use App\Event\InvoiceMetaDefinitionEvent;
|
||||
use App\Event\InvoiceMetaDisplayEvent;
|
||||
@@ -26,16 +24,21 @@ use App\Form\InvoiceEditForm;
|
||||
use App\Form\InvoiceTemplateForm;
|
||||
use App\Form\Toolbar\InvoiceArchiveForm;
|
||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||
use App\Form\Toolbar\InvoiceToolbarSimpleForm;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use App\Form\Type\InvoiceTemplateType;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\InvoiceArchiveQuery;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -47,30 +50,22 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
#[Route(path: '/invoice')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_invoice')")]
|
||||
final class InvoiceController extends AbstractController
|
||||
{
|
||||
private $service;
|
||||
private $templateRepository;
|
||||
private $invoiceRepository;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ServiceInvoice $service, InvoiceTemplateRepository $templateRepository, InvoiceRepository $invoiceRepository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->templateRepository = $templateRepository;
|
||||
$this->invoiceRepository = $invoiceRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
public function __construct(
|
||||
private ServiceInvoice $service,
|
||||
private InvoiceTemplateRepository $templateRepository,
|
||||
private InvoiceRepository $invoiceRepository,
|
||||
private EventDispatcherInterface $dispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
public function indexAction(Request $request, SystemConfiguration $configuration, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
#[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_invoice')")]
|
||||
public function indexAction(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
@@ -81,13 +76,7 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
$token = null;
|
||||
if ($request->query->has('token')) {
|
||||
$token = $request->query->get('token');
|
||||
$request->query->remove('token');
|
||||
}
|
||||
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$form = $this->getToolbarForm($query);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
@@ -96,53 +85,50 @@ final class InvoiceController extends AbstractController
|
||||
$total = 0;
|
||||
$searched = false;
|
||||
|
||||
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||
if ($request->query->has('createInvoice')) {
|
||||
if (!$this->isCsrfTokenValid('invoice.create', $token)) {
|
||||
$this->flashError('action.csrf.error');
|
||||
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
$csrfTokenManager->refreshToken('invoice.create');
|
||||
|
||||
try {
|
||||
return $this->renderInvoice($query, $request);
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', ['%reason%' => 'check doctor/logs']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->get('template')->getData() !== null) {
|
||||
try {
|
||||
$models = $this->service->createModels($query);
|
||||
$searched = true;
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError($ex->getMessage());
|
||||
}
|
||||
if ($form->isValid() && $query->getTemplate() !== null) {
|
||||
try {
|
||||
$models = $this->service->createModels($query);
|
||||
$searched = true;
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
$forms = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$customer = $model->getCustomer();
|
||||
$customerTpl = $model->getTemplate();
|
||||
$total += \count($model->getCalculator()->getEntries());
|
||||
|
||||
$values = [
|
||||
'invoiceDate' => $query->getInvoiceDate(),
|
||||
'template' => $customerTpl
|
||||
];
|
||||
|
||||
$forms[] = $this->createFormWithName('customer_' . $customer->getId(), FormType::class, $values, [
|
||||
'csrf_protection' => false,
|
||||
])
|
||||
->add('template', InvoiceTemplateType::class)
|
||||
->add('invoiceDate', DatePickerType::class, [
|
||||
'required' => true,
|
||||
])
|
||||
->createView();
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'models' => $models,
|
||||
'forms' => $forms,
|
||||
'form' => $form->createView(),
|
||||
'limit_preview' => ($total > 500),
|
||||
'searched' => $searched,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/preview/{customer}/{token}", name="invoice_preview", methods={"GET"})
|
||||
* @Security("is_granted('access', customer)")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*/
|
||||
public function previewAction(Customer $customer, string $token, Request $request, SystemConfiguration $configuration): Response
|
||||
#[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])]
|
||||
#[Security("is_granted('access', customer) and is_granted('create_invoice')")]
|
||||
public function previewAction(Customer $customer, string $token, Request $request): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
@@ -158,7 +144,8 @@ final class InvoiceController extends AbstractController
|
||||
// so the new token would not be loaded
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$query->setAllowTemplateOverwrite(false);
|
||||
$form = $this->getToolbarForm($query);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
@@ -168,10 +155,9 @@ final class InvoiceController extends AbstractController
|
||||
$query->setCustomers([$customer]);
|
||||
$model = $this->service->createModel($query);
|
||||
|
||||
return $this->service->renderInvoiceWithModel($model, $this->dispatcher, true);
|
||||
return $this->service->renderInvoice($model, $this->dispatcher, true);
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
} else {
|
||||
$this->flashFormError($form);
|
||||
@@ -180,12 +166,9 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/save-invoice/{customer}/{template}/{token}", name="invoice_create", methods={"GET"})
|
||||
* @Security("is_granted('access', customer)")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*/
|
||||
public function createInvoiceAction(Customer $customer, InvoiceTemplate $template, string $token, Request $request, SystemConfiguration $configuration, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
#[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])]
|
||||
#[Security("is_granted('access', customer) and is_granted('create_invoice')")]
|
||||
public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
@@ -198,28 +181,40 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$query->setAllowTemplateOverwrite(false);
|
||||
$form = $this->getToolbarForm($query);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
if ($form->isValid()) {
|
||||
$query->setTemplate($template);
|
||||
$query->setCustomers([$customer]);
|
||||
try {
|
||||
$query->setCustomers([$customer]);
|
||||
$model = $this->service->createModel($query);
|
||||
|
||||
return $this->renderInvoice($query, $request);
|
||||
// save default template for customer if not yet set
|
||||
if ($customer->getInvoiceTemplate() === null) {
|
||||
$customer->setInvoiceTemplate($query->getTemplate());
|
||||
$customerRepository->saveCustomer($customer);
|
||||
}
|
||||
|
||||
$invoice = $this->service->createInvoice($model, $this->dispatcher);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoice->getId()]);
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
} else {
|
||||
$this->flashFormError($form);
|
||||
}
|
||||
|
||||
$this->flashFormError($form);
|
||||
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/change-status/{id}/{status}/{token}", name="admin_invoice_status", methods={"GET", "POST"})
|
||||
* @Security("is_granted('access', invoice.getCustomer())")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*/
|
||||
#[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")]
|
||||
public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||
@@ -238,6 +233,7 @@ final class InvoiceController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
return $this->render('invoice/invoice_edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'invoice' => $invoice,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
@@ -253,11 +249,8 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/edit/{id}", name="admin_invoice_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('access', invoice.getCustomer())")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*/
|
||||
#[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")]
|
||||
public function editAction(Invoice $invoice, Request $request): Response
|
||||
{
|
||||
$form = $this->createInvoiceEditForm($invoice);
|
||||
@@ -267,24 +260,22 @@ final class InvoiceController extends AbstractController
|
||||
try {
|
||||
$this->invoiceRepository->saveInvoice($invoice);
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
} catch (Exception $ex) {
|
||||
$this->handleFormUpdateException($ex, $form);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('invoice/invoice_edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'invoice' => $invoice,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/delete/{id}/{token}", name="admin_invoice_delete", methods={"GET"})
|
||||
* @Security("is_granted('access', invoice.getCustomer())")
|
||||
* @Security("is_granted('delete_invoice')")
|
||||
*/
|
||||
#[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('delete_invoice')")]
|
||||
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||
@@ -305,11 +296,8 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/download/{id}", name="admin_invoice_download", methods={"GET"})
|
||||
* @Security("is_granted('access', invoice.getCustomer())")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*/
|
||||
#[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])]
|
||||
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('view_invoice')")]
|
||||
public function downloadAction(Invoice $invoice): Response
|
||||
{
|
||||
$file = $this->service->getInvoiceFile($invoice);
|
||||
@@ -323,10 +311,8 @@ final class InvoiceController extends AbstractController
|
||||
return $this->file($file->getRealPath(), $file->getBasename());
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/show/{page}", defaults={"page": 1}, requirements={"page": "[1-9]\d*"}, name="admin_invoice_list", methods={"GET"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
#[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])]
|
||||
#[Security("is_granted('view_invoice')")]
|
||||
public function showInvoicesAction(Request $request, int $page): Response
|
||||
{
|
||||
$invoice = null;
|
||||
@@ -344,21 +330,48 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
}
|
||||
|
||||
$invoices = $this->invoiceRepository->getPagerfantaForQuery($query);
|
||||
$entries = $this->invoiceRepository->getPagerfantaForQuery($query);
|
||||
$metaColumns = $this->findMetaColumns($query);
|
||||
|
||||
$table = new DataTable('invoices', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_invoice_list');
|
||||
$table->setReloadEvents('kimai.invoiceUpdate');
|
||||
|
||||
$table->addColumn('avatar', ['class' => 'text-nowrap w-avatar d-none d-md-table-cell', 'title' => false, 'orderBy' => false]);
|
||||
$table->addColumn('date', ['class' => 'd-none d-sm-table-cell text-nowrap w-min']);
|
||||
$table->addColumn('user', ['class' => 'd-none text-nowrap w-min', 'orderBy' => false]);
|
||||
$table->addColumn('customer', ['class' => 'alwaysVisible text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('comment', ['class' => 'd-none', 'title' => 'description']);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false]);
|
||||
}
|
||||
|
||||
$table->addColumn('invoice_number', ['class' => 'd-none d-md-table-cell w-min', 'title' => 'invoice.number', 'orderBy' => false]);
|
||||
$table->addColumn('due_date', ['class' => 'd-none w-min', 'title' => 'invoice.due_days', 'orderBy' => false]);
|
||||
$table->addColumn('payment_date', ['class' => 'd-none w-min', 'title' => 'invoice.payment_date', 'orderBy' => false]);
|
||||
$table->addColumn('status', ['class' => 'd-none d-sm-table-cell w-min', 'orderBy' => false]);
|
||||
$table->addColumn('subtotal', ['class' => 'd-none text-end w-min', 'title' => 'invoice.subtotal', 'orderBy' => false]);
|
||||
$table->addColumn('tax', ['class' => 'd-none text-end w-min', 'title' => 'invoice.tax']);
|
||||
$table->addColumn('total_rate', ['class' => 'd-none d-md-table-cell text-end w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup('all_invoices');
|
||||
$page->setDataTable($table);
|
||||
$page->setActionName('invoice_archive');
|
||||
|
||||
return $this->render('invoice/listing.html.twig', [
|
||||
'entries' => $invoices,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'download' => $invoice,
|
||||
'metaColumns' => $this->findMetaColumns($query),
|
||||
'metaColumns' => $metaColumns,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="invoice_export", methods={"GET"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
||||
#[Security("is_granted('view_invoice')")]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
@@ -380,35 +393,52 @@ final class InvoiceController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{page}", requirements={"page": "[1-9]\d*"}, defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
#[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function listTemplateAction(int $page): Response
|
||||
{
|
||||
$query = new BaseQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$templates = $this->templateRepository->getPagerfantaForQuery($query);
|
||||
$entries = $this->templateRepository->getPagerfantaForQuery($query);
|
||||
|
||||
$table = new DataTable('invoice_template', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setPaginationRoute('admin_invoice_template');
|
||||
$table->setReloadEvents('kimai.invoiceTemplateUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]);
|
||||
$table->addColumn('title', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('company', ['class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('vat_id', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('tax_rate', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('due_days', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('address', ['class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('contact', ['class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('calculator', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_calculator', 'translation_domain' => 'invoice-calculator']);
|
||||
$table->addColumn('renderer', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_renderer', 'translation_domain' => 'invoice-renderer']);
|
||||
$table->addColumn('language', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions', 'orderBy' => false]);
|
||||
|
||||
$page = $this->createPageSetup('admin_invoice_template.title');
|
||||
$page->setDataTable($table);
|
||||
$page->setActionName('invoice_templates');
|
||||
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
#[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/document_upload", name="admin_invoice_document_upload", methods={"GET", "POST"})
|
||||
* @Security("is_granted('upload_invoice_template')")
|
||||
*/
|
||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('upload_invoice_template')")]
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
|
||||
{
|
||||
$dir = $documentRepository->getUploadDirectory();
|
||||
@@ -450,7 +480,7 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
if (!file_exists($invoiceDir)) {
|
||||
@mkdir($invoiceDir, 0777);
|
||||
@mkdir($invoiceDir, 0o777);
|
||||
}
|
||||
|
||||
if (!is_dir($invoiceDir)) {
|
||||
@@ -494,7 +524,10 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$page = $this->createPageSetup('admin_invoice_template.title');
|
||||
|
||||
return $this->render('invoice/document_upload.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'error_replacer' => ['%max%' => $event->getMaximumAllowedDocuments(), '%dir%' => $dir],
|
||||
'upload_error' => $uploadError,
|
||||
'can_upload' => $canUpload,
|
||||
@@ -504,10 +537,8 @@ final class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/document/{id}/delete/{token}", name="invoice_document_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
#[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response
|
||||
{
|
||||
$document = $documentRepository->findByName($id);
|
||||
@@ -549,30 +580,38 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||
#[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
return $this->createTemplate($request, $copyFrom);
|
||||
}
|
||||
|
||||
#[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function createTemplateAction(Request $request): Response
|
||||
{
|
||||
return $this->createTemplate($request, null);
|
||||
}
|
||||
|
||||
private function createTemplate(Request $request, ?InvoiceTemplate $copyFrom = null): Response
|
||||
{
|
||||
$template = new InvoiceTemplate();
|
||||
$template->setLanguage($request->getLocale());
|
||||
|
||||
if (null !== $copyFrom) {
|
||||
$template = clone $copyFrom;
|
||||
$template->setName('Copy of ' . $copyFrom->getName());
|
||||
$template->setName($copyFrom->getName() . ' (1)');
|
||||
}
|
||||
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/delete/{token}", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function deleteTemplate(InvoiceTemplate $template, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
#[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_invoice_template')")]
|
||||
public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $token))) {
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) {
|
||||
$this->flashError('action.csrf.error');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
@@ -599,6 +638,7 @@ final class InvoiceController extends AbstractController
|
||||
$query = new InvoiceQuery();
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setInvoiceDate($factory->createDateTime());
|
||||
// limit access to data from teams
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
@@ -610,32 +650,6 @@ final class InvoiceController extends AbstractController
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function renderInvoice(InvoiceQuery $query, Request $request)
|
||||
{
|
||||
// use the current request locale as fallback, if no translation was configured
|
||||
if (null !== $query->getTemplate() && null === $query->getTemplate()->getLanguage()) {
|
||||
$query->getTemplate()->setLanguage($request->getLocale());
|
||||
}
|
||||
|
||||
try {
|
||||
$invoices = $this->service->createInvoices($query, $this->dispatcher);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if (\count($invoices) === 1) {
|
||||
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoices[0]->getId()]);
|
||||
} elseif (\count($invoices) > 1) {
|
||||
$this->dispatcher->dispatch(new InvoiceCreatedMultipleEvent($invoices));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
private function flashFormError(FormInterface $form): void
|
||||
{
|
||||
$err = '';
|
||||
@@ -643,7 +657,7 @@ final class InvoiceController extends AbstractController
|
||||
$err .= PHP_EOL . '[' . $error->getOrigin()->getName() . '] ' . $error->getMessage();
|
||||
}
|
||||
|
||||
$this->flashError('action.update.error', ['%reason%' => $err]);
|
||||
$this->flashError('action.update.error', $err);
|
||||
}
|
||||
|
||||
private function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
@@ -659,25 +673,24 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
$page = $this->createPageSetup('admin_invoice_template.title');
|
||||
|
||||
return $this->render('invoice/template_edit.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'template' => $template,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getToolbarForm(InvoiceQuery $query, bool $simple): FormInterface
|
||||
private function getToolbarForm(InvoiceQuery $query): FormInterface
|
||||
{
|
||||
$form = $simple ? InvoiceToolbarSimpleForm::class : InvoiceToolbarForm::class;
|
||||
|
||||
return $this->createForm($form, $query, [
|
||||
return $this->createSearchForm(InvoiceToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'GET',
|
||||
'include_user' => $this->isGranted('view_other_timesheet'),
|
||||
'include_export' => $this->isGranted('edit_export_other_timesheet'),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
@@ -687,9 +700,8 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
private function getArchiveToolbarForm(InvoiceArchiveQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(InvoiceArchiveForm::class, $query, [
|
||||
return $this->createSearchForm(InvoiceArchiveForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_invoice_list', []),
|
||||
'method' => 'GET',
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'attr' => [
|
||||
'id' => 'invoice-archive-form'
|
||||
@@ -734,4 +746,12 @@ final class InvoiceController extends AbstractController
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPageSetup(string $title = 'invoices'): PageSetup
|
||||
{
|
||||
$page = new PageSetup($title);
|
||||
$page->setHelp('invoices.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use App\Repository\RoleRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Security\RoleService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -31,38 +32,20 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage user roles and role permissions.
|
||||
*
|
||||
* @Route(path="/admin/permissions")
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
#[Route(path: '/admin/permissions')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('role_permissions')")]
|
||||
final class PermissionController extends AbstractController
|
||||
{
|
||||
public const TOKEN_NAME = 'user_role_permissions';
|
||||
/**
|
||||
* @var RoleService
|
||||
*/
|
||||
private $roleService;
|
||||
/**
|
||||
* @var RolePermissionManager
|
||||
*/
|
||||
private $manager;
|
||||
/**
|
||||
* @var RoleRepository
|
||||
*/
|
||||
private $roleRepository;
|
||||
|
||||
public function __construct(RoleService $roleService, RolePermissionManager $manager, RoleRepository $roleRepository)
|
||||
public function __construct(private RolePermissionManager $manager, private RoleRepository $roleRepository)
|
||||
{
|
||||
$this->roleService = $roleService;
|
||||
$this->manager = $manager;
|
||||
$this->roleRepository = $roleRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="admin_user_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService)
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
$existing = [];
|
||||
@@ -74,8 +57,7 @@ final class PermissionController extends AbstractController
|
||||
$existing = array_map('strtoupper', $existing);
|
||||
|
||||
// automatically import all hard coded (default) roles into the database table
|
||||
foreach ($this->roleService->getAvailableNames() as $roleName) {
|
||||
$roleName = strtoupper($roleName);
|
||||
foreach ($roleService->getAvailableNames() as $roleName) {
|
||||
if (!\in_array($roleName, $existing)) {
|
||||
$role = new Role();
|
||||
$role->setName($roleName);
|
||||
@@ -152,6 +134,9 @@ final class PermissionController extends AbstractController
|
||||
foreach ($all as $role) {
|
||||
$roles[$role->getName()] = $role;
|
||||
}
|
||||
$default = $roles['ROLE_USER'];
|
||||
unset($roles['ROLE_USER']);
|
||||
$roles['ROLE_USER'] = $default;
|
||||
|
||||
$event = new PermissionsEvent();
|
||||
foreach ($permissionSorted as $title => $permissions) {
|
||||
@@ -160,20 +145,23 @@ final class PermissionController extends AbstractController
|
||||
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$page = new PageSetup('profile.roles');
|
||||
$page->setHelp('permissions.html');
|
||||
$page->setActionName('user_permissions');
|
||||
|
||||
return $this->render('permission/permissions.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'token' => $csrfTokenManager->refreshToken(self::TOKEN_NAME)->getValue(),
|
||||
'roles' => array_values($roles),
|
||||
'sorted' => $event->getPermissions(),
|
||||
'manager' => $this->manager,
|
||||
'system_roles' => $this->roleService->getSystemRoles(),
|
||||
'always_apply_superadmin' => RolePermissionManager::SUPER_ADMIN_PERMISSIONS,
|
||||
'system_roles' => $roleService->getSystemRoles(),
|
||||
'always_apply_superadmin' => array_keys(RolePermissionManager::SUPER_ADMIN_PERMISSIONS),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/create", name="admin_user_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
#[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
public function createRole(Request $request): Response
|
||||
{
|
||||
$role = new Role();
|
||||
@@ -196,16 +184,18 @@ final class PermissionController extends AbstractController
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
|
||||
$page = new PageSetup('profile.roles');
|
||||
$page->setHelp('permissions.html');
|
||||
|
||||
return $this->render('permission/edit_role.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'form' => $form->createView(),
|
||||
'role' => $role,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/{id}/delete/{csrfToken}", name="admin_user_role_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
#[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
@@ -234,10 +224,8 @@ final class PermissionController extends AbstractController
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/{id}/{name}/{value}/{csrfToken}", name="admin_user_permission_save", methods={"POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
#[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
|
||||
#[Security("is_granted('role_permissions')")]
|
||||
public function savePermission(Role $role, string $name, bool $value, string $csrfToken, RolePermissionRepository $rolePermissionRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
@@ -248,8 +236,8 @@ final class PermissionController extends AbstractController
|
||||
throw $this->createNotFoundException('Unknown permission: ' . $name);
|
||||
}
|
||||
|
||||
if (false === $value && $role->getName() === User::ROLE_SUPER_ADMIN && \in_array($name, RolePermissionManager::SUPER_ADMIN_PERMISSIONS)) {
|
||||
throw $this->createAccessDeniedException(sprintf('Permission "%s" cannot be deactivated for role "%s"', $name, $role->getName()));
|
||||
if (false === $value && $role->getName() === User::ROLE_SUPER_ADMIN && \array_key_exists($name, RolePermissionManager::SUPER_ADMIN_PERMISSIONS)) {
|
||||
throw new BadRequestHttpException(sprintf('Permission "%s" cannot be deactivated for role "%s"', $name, $role->getName()));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -259,7 +247,7 @@ final class PermissionController extends AbstractController
|
||||
$permission->setRole($role);
|
||||
$permission->setPermission($name);
|
||||
}
|
||||
$permission->setAllowed((bool) $value);
|
||||
$permission->setAllowed($value);
|
||||
|
||||
$rolePermissionRepository->saveRolePermission($permission);
|
||||
|
||||
|
||||
@@ -10,42 +10,57 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Plugin\PluginManager;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/plugins")
|
||||
* @Security("is_granted('plugins')")
|
||||
*/
|
||||
class PluginController extends AbstractController
|
||||
#[Route(path: '/admin/plugins')]
|
||||
#[Security("is_granted('plugins')")]
|
||||
final class PluginController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var PluginManager
|
||||
*/
|
||||
protected $plugins;
|
||||
|
||||
/**
|
||||
* @param PluginManager $manager
|
||||
*/
|
||||
public function __construct(PluginManager $manager)
|
||||
#[Route(path: '/', name: 'plugins', methods: ['GET'])]
|
||||
public function indexAction(PluginManager $manager, HttpClientInterface $client, CacheInterface $cache): Response
|
||||
{
|
||||
$this->plugins = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="plugins", methods={"GET"})
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$plugins = $this->plugins->getPlugins();
|
||||
foreach ($this->plugins->getPlugins() as $plugin) {
|
||||
$this->plugins->loadMetadata($plugin);
|
||||
$installed = [];
|
||||
$plugins = $manager->getPlugins();
|
||||
foreach ($plugins as $plugin) {
|
||||
$manager->loadMetadata($plugin);
|
||||
$installed[] = $plugin->getId();
|
||||
}
|
||||
|
||||
$page = new PageSetup('menu.plugin');
|
||||
$page->setHelp('plugins.html');
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'plugins' => $plugins,
|
||||
'installed' => $installed,
|
||||
'extensions' => $this->getPluginInformation($client, $cache)
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPluginInformation(HttpClientInterface $client, CacheInterface $cache): array
|
||||
{
|
||||
return $cache->get('kimai.marketplace_extensions', function (ItemInterface $item) use ($client) {
|
||||
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
|
||||
if ($json === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,26 @@ namespace App\Controller;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\Model\TotpActivation;
|
||||
use App\Form\UserApiTokenType;
|
||||
use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Form\UserTwoFactorType;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use App\User\UserService;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -33,28 +41,23 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* User profile controller
|
||||
*
|
||||
* @Route(path="/profile")
|
||||
* @Security("is_granted('view_own_profile') or is_granted('view_other_profile')")
|
||||
*/
|
||||
#[Route(path: '/profile')]
|
||||
#[Security("(is_granted('view_own_profile') or is_granted('view_other_profile'))")]
|
||||
final class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", name="my_profile", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', name: 'my_profile', methods: ['GET'])]
|
||||
public function profileAction(): Response
|
||||
{
|
||||
return $this->redirectToRoute('user_profile', ['username' => $this->getUser()->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile', ['username' => $this->getUser()->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}", name="user_profile", methods={"GET"})
|
||||
* @Security("is_granted('view', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}', name: 'user_profile', methods: ['GET'])]
|
||||
#[Security("is_granted('view', profile)")]
|
||||
public function indexAction(User $profile, TimesheetRepository $repository, TimesheetStatisticService $statisticService): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$userStats = $repository->getUserStatistics($profile, false);
|
||||
$userStats = $repository->getUserStatistics($profile);
|
||||
$firstEntry = $statisticService->findFirstRecordDate($profile);
|
||||
|
||||
$begin = $firstEntry ?? $dateFactory->getStartOfMonth();
|
||||
@@ -75,10 +78,8 @@ final class ProfileController extends AbstractController
|
||||
return $this->render('user/stats.html.twig', $viewVars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/edit", name="user_profile_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}/edit', name: 'user_profile_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', profile)")]
|
||||
public function editAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createEditForm($profile);
|
||||
@@ -89,20 +90,18 @@ final class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => 'settings',
|
||||
'tab' => 'edit',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/password", name="user_profile_password", methods={"GET", "POST"})
|
||||
* @Security("is_granted('password', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}/password', name: 'user_profile_password', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('password', profile)")]
|
||||
public function passwordAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createPasswordForm($profile);
|
||||
@@ -113,7 +112,7 @@ final class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/form.html.twig', [
|
||||
@@ -123,10 +122,8 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/api-token", name="user_profile_api_token", methods={"GET", "POST"})
|
||||
* @Security("is_granted('api-token', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}/api-token', name: 'user_profile_api_token', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('api-token', profile)")]
|
||||
public function apiTokenAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createApiTokenForm($profile);
|
||||
@@ -137,7 +134,7 @@ final class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/api-token.html.twig', [
|
||||
@@ -147,10 +144,8 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/roles", name="user_profile_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('roles', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}/roles', name: 'user_profile_roles', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('roles', profile)")]
|
||||
public function rolesAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$isSuperAdmin = $profile->isSuperAdmin();
|
||||
@@ -169,7 +164,7 @@ final class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/form.html.twig', [
|
||||
@@ -179,10 +174,8 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||
* @Security("is_granted('teams', profile)")
|
||||
*/
|
||||
#[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('teams', profile)")]
|
||||
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$originalMembers = new ArrayCollection();
|
||||
@@ -206,7 +199,7 @@ final class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/form.html.twig', [
|
||||
@@ -216,62 +209,32 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||
* @Security("is_granted('preferences', profile)")
|
||||
*/
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher): Response
|
||||
#[Route(path: '/{username}/prefs', name: 'user_profile_preferences', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('preferences', profile)")]
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
$original[$preference->getName()] = $preference;
|
||||
}
|
||||
|
||||
$form = $this->createPreferencesForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$preferences = $profile->getPreferences();
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$userRepository->saveUser($profile);
|
||||
|
||||
// do not allow to add unknown preferences
|
||||
foreach ($preferences as $preference) {
|
||||
if (!isset($original[$preference->getName()])) {
|
||||
$preferences->removeElement($preference);
|
||||
}
|
||||
}
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// but allow to delete already saved settings
|
||||
foreach ($original as $name => $preference) {
|
||||
if (false === $profile->getPreferences()->contains($preference)) {
|
||||
$entityManager->remove($preference);
|
||||
}
|
||||
}
|
||||
|
||||
$profile->setPreferences($preferences);
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale, false);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUsername()
|
||||
]);
|
||||
} else {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Validation failed']);
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale, false);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUserIdentifier()
|
||||
]);
|
||||
}
|
||||
|
||||
// prepare ordered preferences
|
||||
@@ -304,7 +267,7 @@ final class ProfileController extends AbstractController
|
||||
UserPreferencesForm::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_preferences', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_preferences', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
@@ -316,7 +279,7 @@ final class ProfileController extends AbstractController
|
||||
UserEditType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
|
||||
'include_preferences' => false,
|
||||
@@ -330,7 +293,7 @@ final class ProfileController extends AbstractController
|
||||
UserRolesType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_roles', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_roles', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]
|
||||
);
|
||||
@@ -342,7 +305,7 @@ final class ProfileController extends AbstractController
|
||||
UserTeamsType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_teams', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_teams', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]
|
||||
);
|
||||
@@ -354,7 +317,7 @@ final class ProfileController extends AbstractController
|
||||
UserPasswordType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
@@ -366,9 +329,98 @@ final class ProfileController extends AbstractController
|
||||
UserApiTokenType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_api_token', ['username' => $user->getUsername()]),
|
||||
'action' => $this->generateUrl('user_profile_api_token', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/2fa', name: 'user_profile_2fa', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")]
|
||||
public function twoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if (!$profile->hasTotpSecret()) {
|
||||
$profile->setTotpSecret($totpAuthenticator->generateSecret());
|
||||
$userService->updateUser($profile);
|
||||
}
|
||||
|
||||
$data = new TotpActivation($profile);
|
||||
|
||||
$form = $this->createForm(UserTwoFactorType::class, $data, [
|
||||
'action' => $this->generateUrl('user_profile_2fa', ['username' => $profile->getUserIdentifier()]),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$profile->enableTotpAuthentication();
|
||||
$userService->updateUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_2fa', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/2fa.html.twig', [
|
||||
'tab' => '2fa',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
'deactivate' => $this->getTwoFactorDeactivationForm($profile)->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getTwoFactorDeactivationForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createFormBuilder(
|
||||
[],
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_2fa_deactivate', ['username' => $user->getUserIdentifier()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
)->getForm();
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/2fa_deactivate', name: 'user_profile_2fa_deactivate', methods: ['POST'])]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")]
|
||||
public function deactivateTwoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if ($profile->hasTotpSecret()) {
|
||||
$form = $this->getTwoFactorDeactivationForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$profile->disableTotpAuthentication();
|
||||
$userService->updateUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_2fa', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])]
|
||||
#[Security("is_granted('2fa', profile)")]
|
||||
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
if (!$profile->hasTotpSecret()) {
|
||||
throw $this->createNotFoundException('User has no TOTP secret.');
|
||||
}
|
||||
|
||||
$qrCodeContent = $totpAuthenticator->getQRContent($profile);
|
||||
|
||||
$result = Builder::create()
|
||||
->writer(new PngWriter())
|
||||
->writerOptions([])
|
||||
->data($qrCodeContent)
|
||||
->encoding(new Encoding('UTF-8'))
|
||||
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
|
||||
->size(200)
|
||||
->margin(0)
|
||||
->roundBlockSizeMode(new RoundBlockSizeModeMargin())
|
||||
->build();
|
||||
|
||||
return new Response($result->getString(), 200, ['Content-Type' => 'image/png']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectDetailControllerEvent;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
@@ -39,52 +38,30 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage projects.
|
||||
*
|
||||
* @Route(path="/admin/project")
|
||||
* @Security("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')")
|
||||
*/
|
||||
#[Route(path: '/admin/project')]
|
||||
#[Security("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')")]
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var ProjectService
|
||||
*/
|
||||
private $projectService;
|
||||
|
||||
public function __construct(ProjectRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher, ProjectService $projectService)
|
||||
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->projectService = $projectService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
@@ -97,12 +74,47 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
$metaColumns = $this->findMetaColumns($query);
|
||||
|
||||
$table = new DataTable('project_admin', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_project_paginated');
|
||||
$table->setReloadEvents('kimai.projectUpdate kimai.projectDelete kimai.projectTeamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('customer', ['class' => 'd-none']);
|
||||
$table->addColumn('comment', ['class' => 'd-none', 'title' => 'description']);
|
||||
$table->addColumn('orderNumber', ['class' => 'd-none']);
|
||||
$table->addColumn('orderDate', ['class' => 'd-none']);
|
||||
$table->addColumn('project_start', ['class' => 'd-none']);
|
||||
$table->addColumn('project_end', ['class' => 'd-none']);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_money', 'project')) {
|
||||
$table->addColumn('budget', ['class' => 'd-none text-end w-min', 'title' => 'budget']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget_time', 'project')) {
|
||||
$table->addColumn('timeBudget', ['class' => 'd-none text-end w-min', 'title' => 'timeBudget']);
|
||||
}
|
||||
|
||||
$table->addColumn('billable', ['class' => 'd-none text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('team', ['class' => 'text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('visible', ['class' => 'd-none text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setDataTable($table);
|
||||
$page->setActionName('projects');
|
||||
|
||||
return $this->render('project/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'metaColumns' => $this->findMetaColumns($query),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'metaColumns' => $metaColumns,
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
}
|
||||
@@ -111,7 +123,7 @@ final class ProjectController extends AbstractController
|
||||
* @param ProjectQuery $query
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
protected function findMetaColumns(ProjectQuery $query): array
|
||||
private function findMetaColumns(ProjectQuery $query): array
|
||||
{
|
||||
$event = new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::PROJECT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -119,10 +131,8 @@ final class ProjectController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_project_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('permissions', project)")]
|
||||
public function teamPermissions(Project $project, Request $request)
|
||||
{
|
||||
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||
@@ -148,17 +158,27 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('project/permissions.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{customer}", name="admin_project_create_with_customer", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_project')")
|
||||
*/
|
||||
public function createAction(Request $request, ?Customer $customer = null)
|
||||
#[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_project')")]
|
||||
public function createWithCustomerAction(Request $request, Customer $customer)
|
||||
{
|
||||
return $this->createProject($request, $customer);
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_project')")]
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
return $this->createProject($request, null);
|
||||
}
|
||||
|
||||
private function createProject(Request $request, ?Customer $customer = null)
|
||||
{
|
||||
$project = $this->projectService->createNewProject($customer);
|
||||
|
||||
@@ -170,22 +190,21 @@ final class ProjectController extends AbstractController
|
||||
$this->projectService->saveNewProject($project, new Context($this->getUser()));
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
return $this->redirectToRouteAfterCreate('project_details', ['id' => $project->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_delete/{token}", name="project_comment_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")]
|
||||
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$projectId = $comment->getProject()->getId();
|
||||
@@ -207,14 +226,12 @@ final class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_details', ['id' => $projectId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_add", name="project_comment_add", methods={"POST"})
|
||||
* @Security("is_granted('comments_create', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])]
|
||||
#[Security("is_granted('comments', project)")]
|
||||
public function addCommentAction(Project $project, Request $request)
|
||||
{
|
||||
$comment = new ProjectComment();
|
||||
$form = $this->getCommentForm($project, $comment);
|
||||
$comment = new ProjectComment($project);
|
||||
$form = $this->getCommentForm($comment);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -229,10 +246,8 @@ final class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_pin/{token}", name="project_comment_pin", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")
|
||||
*/
|
||||
#[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])]
|
||||
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")]
|
||||
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$projectId = $comment->getProject()->getId();
|
||||
@@ -255,21 +270,18 @@ final class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_details', ['id' => $projectId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/create_team", name="project_team_create", methods={"GET"})
|
||||
* @Security("is_granted('create_team') and is_granted('edit', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
|
||||
#[Security("is_granted('create_team') and is_granted('permissions', project)")]
|
||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository)
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
|
||||
$this->flashError('action.update.error', 'Team already existing');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
$defaultTeam = new Team();
|
||||
$defaultTeam->setName($project->getName());
|
||||
$defaultTeam = new Team($project->getName());
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addProject($project);
|
||||
|
||||
@@ -282,10 +294,8 @@ final class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/activities/{page}", defaults={"page": 1}, name="project_activities", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', project)")]
|
||||
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
@@ -298,7 +308,6 @@ final class ProjectController extends AbstractController
|
||||
$query->addOrderGroup('visible', ActivityQuery::ORDER_DESC);
|
||||
$query->addOrderGroup('name', ActivityQuery::ORDER_ASC);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $activityRepository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('project/embed_activities.html.twig', [
|
||||
@@ -309,11 +318,9 @@ final class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="project_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService)
|
||||
#[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view', project)")]
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -340,10 +347,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
if ($this->isGranted('comments', $project)) {
|
||||
$comments = $this->repository->getComments($project);
|
||||
}
|
||||
|
||||
if ($this->isGranted('comments_create', $project)) {
|
||||
$commentForm = $this->getCommentForm($project, new ProjectComment())->createView();
|
||||
$commentForm = $this->getCommentForm(new ProjectComment($project))->createView();
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
||||
@@ -355,7 +359,13 @@ final class ProjectController extends AbstractController
|
||||
$this->dispatcher->dispatch($event);
|
||||
$boxes = $event->getController();
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setActionName('project');
|
||||
$page->setActionView('project_details');
|
||||
$page->setActionPayload(['project' => $project, 'token' => $csrfTokenManager->getToken('project.duplicate')]);
|
||||
|
||||
return $this->render('project/details.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'project' => $project,
|
||||
'comments' => $comments,
|
||||
'commentForm' => $commentForm,
|
||||
@@ -369,17 +379,27 @@ final class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/rate", name="admin_project_rate_add", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository)
|
||||
#[Route(path: '/{id}/rate/{rate}', name: 'admin_project_rate_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function editRateAction(Project $project, ProjectRate $rate, Request $request, ProjectRateRepository $repository): Response
|
||||
{
|
||||
return $this->rateFormAction($project, $rate, $request, $repository, $this->generateUrl('admin_project_rate_edit', ['id' => $project->getId(), 'rate' => $rate->getId()]));
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/rate', name: 'admin_project_rate_add', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository): Response
|
||||
{
|
||||
$rate = new ProjectRate();
|
||||
$rate->setProject($project);
|
||||
|
||||
return $this->rateFormAction($project, $rate, $request, $repository, $this->generateUrl('admin_project_rate_add', ['id' => $project->getId()]));
|
||||
}
|
||||
|
||||
private function rateFormAction(Project $project, ProjectRate $rate, Request $request, ProjectRateRepository $repository, string $formUrl): Response
|
||||
{
|
||||
$form = $this->createForm(ProjectRateForm::class, $rate, [
|
||||
'action' => $this->generateUrl('admin_project_rate_add', ['id' => $project->getId()]),
|
||||
'action' => $formUrl,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
@@ -397,15 +417,14 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('project/rates.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($project);
|
||||
@@ -423,15 +442,14 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/duplicate/{token}", name="admin_project_duplicate", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', project)")]
|
||||
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
|
||||
@@ -449,10 +467,8 @@ final class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*/
|
||||
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', project)")]
|
||||
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService)
|
||||
{
|
||||
$stats = $statisticService->getProjectStatistics($project);
|
||||
@@ -488,15 +504,14 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('project/delete.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'project' => $project,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="project_export", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
@@ -522,25 +537,23 @@ final class ProjectController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(ProjectQuery $query): FormInterface
|
||||
private function getToolbarForm(ProjectQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ProjectToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(ProjectToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_project', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
private function getCommentForm(Project $project, ProjectComment $comment): FormInterface
|
||||
private function getCommentForm(ProjectComment $comment): FormInterface
|
||||
{
|
||||
if (null === $comment->getId()) {
|
||||
$comment->setProject($project);
|
||||
$comment->setCreatedBy($this->getUser());
|
||||
}
|
||||
|
||||
return $this->createForm(ProjectCommentForm::class, $comment, [
|
||||
'action' => $this->generateUrl('project_comment_add', ['id' => $project->getId()]),
|
||||
'action' => $this->generateUrl('project_comment_add', ['id' => $comment->getProject()->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
@@ -565,7 +578,14 @@ final class ProjectController extends AbstractController
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'include_budget' => $this->isGranted('budget', $project),
|
||||
'include_time' => $this->isGranted('time', $project),
|
||||
'time_increment' => 15,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPageSetup(): PageSetup
|
||||
{
|
||||
$page = new PageSetup('projects');
|
||||
$page->setHelp('project.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,32 +16,23 @@ use App\Model\QuickEntryWeek;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to enter times in weekly form.
|
||||
*
|
||||
* @Route(path="/quick_entry")
|
||||
* @Security("is_granted('quick-entry')")
|
||||
*/
|
||||
class QuickEntryController extends AbstractController
|
||||
#[Route(path: '/quick_entry')]
|
||||
#[Security("is_granted('quick-entry')")]
|
||||
final class QuickEntryController extends AbstractController
|
||||
{
|
||||
private $configuration;
|
||||
private $timesheetService;
|
||||
private $repository;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration, TimesheetService $timesheetService, TimesheetRepository $repository)
|
||||
public function __construct(private SystemConfiguration $configuration, private TimesheetService $timesheetService, private TimesheetRepository $repository)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{begin}", name="quick_entry", methods={"GET", "POST"})
|
||||
*/
|
||||
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
|
||||
public function quickEntry(Request $request, ?string $begin = null)
|
||||
{
|
||||
$factory = $this->getDateTimeFactory();
|
||||
@@ -67,7 +58,7 @@ class QuickEntryController extends AbstractController
|
||||
$query->setBegin($startWeek);
|
||||
$query->setEnd($endWeek);
|
||||
$query->setName('quickEntryForm');
|
||||
$query->setUser($this->getUser());
|
||||
$query->setUser($user);
|
||||
|
||||
$result = $this->repository->getTimesheetResult($query);
|
||||
|
||||
@@ -104,7 +95,7 @@ class QuickEntryController extends AbstractController
|
||||
$startFrom = clone $startWeek;
|
||||
$startFrom->modify(sprintf('-%s weeks', $takeOverWeeks));
|
||||
}
|
||||
$timesheets = $this->repository->getRecentActivities($this->getUser(), $startFrom, $amount);
|
||||
$timesheets = $this->repository->getRecentActivities($user, $startFrom, $amount);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId();
|
||||
if (\array_key_exists($id, $rows)) {
|
||||
@@ -112,7 +103,7 @@ class QuickEntryController extends AbstractController
|
||||
}
|
||||
// there is an edge case possible with a project that starts and ends between the start and end date
|
||||
// user could still select it from the dropdown, but it is better to hide a row than displaying already ended projects
|
||||
if (!$timesheet->getProject()->isVisibleAtDate($startWeek) && !$timesheet->getProject()->isVisibleAtDate($endWeek)) {
|
||||
if ($timesheet->getProject() !== null && (!$timesheet->getProject()->isVisibleAtDate($startWeek) && !$timesheet->getProject()->isVisibleAtDate($endWeek))) {
|
||||
continue;
|
||||
}
|
||||
$rows[$id] = [
|
||||
@@ -225,12 +216,15 @@ class QuickEntryController extends AbstractController
|
||||
return $this->redirectToRoute('quick_entry', ['begin' => $begin->format('Y-m-d')]);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error');
|
||||
$this->logException($ex);
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('quick_entry.title');
|
||||
$page->setHelp('weekly-times.html');
|
||||
|
||||
return $this->render('quick-entry/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'days' => $week,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
|
||||
@@ -21,15 +21,8 @@ use DateTime;
|
||||
|
||||
abstract class AbstractUserReportController extends AbstractController
|
||||
{
|
||||
protected $statisticService;
|
||||
private $projectRepository;
|
||||
private $activityRepository;
|
||||
|
||||
public function __construct(TimesheetStatisticService $statisticService, ProjectRepository $projectRepository, ActivityRepository $activityRepository)
|
||||
public function __construct(protected TimesheetStatisticService $statisticService, private ProjectRepository $projectRepository, private ActivityRepository $activityRepository)
|
||||
{
|
||||
$this->statisticService = $statisticService;
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->activityRepository = $activityRepository;
|
||||
}
|
||||
|
||||
protected function canSelectUser(): bool
|
||||
|
||||
@@ -14,38 +14,32 @@ use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Reporting\CustomerMonthlyProjects\CustomerMonthlyProjects;
|
||||
use App\Reporting\CustomerMonthlyProjects\CustomerMonthlyProjectsForm;
|
||||
use App\Reporting\CustomerMonthlyProjects\CustomerMonthlyProjectsRepository;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/customer/monthly_projects")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_reporting') and is_granted('view_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/reporting/customer/monthly_projects')]
|
||||
#[Security("is_granted('report:customer') and is_granted('report:other')")]
|
||||
final class CustomerMonthlyProjectsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/view", name="report_customer_monthly_projects", methods={"GET","POST"})
|
||||
*/
|
||||
public function report(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
#[Route(path: '/view', name: 'report_customer_monthly_projects', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, CustomerMonthlyProjectsRepository $repository, UserRepository $userRepository): Response
|
||||
{
|
||||
return $this->render(
|
||||
'reporting/customer/monthly_projects.html.twig',
|
||||
$this->getData($request, $statisticService, $userRepository)
|
||||
$this->getData($request, $repository, $userRepository)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="report_customer_monthly_projects_export", methods={"GET","POST"})
|
||||
*/
|
||||
public function export(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
#[Route(path: '/export', name: 'report_customer_monthly_projects_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, CustomerMonthlyProjectsRepository $repository, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
$data = $this->getData($request, $repository, $userRepository);
|
||||
|
||||
$content = $this->render('reporting/customer/monthly_projects_export.html.twig', $data)->getContent();
|
||||
|
||||
@@ -57,12 +51,13 @@ final class CustomerMonthlyProjectsController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): array
|
||||
private function getData(Request $request, CustomerMonthlyProjectsRepository $repository, UserRepository $userRepository): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory();
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setSystemAccount(false);
|
||||
$query->setCurrentUser($currentUser);
|
||||
$allUsers = $userRepository->getUsersForQuery($query);
|
||||
|
||||
@@ -94,7 +89,7 @@ final class CustomerMonthlyProjectsController extends AbstractController
|
||||
$next = clone $start;
|
||||
$next->modify('+1 month');
|
||||
|
||||
$stats = $statisticService->getGroupedByCustomerProjectActivityUser($start, $end, $allUsers);
|
||||
$stats = $repository->getGroupedByCustomerProjectActivityUser($start, $end, $allUsers, $values->getCustomer());
|
||||
|
||||
return [
|
||||
'dataType' => $values->getSumType(),
|
||||
|
||||
@@ -20,20 +20,18 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectDateRangeController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_daterange", name="report_project_daterange", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
|
||||
*/
|
||||
#[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectDaterangeQuery($dateFactory->getStartOfMonth(), $user);
|
||||
$form = $this->createForm(ProjectDateRangeForm::class, $query, [
|
||||
$form = $this->createFormForGetRequest(ProjectDateRangeForm::class, $query, [
|
||||
'timezone' => $user->getTimezone()
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$dateRange = new DateRange(true);
|
||||
$dateRange->setBegin($query->getMonth());
|
||||
@@ -52,6 +50,7 @@ final class ProjectDateRangeController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_daterange.html.twig', [
|
||||
'report_title' => 'report_project_daterange',
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'queryEnd' => $dateRange->getEnd(),
|
||||
|
||||
@@ -10,40 +10,50 @@
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Project;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsForm;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectDetailsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_details", name="report_project_details", methods={"GET"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('details', 'project')")
|
||||
*/
|
||||
#[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])]
|
||||
#[Security("is_granted('report:project') and is_granted('details', 'project')")]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectDetailsQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createForm(ProjectDetailsForm::class, $query);
|
||||
$form = $this->createFormForGetRequest(ProjectDetailsForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$projectView = null;
|
||||
$projectDetails = null;
|
||||
$project = $query->getProject();
|
||||
|
||||
if ($query->getProject() !== null && $this->isGranted('details', $query->getProject())) {
|
||||
$projectViews = $service->getProjectView($user, [$query->getProject()], $query->getToday());
|
||||
if ($project !== null && $this->isGranted('details', $project)) {
|
||||
$projectViews = $service->getProjectView($user, [$project], $query->getToday());
|
||||
$projectView = $projectViews[0];
|
||||
$projectDetails = $service->getProjectsDetails($query);
|
||||
}
|
||||
|
||||
$page = new PageSetup('projects');
|
||||
$page->setHelp('project.html');
|
||||
|
||||
if ($project !== null) {
|
||||
$page->setActionName('project');
|
||||
$page->setActionView('project_details_report');
|
||||
$page->setActionPayload(['project' => $project]);
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_details.html.twig', [
|
||||
'project' => $query->getProject(),
|
||||
'page_setup' => $page,
|
||||
'report_title' => 'report_project_details',
|
||||
'project' => $project,
|
||||
'project_view' => $projectView,
|
||||
'project_details' => $projectDetails,
|
||||
'form' => $form->createView(),
|
||||
|
||||
@@ -19,10 +19,8 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectInactiveController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_inactive", name="report_project_inactive", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
|
||||
*/
|
||||
#[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
@@ -30,7 +28,7 @@ final class ProjectInactiveController extends AbstractController
|
||||
$now = $dateFactory->createDateTime();
|
||||
|
||||
$query = new ProjectInactiveQuery($dateFactory->createDateTime('-1 year'), $user);
|
||||
$form = $this->createForm(ProjectInactiveForm::class, $query, [
|
||||
$form = $this->createFormForGetRequest(ProjectInactiveForm::class, $query, [
|
||||
'timezone' => $user->getTimezone()
|
||||
]);
|
||||
$form->submit($request->query->all(), false);
|
||||
@@ -47,10 +45,10 @@ final class ProjectInactiveController extends AbstractController
|
||||
$byCustomer[$customer->getId()]['projects'][] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_view.html.twig', [
|
||||
return $this->render('reporting/project_inactive.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'title' => 'report_inactive_project',
|
||||
'report_title' => 'report_inactive_project',
|
||||
'tableName' => 'inactive_project_reporting',
|
||||
'now' => $now,
|
||||
'skipColumns' => ['today', 'week', 'month', 'projectStart', 'projectEnd', 'comment'],
|
||||
|
||||
@@ -19,17 +19,15 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectViewController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
|
||||
*/
|
||||
#[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectViewQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createForm(ProjectViewForm::class, $query);
|
||||
$form = $this->createFormForGetRequest(ProjectViewForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$projects = $service->findProjectsForView($query);
|
||||
@@ -47,7 +45,7 @@ final class ProjectViewController extends AbstractController
|
||||
return $this->render('reporting/project_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'title' => 'report_project_view',
|
||||
'report_title' => 'report_project_view',
|
||||
'tableName' => 'project_view_reporting',
|
||||
'now' => $dateFactory->createDateTime(),
|
||||
]);
|
||||
|
||||
@@ -13,8 +13,8 @@ use App\Controller\AbstractController;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\MonthlyUserList;
|
||||
use App\Reporting\MonthlyUserListForm;
|
||||
use App\Reporting\MonthlyUserList\MonthlyUserList;
|
||||
use App\Reporting\MonthlyUserList\MonthlyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
@@ -24,15 +24,11 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/users")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_reporting') and is_granted('view_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
final class ReportUsersMonthController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/month", name="report_monthly_users", methods={"GET","POST"})
|
||||
*/
|
||||
#[Route(path: '/month', name: 'report_monthly_users', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
return $this->render(
|
||||
@@ -41,9 +37,7 @@ final class ReportUsersMonthController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/month_export", name="report_monthly_users_export", methods={"GET","POST"})
|
||||
*/
|
||||
#[Route(path: '/month_export', name: 'report_monthly_users_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
@@ -66,7 +60,7 @@ final class ReportUsersMonthController extends AbstractController
|
||||
$values = new MonthlyUserList();
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
|
||||
$form = $this->createForm(MonthlyUserListForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(MonthlyUserListForm::class, $values, [
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
]);
|
||||
@@ -74,6 +68,7 @@ final class ReportUsersMonthController extends AbstractController
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setSystemAccount(false);
|
||||
$query->setCurrentUser($currentUser);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
|
||||
@@ -13,8 +13,8 @@ use App\Controller\AbstractController;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\WeeklyUserList;
|
||||
use App\Reporting\WeeklyUserListForm;
|
||||
use App\Reporting\WeeklyUserList\WeeklyUserList;
|
||||
use App\Reporting\WeeklyUserList\WeeklyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
@@ -24,15 +24,11 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/users")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_reporting') and is_granted('view_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
final class ReportUsersWeekController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/week", name="report_weekly_users", methods={"GET","POST"})
|
||||
*/
|
||||
#[Route(path: '/week', name: 'report_weekly_users', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
return $this->render(
|
||||
@@ -41,9 +37,7 @@ final class ReportUsersWeekController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/week_export", name="report_weekly_users_export", methods={"GET","POST"})
|
||||
*/
|
||||
#[Route(path: '/week_export', name: 'report_weekly_users_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
@@ -66,7 +60,7 @@ final class ReportUsersWeekController extends AbstractController
|
||||
$values = new WeeklyUserList();
|
||||
$values->setDate($dateTimeFactory->getStartOfWeek());
|
||||
|
||||
$form = $this->createForm(WeeklyUserListForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(WeeklyUserListForm::class, $values, [
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
]);
|
||||
@@ -74,6 +68,7 @@ final class ReportUsersWeekController extends AbstractController
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setSystemAccount(false);
|
||||
$query->setCurrentUser($currentUser);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
|
||||
@@ -14,8 +14,8 @@ use App\Controller\AbstractController;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Reporting\YearlyUserList;
|
||||
use App\Reporting\YearlyUserListForm;
|
||||
use App\Reporting\YearlyUserList\YearlyUserList;
|
||||
use App\Reporting\YearlyUserList\YearlyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
@@ -26,19 +26,16 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/users")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_reporting') and is_granted('view_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/reporting/users')]
|
||||
#[Security("is_granted('report:other')")]
|
||||
final class ReportUsersYearController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/year", name="report_yearly_users", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
return $this->render(
|
||||
@@ -48,12 +45,11 @@ final class ReportUsersYearController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/year_export", name="report_yearly_users_export", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
|
||||
@@ -82,7 +78,7 @@ final class ReportUsersYearController extends AbstractController
|
||||
$values = new YearlyUserList();
|
||||
$values->setDate(clone $defaultDate);
|
||||
|
||||
$form = $this->createForm(YearlyUserListForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(YearlyUserListForm::class, $values, [
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
]);
|
||||
@@ -90,6 +86,7 @@ final class ReportUsersYearController extends AbstractController
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setSystemAccount(false);
|
||||
$query->setCurrentUser($currentUser);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
@@ -126,7 +123,7 @@ final class ReportUsersYearController extends AbstractController
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $values,
|
||||
'subReportDate' => $values->getDate(),
|
||||
'period_attribute' => 'months',
|
||||
'dataType' => $values->getSumType(),
|
||||
'report_title' => 'report_yearly_users',
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\MonthByUser;
|
||||
use App\Reporting\MonthByUserForm;
|
||||
use App\Reporting\MonthByUser\MonthByUser;
|
||||
use App\Reporting\MonthByUser\MonthByUserForm;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -20,19 +19,16 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/user")
|
||||
* @Security("is_granted('view_reporting')")
|
||||
*/
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
final class UserMonthController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/month", name="report_user_month", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/month', name: 'report_user_month', methods: ['GET', 'POST'])]
|
||||
public function monthByUser(Request $request): Response
|
||||
{
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
@@ -48,7 +44,7 @@ final class UserMonthController extends AbstractUserReportController
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
|
||||
$form = $this->createForm(MonthByUserForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(MonthByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\WeekByUser;
|
||||
use App\Reporting\WeekByUserForm;
|
||||
use App\Reporting\WeekByUser\WeekByUser;
|
||||
use App\Reporting\WeekByUser\WeekByUserForm;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -20,19 +19,16 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/user")
|
||||
* @Security("is_granted('view_reporting')")
|
||||
*/
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
final class UserWeekController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/week", name="report_user_week", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/week', name: 'report_user_week', methods: ['GET', 'POST'])]
|
||||
public function weekByUser(Request $request): Response
|
||||
{
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
@@ -48,7 +44,7 @@ final class UserWeekController extends AbstractUserReportController
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfWeek());
|
||||
|
||||
$form = $this->createForm(WeekByUserForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(WeekByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
|
||||
@@ -13,8 +13,8 @@ use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Model\DateStatisticInterface;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Reporting\YearByUser;
|
||||
use App\Reporting\YearByUserForm;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Reporting\YearByUser\YearByUserForm;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -23,19 +23,16 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting/user")
|
||||
* @Security("is_granted('view_reporting')")
|
||||
*/
|
||||
#[Route(path: '/reporting/user')]
|
||||
#[Security("is_granted('report:user')")]
|
||||
final class UserYearController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/year", name="report_user_year", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_user_year', methods: ['GET', 'POST'])]
|
||||
public function yearByUser(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
|
||||
@@ -58,7 +55,7 @@ final class UserYearController extends AbstractUserReportController
|
||||
|
||||
$values->setDate(clone $defaultDate);
|
||||
|
||||
$form = $this->createForm(YearByUserForm::class, $values, [
|
||||
$form = $this->createFormForGetRequest(YearByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
|
||||
@@ -10,49 +10,27 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Reporting\ReportingService;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to render reports.
|
||||
*
|
||||
* @Route(path="/reporting")
|
||||
* @Security("is_granted('view_reporting')")
|
||||
*/
|
||||
#[Route(path: '/reporting')]
|
||||
#[Security("is_granted('view_reporting')")]
|
||||
final class ReportingController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", name="reporting", methods={"GET"})
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/', name: 'reporting', methods: ['GET'])]
|
||||
public function defaultReport(ReportingService $reportingService): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$route = null;
|
||||
$page = new PageSetup('menu.reporting');
|
||||
$page->setHelp('reporting.html');
|
||||
|
||||
$defaultReport = $user->getPreferenceValue('reporting.initial_view', ReportingService::DEFAULT_VIEW, false);
|
||||
$allReports = $reportingService->getAvailableReports($user);
|
||||
|
||||
foreach ($allReports as $report) {
|
||||
if ($report->getId() === $defaultReport) {
|
||||
$route = $report->getRoute();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback, if the configured report could not be found
|
||||
// e.g. when it was deleted or replaced by an enhanced version with a new id
|
||||
if ($route === null && \count($allReports) > 0) {
|
||||
$report = $allReports[array_keys($allReports)[0]];
|
||||
$route = $report->getRoute();
|
||||
}
|
||||
|
||||
if ($route === null) {
|
||||
throw $this->createNotFoundException('Unknown default report');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($route);
|
||||
return $this->render('reporting/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'reports' => $reportingService->getAvailableReports($this->getUser()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,28 +27,22 @@ use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/resetting")
|
||||
*/
|
||||
#[Route(path: '/resetting')]
|
||||
final class PasswordResetController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->configuration = $configuration;
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private UserService $userService,
|
||||
private SystemConfiguration $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request reset user password: show form.
|
||||
*
|
||||
* @Route(path="/request", name="fos_user_resetting_request", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/request', name: 'resetting_request', methods: ['GET'])]
|
||||
public function requestAction(): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
@@ -60,10 +54,9 @@ final class PasswordResetController extends AbstractController
|
||||
|
||||
/**
|
||||
* Request reset user password: submit form and send email.
|
||||
*
|
||||
* @Route(path="/send-email", name="fos_user_resetting_send_email", methods={"POST"})
|
||||
*/
|
||||
public function sendEmailAction(Request $request): Response
|
||||
#[Route(path: '/send-email', name: 'resetting_send_email', methods: ['POST'])]
|
||||
public function sendEmailAction(Request $request, TranslatorInterface $translator): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
@@ -75,7 +68,7 @@ final class PasswordResetController extends AbstractController
|
||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isInternalUser()) {
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUsername(), $user->getAuth())
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUserIdentifier(), $user->getAuth())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +76,7 @@ final class PasswordResetController extends AbstractController
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
}
|
||||
|
||||
$mail = $this->generateResettingEmailMessage($user);
|
||||
$mail = $this->generateResettingEmailMessage($user, $translator);
|
||||
$event = new EmailPasswordResetEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
@@ -94,14 +87,13 @@ final class PasswordResetController extends AbstractController
|
||||
$this->userService->updateUser($user);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('fos_user_resetting_check_email', ['username' => $username]);
|
||||
return $this->redirectToRoute('resetting_check_email', ['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user to check his email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_resetting_check_email", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/check-email', name: 'resetting_check_email', methods: ['GET'])]
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
@@ -112,7 +104,7 @@ final class PasswordResetController extends AbstractController
|
||||
|
||||
if (empty($username)) {
|
||||
// the user does not come from the sendEmail action
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
return $this->redirectToRoute('resetting_request');
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/check_email.html.twig', [
|
||||
@@ -122,9 +114,8 @@ final class PasswordResetController extends AbstractController
|
||||
|
||||
/**
|
||||
* Reset user password.
|
||||
*
|
||||
* @Route(path="/reset/{token}", name="fos_user_resetting_reset", methods={"GET", "POST"})
|
||||
*/
|
||||
#[Route(path: '/reset/{token}', name: 'resetting_reset', methods: ['GET', 'POST'])]
|
||||
public function resetAction(Request $request, LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
@@ -134,11 +125,11 @@ final class PasswordResetController extends AbstractController
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
return $this->redirectToRoute('login');
|
||||
}
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) {
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
return $this->redirectToRoute('resetting_request');
|
||||
}
|
||||
|
||||
$form = $this->createResetForm();
|
||||
@@ -169,20 +160,20 @@ final class PasswordResetController extends AbstractController
|
||||
{
|
||||
$options = ['validation_groups' => ['ResetPassword', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_resetting_form', PasswordResetForm::class, $options)->getForm();
|
||||
return $this->createFormBuilder()->create('resetting_form', PasswordResetForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function generateResettingEmailMessage(User $user): Email
|
||||
private function generateResettingEmailMessage(User $user, TranslatorInterface $translator): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$url = $this->generateUrl('resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('reset.subject', ['%username%' => $username], 'email', $language)
|
||||
$translator->trans('reset.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/password-reset.html.twig')
|
||||
->context([
|
||||
|
||||
@@ -10,62 +10,36 @@
|
||||
namespace App\Controller\Security;
|
||||
|
||||
use App\Configuration\SamlConfigurationInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
final class SecurityController extends AbstractController
|
||||
{
|
||||
private $tokenManager;
|
||||
private $samlConfiguration;
|
||||
|
||||
public function __construct(CsrfTokenManagerInterface $tokenManager, SamlConfigurationInterface $samlConfiguration)
|
||||
public function __construct(private CsrfTokenManagerInterface $tokenManager, private SamlConfigurationInterface $samlConfiguration)
|
||||
{
|
||||
$this->tokenManager = $tokenManager;
|
||||
$this->samlConfiguration = $samlConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login", name="fos_user_security_login", methods={"GET", "POST"})
|
||||
*/
|
||||
public function loginAction(Request $request): Response
|
||||
#[Route(path: '/login', name: 'login', methods: ['GET', 'POST'])]
|
||||
public function loginAction(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
|
||||
/** @var SessionInterface $session */
|
||||
$session = $request->getSession();
|
||||
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
$lastUsernameKey = Security::LAST_USERNAME;
|
||||
|
||||
// get the error if any (works with forward and redirect -- see below)
|
||||
if ($request->attributes->has($authErrorKey)) {
|
||||
$error = $request->attributes->get($authErrorKey);
|
||||
} elseif (null !== $session && $session->has($authErrorKey)) {
|
||||
$error = $session->get($authErrorKey);
|
||||
$session->remove($authErrorKey);
|
||||
} else {
|
||||
$error = null;
|
||||
}
|
||||
|
||||
if (!$error instanceof AuthenticationException) {
|
||||
$error = null; // The value does not come from the security component.
|
||||
}
|
||||
|
||||
$lastUsername = '';
|
||||
if ($request->hasSession()) {
|
||||
$lastUsername = $session->get($lastUsernameKey);
|
||||
}
|
||||
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
$csrfToken = $this->tokenManager->getToken('authenticate')->getValue();
|
||||
|
||||
if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED') && $this->getUser()->isInternalUser()) {
|
||||
return $this->render('security/unlock.html.twig', [
|
||||
'error' => $error,
|
||||
'csrf_token' => $csrfToken,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
@@ -74,17 +48,13 @@ final class SecurityController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login_check", name="fos_user_security_check", methods={"POST"})
|
||||
*/
|
||||
#[Route(path: '/login_check', name: 'security_check', methods: ['POST'])]
|
||||
public function checkAction()
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/logout", name="fos_user_security_logout", methods={"GET", "POST"})
|
||||
*/
|
||||
#[Route(path: '/logout', name: 'logout', methods: ['GET', 'POST'])]
|
||||
public function logoutAction()
|
||||
{
|
||||
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
|
||||
|
||||
@@ -28,29 +28,21 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/register")
|
||||
*/
|
||||
class SelfRegistrationController extends AbstractController
|
||||
#[Route(path: '/register')]
|
||||
final class SelfRegistrationController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $tokenStorage;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, TokenStorageInterface $tokenStorage, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->configuration = $configuration;
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private UserService $userService,
|
||||
private TokenStorageInterface $tokenStorage,
|
||||
private SystemConfiguration $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="fos_user_registration_register", methods={"GET", "POST"})
|
||||
*/
|
||||
public function registerAction(Request $request): Response
|
||||
#[Route(path: '/', name: 'registration_register', methods: ['GET', 'POST'])]
|
||||
public function registerAction(Request $request, TranslatorInterface $translator): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
@@ -68,18 +60,18 @@ class SelfRegistrationController extends AbstractController
|
||||
$user->setEnabled(false);
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
|
||||
$mail = $this->generateConfirmationEmail($user);
|
||||
$mail = $this->generateConfirmationEmail($user, $translator);
|
||||
$event = new EmailSelfRegistrationEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$request->getSession()->set('fos_user_send_confirmation_email/email', $user->getEmail());
|
||||
$request->getSession()->set('confirmation_email_address', $user->getEmail());
|
||||
|
||||
$this->userService->saveNewUser($user);
|
||||
|
||||
return $this->redirectToRoute('fos_user_registration_check_email');
|
||||
return $this->redirectToRoute('user_registration_check_email');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/register.html.twig', [
|
||||
@@ -89,26 +81,25 @@ class SelfRegistrationController extends AbstractController
|
||||
|
||||
/**
|
||||
* Tell the user to check their email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_registration_check_email", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/check-email', name: 'user_registration_check_email', methods: ['GET'])]
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$email = $request->getSession()->get('fos_user_send_confirmation_email/email');
|
||||
$email = $request->getSession()->get('confirmation_email_address');
|
||||
|
||||
if (empty($email)) {
|
||||
return $this->redirectToRoute('fos_user_registration_register');
|
||||
return $this->redirectToRoute('registration_register');
|
||||
}
|
||||
|
||||
$request->getSession()->remove('fos_user_send_confirmation_email/email');
|
||||
$request->getSession()->remove('confirmation_email_address');
|
||||
$user = $this->userService->findUserByEmail($email);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
return $this->redirectToRoute('login');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/check_email.html.twig', [
|
||||
@@ -118,9 +109,8 @@ class SelfRegistrationController extends AbstractController
|
||||
|
||||
/**
|
||||
* Receive the confirmation token from user email provider, login the user.
|
||||
*
|
||||
* @Route(path="/confirm/{token}", name="fos_user_registration_confirm", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/confirm/{token}', name: 'registration_confirm', methods: ['GET'])]
|
||||
public function confirmAction(LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
@@ -130,7 +120,7 @@ class SelfRegistrationController extends AbstractController
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
return $this->redirectToRoute('login');
|
||||
}
|
||||
|
||||
$user->setConfirmationToken(null);
|
||||
@@ -138,7 +128,7 @@ class SelfRegistrationController extends AbstractController
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
$response = $this->redirectToRoute('fos_user_registration_confirmed');
|
||||
$response = $this->redirectToRoute('registration_confirmed');
|
||||
$loginManager->logInUser($user, $response);
|
||||
|
||||
return $response;
|
||||
@@ -146,22 +136,16 @@ class SelfRegistrationController extends AbstractController
|
||||
|
||||
/**
|
||||
* Tell the user his account is now confirmed.
|
||||
*
|
||||
* @Route(path="/confirmed", name="fos_user_registration_confirmed", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/confirmed', name: 'registration_confirmed', methods: ['GET'])]
|
||||
public function confirmedAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
if ($user === null) {
|
||||
throw $this->createAccessDeniedException('This user does not have access to this section.');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/confirmed.html.twig', [
|
||||
'user' => $user,
|
||||
'user' => $this->getUser(),
|
||||
'targetUrl' => $this->getTargetUrlFromSession($request->getSession()),
|
||||
]);
|
||||
}
|
||||
@@ -170,13 +154,13 @@ class SelfRegistrationController extends AbstractController
|
||||
{
|
||||
$options = ['validation_groups' => ['Registration', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_registration_form', SelfRegistrationForm::class, $options)->getForm();
|
||||
return $this->createFormBuilder()->create('user_registration_form', SelfRegistrationForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function getTargetUrlFromSession(SessionInterface $session): ?string
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if (!method_exists($token, 'getProviderKey')) {
|
||||
if ($token === null || !method_exists($token, 'getProviderKey')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -189,17 +173,17 @@ class SelfRegistrationController extends AbstractController
|
||||
return null;
|
||||
}
|
||||
|
||||
private function generateConfirmationEmail(User $user): Email
|
||||
private function generateConfirmationEmail(User $user, TranslatorInterface $translator): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$url = $this->generateUrl('registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('registration.subject', ['%username%' => $username], 'email', $language)
|
||||
$translator->trans('registration.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/confirmation.html.twig')
|
||||
->context([
|
||||
|
||||
@@ -31,18 +31,19 @@ use App\Form\Type\TrackingModeType;
|
||||
use App\Form\Type\WeekDaysType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Validator\Constraints\AllowedHtmlTags;
|
||||
use App\Timesheet\LockdownService;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Validator\Constraints\ColorChoices;
|
||||
use App\Validator\Constraints\DateTimeFormat;
|
||||
use App\Validator\Constraints\TimeFormat;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -54,35 +55,16 @@ use Symfony\Component\Validator\Constraints\Regex;
|
||||
|
||||
/**
|
||||
* Controller used for executing system relevant tasks.
|
||||
*
|
||||
* @Route(path="/admin/system-config")
|
||||
* @Security("is_granted('system_configuration')")
|
||||
*/
|
||||
#[Route(path: '/admin/system-config')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('system_configuration')")]
|
||||
final class SystemConfigurationController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configurations;
|
||||
/**
|
||||
* @var ConfigurationRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, ConfigurationRepository $repository, SystemConfiguration $config)
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private ConfigurationRepository $repository, private SystemConfiguration $systemConfiguration, private LockdownService $lockdownService)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->repository = $repository;
|
||||
$this->configurations = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="system_configuration", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', name: 'system_configuration', methods: ['GET'])]
|
||||
public function indexAction(): Response
|
||||
{
|
||||
$configSettings = $this->getInitializedConfigurations();
|
||||
@@ -95,14 +77,16 @@ final class SystemConfigurationController extends AbstractController
|
||||
];
|
||||
}
|
||||
|
||||
$page = new PageSetup('menu.system_configuration');
|
||||
$page->setHelp('configurations.html');
|
||||
|
||||
return $this->render('system-configuration/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'sections' => $configurations,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/edit/{section}", name="system_configuration_section", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/edit/{section}', name: 'system_configuration_section', methods: ['GET'])]
|
||||
public function sectionAction(string $section): Response
|
||||
{
|
||||
$configSettings = $this->getInitializedConfigurations();
|
||||
@@ -119,27 +103,31 @@ final class SystemConfigurationController extends AbstractController
|
||||
];
|
||||
}
|
||||
|
||||
return $this->render('system-configuration/index.html.twig', [
|
||||
$page = new PageSetup('menu.system_configuration');
|
||||
$page->setHelp('configurations.html');
|
||||
|
||||
return $this->render('system-configuration/section.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'sections' => $configurations,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/update/{section}/{single}", defaults={"single": "0"}, name="system_configuration_update", methods={"POST"})
|
||||
*
|
||||
* @internal do not link directly to this route
|
||||
* @param Request $request
|
||||
* @param string $section
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function configUpdate(Request $request, string $section, string $single)
|
||||
#[Route(path: '/update/{section}/{single}', defaults: ['single' => 0], name: 'system_configuration_update', methods: ['POST'])]
|
||||
public function configUpdate(Request $request, string $section, string $single): Response
|
||||
{
|
||||
$single = (bool) $single;
|
||||
$configModel = null;
|
||||
$configSettings = $this->getInitializedConfigurations();
|
||||
|
||||
foreach ($configSettings as $configModel) {
|
||||
if ($configModel->getSection() === $section) {
|
||||
foreach ($configSettings as $model) {
|
||||
if ($model->getSection() === $section) {
|
||||
$configModel = $model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -151,40 +139,36 @@ final class SystemConfigurationController extends AbstractController
|
||||
$form = $this->createConfigurationsForm($configModel, $single);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveSystemConfiguration($form->getData());
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
if ($single) {
|
||||
return $this->redirectToRoute('system_configuration_section', ['section' => $section]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('system_configuration');
|
||||
} else {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Validation problem']);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveSystemConfiguration($form->getData());
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->handleFormUpdateException($ex, $form);
|
||||
}
|
||||
|
||||
if ($single) {
|
||||
return $this->redirectToRoute('system_configuration_section', ['section' => $section]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('system_configuration');
|
||||
}
|
||||
|
||||
$configSettings = $this->getInitializedConfigurations();
|
||||
|
||||
$configurations = [];
|
||||
foreach ($configSettings as $configModel) {
|
||||
if ($single && $section !== $configModel->getSection()) {
|
||||
foreach ($configSettings as $model) {
|
||||
if ($single && $section !== $model->getSection()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($section !== $configModel->getSection()) {
|
||||
$form2 = $this->createConfigurationsForm($configModel, $single);
|
||||
if ($section !== $model->getSection()) {
|
||||
$form2 = $this->createConfigurationsForm($model, $single);
|
||||
} else {
|
||||
$form2 = $form;
|
||||
}
|
||||
$configurations[] = [
|
||||
'model' => $configModel,
|
||||
'model' => $model,
|
||||
'form' => $form2->createView(),
|
||||
];
|
||||
}
|
||||
@@ -210,7 +194,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
/**
|
||||
* @return SystemConfigurationModel[]
|
||||
*/
|
||||
protected function getInitializedConfigurations()
|
||||
private function getInitializedConfigurations(): array
|
||||
{
|
||||
$types = $this->getConfigurationTypes();
|
||||
|
||||
@@ -219,11 +203,11 @@ final class SystemConfigurationController extends AbstractController
|
||||
|
||||
foreach ($event->getConfigurations() as $configs) {
|
||||
foreach ($configs->getConfiguration() as $config) {
|
||||
if (!$this->configurations->has($config->getName())) {
|
||||
if (!$this->systemConfiguration->has($config->getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$configValue = $this->configurations->find($config->getName());
|
||||
$configValue = $this->systemConfiguration->find($config->getName());
|
||||
if (null !== $configValue) {
|
||||
$config->setValue($configValue);
|
||||
}
|
||||
@@ -236,41 +220,29 @@ final class SystemConfigurationController extends AbstractController
|
||||
/**
|
||||
* @return SystemConfigurationModel[]
|
||||
*/
|
||||
protected function getConfigurationTypes()
|
||||
private function getConfigurationTypes(): array
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$lockdownStartHelp = null;
|
||||
$lockdownEndHelp = null;
|
||||
$lockdownGraceHelp = null;
|
||||
$dateFormat = 'D, d M Y H:i:s';
|
||||
|
||||
if ($this->configurations->isTimesheetLockdownActive()) {
|
||||
$userTimezone = $this->getDateTimeFactory()->getTimezone();
|
||||
$timezone = $this->configurations->getTimesheetLockdownTimeZone();
|
||||
|
||||
if ($timezone !== null) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
if ($timezone === null) {
|
||||
$timezone = $userTimezone;
|
||||
}
|
||||
|
||||
if ($this->lockdownService->isLockdownActive()) {
|
||||
try {
|
||||
if (!empty($this->configurations->getTimesheetLockdownPeriodStart())) {
|
||||
$lockdownStartHelp = new \DateTime($this->configurations->getTimesheetLockdownPeriodStart(), $timezone);
|
||||
$lockdownStartHelp->setTimezone($userTimezone);
|
||||
$lockdownStartHelp = $lockdownStartHelp->format($dateFormat);
|
||||
$start = $this->lockdownService->getLockdownStart($user);
|
||||
if ($start !== null) {
|
||||
$lockdownStartHelp = $start->format($dateFormat);
|
||||
}
|
||||
if (!empty($this->configurations->getTimesheetLockdownPeriodEnd())) {
|
||||
$lockdownEndHelp = new \DateTime($this->configurations->getTimesheetLockdownPeriodEnd(), $timezone);
|
||||
if (!empty($this->configurations->getTimesheetLockdownGracePeriod())) {
|
||||
$lockdownGraceHelp = clone $lockdownEndHelp;
|
||||
$lockdownGraceHelp->modify($this->configurations->getTimesheetLockdownGracePeriod());
|
||||
$lockdownGraceHelp->setTimezone($userTimezone);
|
||||
$lockdownGraceHelp = $lockdownGraceHelp->format($dateFormat);
|
||||
}
|
||||
$lockdownEndHelp->setTimezone($userTimezone);
|
||||
$lockdownEndHelp = $lockdownEndHelp->format($dateFormat);
|
||||
|
||||
$end = $this->lockdownService->getLockdownEnd($user);
|
||||
if ($end !== null) {
|
||||
$lockdownEndHelp = $end->format($dateFormat);
|
||||
}
|
||||
|
||||
$grace = $this->lockdownService->getLockdownGrace($user);
|
||||
if ($grace !== null) {
|
||||
$lockdownGraceHelp = $grace->format($dateFormat);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$lockdownStartHelp = 'invalid';
|
||||
@@ -279,52 +251,35 @@ final class SystemConfigurationController extends AbstractController
|
||||
|
||||
$authentication = (new SystemConfigurationModel('authentication'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('user.login')
|
||||
(new Configuration('user.login'))
|
||||
->setLabel('user_auth_login')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.registration')
|
||||
(new Configuration('user.registration'))
|
||||
->setLabel('user_auth_registration')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset')
|
||||
(new Configuration('user.password_reset'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset_retry_ttl')
|
||||
(new Configuration('user.password_reset_retry_ttl'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset_retry_ttl')
|
||||
->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])])
|
||||
->setType(IntegerType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset_token_ttl')
|
||||
(new Configuration('user.password_reset_token_ttl'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset_token_ttl')
|
||||
->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])])
|
||||
->setType(IntegerType::class),
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('ldap.activate')
|
||||
->setLabel('ldap_activate')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('saml.activate')
|
||||
->setLabel('saml_activate')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
*/
|
||||
]);
|
||||
|
||||
if (!$this->configurations->isSamlActive()) {
|
||||
if (!$this->systemConfiguration->isSamlActive()) {
|
||||
$authentication->getConfigurationByName('user.login')->setEnabled(false);
|
||||
}
|
||||
|
||||
if (!$this->configurations->isPasswordResetActive()) {
|
||||
if (!$this->systemConfiguration->isPasswordResetActive()) {
|
||||
$authentication->getConfigurationByName('user.password_reset_retry_ttl')->setEnabled(false);
|
||||
$authentication->getConfigurationByName('user.password_reset_token_ttl')->setEnabled(false);
|
||||
}
|
||||
@@ -332,64 +287,52 @@ final class SystemConfigurationController extends AbstractController
|
||||
return [
|
||||
(new SystemConfigurationModel('timesheet'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('timesheet.mode')
|
||||
(new Configuration('timesheet.mode'))
|
||||
->setType(TrackingModeType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.default_begin')
|
||||
(new Configuration('timesheet.default_begin'))
|
||||
->setType(DateTimeTextType::class)
|
||||
->setConstraints([new DateTimeFormat(), new NotNull()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.allow_future_times')
|
||||
->setType(CheckboxType::class)
|
||||
(new Configuration('timesheet.rules.allow_future_times'))
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.allow_zero_duration')
|
||||
->setType(CheckboxType::class)
|
||||
(new Configuration('timesheet.rules.allow_zero_duration'))
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.allow_overlapping_records')
|
||||
->setType(CheckboxType::class)
|
||||
(new Configuration('timesheet.rules.allow_overlapping_records'))
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.allow_overbooking_budget')
|
||||
->setType(CheckboxType::class)
|
||||
(new Configuration('timesheet.rules.allow_overbooking_budget'))
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.active_entries.hard_limit')
|
||||
(new Configuration('timesheet.active_entries.hard_limit'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 1])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.time_increment')
|
||||
(new Configuration('timesheet.time_increment'))
|
||||
->setType(MinuteIncrementType::class)
|
||||
->setOptions(['deactivate' => false, 'max_one_hour' => true])
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new Range(['min' => 1, 'max' => 60])
|
||||
new Range(['min' => 0, 'max' => 60])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.duration_increment')
|
||||
(new Configuration('timesheet.duration_increment'))
|
||||
->setType(MinuteIncrementType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.break_warning_duration')
|
||||
(new Configuration('timesheet.rules.break_warning_duration'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
*/
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.long_running_duration')
|
||||
(new Configuration('timesheet.rules.long_running_duration'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
@@ -400,24 +343,21 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setTranslation('quick_entry.title')
|
||||
->setTranslationDomain('messages')
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('quick_entry.recent_activities')
|
||||
(new Configuration('quick_entry.recent_activities'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setConstraints([
|
||||
new Range(['min' => 0, 'max' => 20]),
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('quick_entry.recent_activity_weeks')
|
||||
(new Configuration('quick_entry.recent_activity_weeks'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setConstraints([
|
||||
new Range(['min' => 0, 'max' => 20]),
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('quick_entry.minimum_rows')
|
||||
(new Configuration('quick_entry.minimum_rows'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
@@ -426,27 +366,23 @@ final class SystemConfigurationController extends AbstractController
|
||||
]),
|
||||
(new SystemConfigurationModel('lockdown_period'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.lockdown_period_start')
|
||||
(new Configuration('timesheet.rules.lockdown_period_start'))
|
||||
->setOptions(['help' => $lockdownStartHelp])
|
||||
->setType(TextType::class)
|
||||
->setRequired(false)
|
||||
->setConstraints([new DateTimeFormat()])
|
||||
->setConstraints([new DateTimeFormat(['separator' => ','])])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.lockdown_period_end')
|
||||
(new Configuration('timesheet.rules.lockdown_period_end'))
|
||||
->setOptions(['help' => $lockdownEndHelp])
|
||||
->setType(TextType::class)
|
||||
->setRequired(false)
|
||||
->setConstraints([new DateTimeFormat()])
|
||||
->setConstraints([new DateTimeFormat(['separator' => ','])])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.lockdown_period_timezone')
|
||||
(new Configuration('timesheet.rules.lockdown_period_timezone'))
|
||||
->setType(TimezoneType::class)
|
||||
->setRequired(false)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.lockdown_grace_period')
|
||||
(new Configuration('timesheet.rules.lockdown_grace_period'))
|
||||
->setOptions(['help' => $lockdownGraceHelp])
|
||||
->setType(TextType::class)
|
||||
->setRequired(false)
|
||||
@@ -455,33 +391,28 @@ final class SystemConfigurationController extends AbstractController
|
||||
]),
|
||||
(new SystemConfigurationModel('rounding'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('timesheet.rounding.default.mode')
|
||||
(new Configuration('timesheet.rounding.default.mode'))
|
||||
->setType(RoundingModeType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rounding.default.begin')
|
||||
(new Configuration('timesheet.rounding.default.begin'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rounding.default.end')
|
||||
(new Configuration('timesheet.rounding.default.end'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rounding.default.duration')
|
||||
(new Configuration('timesheet.rounding.default.duration'))
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.rounding.default.days')
|
||||
(new Configuration('timesheet.rounding.default.days'))
|
||||
->setType(WeekDaysType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
@@ -490,210 +421,160 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setTranslationDomain('messages')
|
||||
->setConfiguration([
|
||||
// TODO that should be a custom type with validation
|
||||
(new Configuration())
|
||||
->setName('invoice.number_format')
|
||||
(new Configuration('invoice.number_format'))
|
||||
->setLabel('invoice.number_format')
|
||||
->setOptions([
|
||||
'help' => 'allowed_replacer',
|
||||
'help_translation_parameters' => [
|
||||
'%replacer%' => '{Y}, {y}, {M}, {m}, {D}, {d}, {date}, {cc}, {ccy}, {ccm}, {ccd}, {cu}, {cuy}, {cum}, {cud}, {ustaff}, {uid}, {c}, {cy}, {cm}, {cd}, {cname}, {cnumber}'
|
||||
]
|
||||
])
|
||||
->setRequired(true)
|
||||
->setType(TextType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('invoice.simple_form')
|
||||
->setLabel('simple_form')
|
||||
->setRequired(false)
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
$authentication,
|
||||
(new SystemConfigurationModel('customer'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.timezone')
|
||||
(new Configuration('defaults.customer.timezone'))
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get())
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.country')
|
||||
(new Configuration('defaults.customer.country'))
|
||||
->setLabel('country')
|
||||
->setType(CountryType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.currency')
|
||||
(new Configuration('defaults.customer.currency'))
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('customer.choice_pattern')
|
||||
(new Configuration('customer.choice_pattern'))
|
||||
->setLabel('choice_pattern')
|
||||
->setType(CustomerTypePatternType::class),
|
||||
(new Configuration('customer.number_format'))
|
||||
->setLabel('customer.number_format')
|
||||
->setOptions(['help' => 'allowed_replacer', 'help_translation_parameters' => ['%replacer%' => '{cc}']])
|
||||
->setRequired(true)
|
||||
->setType(TextType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('project'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('project.choice_pattern')
|
||||
(new Configuration('project.choice_pattern'))
|
||||
->setLabel('choice_pattern')
|
||||
->setType(ProjectTypePatternType::class),
|
||||
(new Configuration())
|
||||
->setName('project.copy_teams_on_create')
|
||||
(new Configuration('project.copy_teams_on_create'))
|
||||
->setLabel('copy_teams_on_create')
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('activity'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('activity.choice_pattern')
|
||||
(new Configuration('activity.choice_pattern'))
|
||||
->setLabel('choice_pattern')
|
||||
->setType(ActivityTypePatternType::class),
|
||||
// TODO see DependencyInjection/Configuration::getActivityNode()
|
||||
/*
|
||||
(new Configuration('activity.allow_inline_create'))
|
||||
->setLabel('activity.allow_inline_create')
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'), */
|
||||
]),
|
||||
(new SystemConfigurationModel('user'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('defaults.user.timezone')
|
||||
(new Configuration('defaults.user.timezone'))
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get())
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.language')
|
||||
(new Configuration('defaults.user.language'))
|
||||
->setLabel('language')
|
||||
->setType(LanguageType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.theme')
|
||||
(new Configuration('defaults.user.theme'))
|
||||
->setLabel('skin')
|
||||
->setType(SkinType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.currency')
|
||||
(new Configuration('defaults.user.currency'))
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
(new Configuration())
|
||||
->setName('theme.avatar_url')
|
||||
(new Configuration('theme.avatar_url'))
|
||||
->setRequired(false)
|
||||
->setLabel('theme.avatar_url')
|
||||
->setType(CheckboxType::class)
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('theme'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('theme.autocomplete_chars')
|
||||
->setLabel('theme.autocomplete_chars')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.markdown_content')
|
||||
(new Configuration('timesheet.markdown_content'))
|
||||
->setLabel('theme.markdown_content')
|
||||
->setType(CheckboxType::class)
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('theme.tags_create')
|
||||
->setLabel('theme.tags_create')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('theme.colors_limited')
|
||||
->setLabel('theme.colors_limited')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('theme.color_choices')
|
||||
(new Configuration('theme.color_choices'))
|
||||
->setRequired(false)
|
||||
->setLabel('theme.color_choices')
|
||||
->setType(ArrayToCommaStringType::class)
|
||||
->setOptions(['help' => 'help.theme.color_choices'])
|
||||
->setConstraints([new ColorChoices()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// random colors as fallback
|
||||
(new Configuration())
|
||||
->setName('theme.random_colors')
|
||||
->setRequired(false)
|
||||
->setLabel('theme.random_colors')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel('calendar'))
|
||||
->setTranslation('calendar')
|
||||
->setTranslationDomain('messages')
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('calendar.week_numbers')
|
||||
(new Configuration('calendar.week_numbers'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(CheckboxType::class),
|
||||
(new Configuration())
|
||||
->setName('calendar.weekends')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration('calendar.weekends'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(CheckboxType::class),
|
||||
(new Configuration())
|
||||
->setName('calendar.businessHours.begin')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration('calendar.businessHours.begin'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(DayTimeType::class)
|
||||
->setConstraints([new NotBlank(), new TimeFormat()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.businessHours.end')
|
||||
(new Configuration('calendar.businessHours.end'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(DayTimeType::class)
|
||||
->setConstraints([new NotBlank(), new TimeFormat()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.visibleHours.begin')
|
||||
(new Configuration('calendar.visibleHours.begin'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(DayTimeType::class)
|
||||
->setConstraints([new NotBlank(), new TimeFormat()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.visibleHours.end')
|
||||
(new Configuration('calendar.visibleHours.end'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(DayTimeType::class)
|
||||
->setConstraints([new NotBlank(), new TimeFormat()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.slot_duration')
|
||||
(new Configuration('calendar.slot_duration'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new Regex(['pattern' => '/[0-2]{1}[0-9]{1}:[0-9]{2}:[0-9]{2}/']), new NotNull()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.dragdrop_amount')
|
||||
(new Configuration('calendar.dragdrop_amount'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(IntegerType::class)
|
||||
->setConstraints([new Range(['min' => 0, 'max' => 20]), new NotNull()]),
|
||||
(new Configuration())
|
||||
->setName('calendar.dragdrop_data')
|
||||
(new Configuration('calendar.dragdrop_data'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(CheckboxType::class),
|
||||
(new Configuration())
|
||||
->setName('calendar.title_pattern')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration('calendar.title_pattern'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(CalendarTitlePatternType::class),
|
||||
]),
|
||||
(new SystemConfigurationModel('branding'))
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('theme.branding.logo')
|
||||
(new Configuration('theme.branding.logo'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(TextType::class),
|
||||
(new Configuration())
|
||||
->setName('theme.branding.company')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new AllowedHtmlTags(['tags' => '<b><i><u><strong><em><img><svg>'])]),
|
||||
(new Configuration())
|
||||
->setName('theme.branding.mini')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new AllowedHtmlTags(['tags' => '<b><i><u><strong><em><img><svg>'])]),
|
||||
(new Configuration())
|
||||
->setName('theme.branding.title')
|
||||
(new Configuration('theme.branding.company'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(TextType::class),
|
||||
(new Configuration())
|
||||
->setName('company.financial_year')
|
||||
(new Configuration('company.financial_year'))
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(DatePickerType::class)
|
||||
->setOptions(['input' => 'string']),
|
||||
->setOptions(['input' => 'string']),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -16,28 +16,27 @@ use App\Form\TagEditForm;
|
||||
use App\Form\Toolbar\TagToolbarForm;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/tags")
|
||||
* @Security("is_granted('view_tag')")
|
||||
*/
|
||||
class TagController extends AbstractController
|
||||
#[Route(path: '/admin/tags')]
|
||||
#[Security("is_granted('view_tag')")]
|
||||
final class TagController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="tags", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="tags_paginated", methods={"GET"})
|
||||
*
|
||||
* @param TagRepository $repository
|
||||
* @param Request $request
|
||||
* @param int $page
|
||||
* @return Response
|
||||
*/
|
||||
public function listTags(TagRepository $repository, Request $request, $page)
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'tags', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'tags_paginated', methods: ['GET'])]
|
||||
public function listTags(TagRepository $repository, Request $request, $page): Response
|
||||
{
|
||||
$query = new TagQuery();
|
||||
$query->setPage($page);
|
||||
@@ -47,24 +46,37 @@ class TagController extends AbstractController
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
$tags = $repository->getTagCount($query);
|
||||
$entries = $repository->getTagCount($query);
|
||||
$multiUpdateForm = $this->getMultiUpdateForm($repository);
|
||||
|
||||
$table = new DataTable('admin_tags', $query);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPagination($entries);
|
||||
$table->setPaginationRoute('tags_paginated');
|
||||
$table->setReloadEvents('kimai.tagUpdate');
|
||||
$table->setBatchForm($multiUpdateForm);
|
||||
|
||||
if ($multiUpdateForm !== null) {
|
||||
$multiUpdateForm = $multiUpdateForm->createView();
|
||||
$table->addColumn('id', ['class' => 'alwaysVisible multiCheckbox', 'orderBy' => false, 'title' => false, 'batchUpdate' => true]);
|
||||
}
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('amount', ['class' => 'text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('tags');
|
||||
$page->setActionName('tags');
|
||||
$page->setHelp('tags.html');
|
||||
$page->setDataTable($table);
|
||||
|
||||
return $this->render('tags/index.html.twig', [
|
||||
'tags' => $tags,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'multiUpdateForm' => $multiUpdateForm,
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="tags_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_tag')")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'tags_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_tag')")]
|
||||
public function editAction(Tag $tag, TagRepository $repository, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TagEditForm::class, $tag, [
|
||||
@@ -81,20 +93,22 @@ class TagController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('tags');
|
||||
$page->setHelp('tags.html');
|
||||
|
||||
return $this->render('tags/edit.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'tag' => $tag,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="tags_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_tag')")
|
||||
*/
|
||||
#[Route(path: '/create', name: 'tags_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('manage_tag')")]
|
||||
public function createAction(TagRepository $repository, Request $request)
|
||||
{
|
||||
$tag = new Tag();
|
||||
@@ -113,20 +127,22 @@ class TagController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('tags');
|
||||
$page->setHelp('tags.html');
|
||||
|
||||
return $this->render('tags/edit.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'tag' => $tag,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="tags_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_tag')")
|
||||
*/
|
||||
#[Route(path: '/multi-delete', name: 'tags_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_tag')")]
|
||||
public function multiDelete(TagRepository $repository, Request $request)
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
@@ -146,7 +162,7 @@ class TagController extends AbstractController
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
protected function getMultiUpdateForm(TagRepository $repository): ?FormInterface
|
||||
private function getMultiUpdateForm(TagRepository $repository): ?FormInterface
|
||||
{
|
||||
$dto = new MultiUpdateTableDTO();
|
||||
if ($this->isGranted('delete_tag')) {
|
||||
@@ -164,17 +180,12 @@ class TagController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TagQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(TagQuery $query)
|
||||
private function getToolbarForm(TagQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TagToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(TagToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('tags', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,41 +16,31 @@ use App\Form\TeamProjectForm;
|
||||
use App\Form\Toolbar\TeamToolbarForm;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/teams")
|
||||
* @Security("is_granted('view_team')")
|
||||
*/
|
||||
#[Route(path: '/admin/teams')]
|
||||
#[Security("is_granted('view_team')")]
|
||||
final class TeamController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(TeamRepository $repository)
|
||||
public function __construct(private TeamRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_team", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_team_paginated", methods={"GET"})
|
||||
*
|
||||
* @param TeamRepository $repository
|
||||
* @param Request $request
|
||||
* @param int $page
|
||||
* @return Response
|
||||
*/
|
||||
public function listTeams(TeamRepository $repository, Request $request, $page)
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_team', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_team_paginated', methods: ['GET'])]
|
||||
public function listTeams(TeamRepository $repository, Request $request, $page): Response
|
||||
{
|
||||
$query = new TeamQuery();
|
||||
$query->setPage($page);
|
||||
@@ -61,69 +51,66 @@ final class TeamController extends AbstractController
|
||||
return $this->redirectToRoute('admin_team');
|
||||
}
|
||||
|
||||
$teams = $repository->getPagerfantaForQuery($query);
|
||||
$entries = $repository->getPagerfantaForQuery($query);
|
||||
|
||||
$table = new DataTable('admin_teams', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_team_paginated');
|
||||
$table->setReloadEvents('kimai.teamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('teamlead', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
$table->addColumn('teamlead_avatar', ['title' => 'team.member', 'translation_domain' => 'teams', 'class' => 'd-none d-lg-table-cell avatars avatar-list avatar-list-stacked', 'orderBy' => false]);
|
||||
$table->addColumn('user', ['class' => 'd-none badges', 'orderBy' => false, 'title' => 'user']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
$page->setActionName('teams');
|
||||
$page->setHelp('teams.html');
|
||||
$page->setDataTable($table);
|
||||
|
||||
return $this->render('team/index.html.twig', [
|
||||
'teams' => $teams,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_team_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_team')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
* @return Response
|
||||
*/
|
||||
public function createTeam(Request $request)
|
||||
#[Route(path: '/create', name: 'admin_team_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_team')")]
|
||||
public function createTeam(Request $request): Response
|
||||
{
|
||||
return $this->renderEditScreen(new Team(), $request);
|
||||
return $this->renderEditScreen(new Team(''), $request, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/duplicate/{token}", name="team_duplicate", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team) and is_granted('create_team')")
|
||||
*/
|
||||
public function duplicateTeam(Team $team, string $token, CsrfTokenManagerInterface $csrfTokenManager)
|
||||
#[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team) and is_granted('create_team')")]
|
||||
public function duplicateTeam(Team $team, Request $request)
|
||||
{
|
||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('team.duplicate', $token))) {
|
||||
$this->flashError('action.csrf.error');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
}
|
||||
|
||||
$csrfTokenManager->refreshToken('team.duplicate');
|
||||
|
||||
$newTeam = clone $team;
|
||||
$newTeam->setName($team->getName() . ' [COPY]');
|
||||
|
||||
try {
|
||||
$this->repository->saveTeam($newTeam);
|
||||
$this->flashSuccess('action.update.success');
|
||||
$i = 1;
|
||||
do {
|
||||
$newName = sprintf('%s (%s)', $team->getName(), $i++);
|
||||
} while ($this->repository->count(['name' => $newName]) > 0 && $i < 10);
|
||||
$newTeam->setName($newName);
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $newTeam->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_team');
|
||||
return $this->renderEditScreen($newTeam, $request, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_team_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'admin_team_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team)")]
|
||||
public function editAction(Team $team, Request $request)
|
||||
{
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit_member", name="admin_team_member", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit_member', name: 'admin_team_member', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', team)")]
|
||||
public function editMemberAction(Team $team, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
@@ -144,13 +131,17 @@ final class TeamController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
$page->setHelp('teams.html');
|
||||
|
||||
return $this->render('team/edit_member.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'team' => $team,
|
||||
'form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderEditScreen(Team $team, Request $request): Response
|
||||
private function renderEditScreen(Team $team, Request $request, bool $create = false): Response
|
||||
{
|
||||
$customerForm = null;
|
||||
$projectForm = null;
|
||||
@@ -173,9 +164,13 @@ final class TeamController extends AbstractController
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($create) {
|
||||
return $this->redirectToRouteAfterCreate('admin_team_edit', ['id' => $team->getId()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,21 +208,24 @@ final class TeamController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
$page->setHelp('teams.html');
|
||||
|
||||
return $this->render('team/edit.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'team' => $team,
|
||||
'form' => $editForm->createView(),
|
||||
'customerForm' => $customerForm ? $customerForm->createView() : null,
|
||||
'projectForm' => $projectForm ? $projectForm->createView() : null,
|
||||
'customerForm' => $customerForm?->createView(),
|
||||
'projectForm' => $projectForm?->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getToolbarForm(TeamQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TeamToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(TeamToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_team', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Event\TimesheetDuplicatePostEvent;
|
||||
use App\Event\TimesheetDuplicatePreEvent;
|
||||
@@ -26,12 +25,14 @@ use App\Form\TimesheetEditForm;
|
||||
use App\Form\TimesheetPreCreateForm;
|
||||
use App\Form\Toolbar\TimesheetExportToolbarForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -40,33 +41,13 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class TimesheetAbstractController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
/**
|
||||
* @var TimesheetService
|
||||
*/
|
||||
protected $service;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(
|
||||
TimesheetRepository $repository,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
TimesheetService $timesheetService,
|
||||
SystemConfiguration $configuration
|
||||
protected TimesheetRepository $repository,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected TimesheetService $service,
|
||||
protected SystemConfiguration $configuration,
|
||||
protected TagRepository $tagRepository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->service = $timesheetService;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
@@ -74,37 +55,75 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $this->service->getActiveTrackingMode();
|
||||
}
|
||||
|
||||
protected function index(TimesheetQuery $query, Request $request, string $route, string $renderTemplate, string $location): Response
|
||||
protected function index(TimesheetQuery $query, Request $request, string $route, string $paginationRoute, string $location): Response
|
||||
{
|
||||
$form = $this->getToolbarForm($query);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute($route);
|
||||
}
|
||||
|
||||
$tags = $query->getTags(true);
|
||||
if (!empty($tags)) {
|
||||
/** @var TagRepository $tagRepo */
|
||||
$tagRepo = $this->getDoctrine()->getRepository(Tag::class);
|
||||
$query->setTags(
|
||||
new ArrayCollection(
|
||||
$tagRepo->findIdsByTagNameList(implode(',', $tags))
|
||||
)
|
||||
);
|
||||
}
|
||||
$canSeeRate = $this->canSeeRate();
|
||||
$canSeeUsername = $this->canSeeUsername();
|
||||
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$pager = $this->repository->getPagerfantaForQuery($query);
|
||||
$result = $this->repository->getTimesheetResult($query);
|
||||
$metaColumns = $this->findMetaColumns($query, $location);
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
'entries' => $pager,
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'multiUpdateForm' => $this->getMultiUpdateActionForm()->createView(),
|
||||
$table = new DataTable($this->getTableName(), $query);
|
||||
$table->setPagination($result->getPagerfanta());
|
||||
$table->setSearchForm($form);
|
||||
$table->setBatchForm($this->getMultiUpdateActionForm());
|
||||
$table->setPaginationRoute($paginationRoute);
|
||||
$table->setReloadEvents('kimai.timesheetUpdate kimai.timesheetDelete');
|
||||
|
||||
$table->addColumn('date', ['class' => 'alwaysVisible', 'orderBy' => 'begin']);
|
||||
|
||||
if ($this->canSeeStartEndTime()) {
|
||||
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'begin']);
|
||||
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'end']);
|
||||
}
|
||||
|
||||
$table->addColumn('duration', ['class' => 'text-end text-nowrap']);
|
||||
|
||||
if ($canSeeRate) {
|
||||
$table->addColumn('hourlyRate', ['class' => 'text-end d-none']);
|
||||
$table->addColumn('rate', ['class' => 'text-end']);
|
||||
}
|
||||
|
||||
$table->addColumn('customer', ['class' => 'd-none d-md-table-cell']);
|
||||
$table->addColumn('project', ['class' => 'd-none d-lg-table-cell']);
|
||||
$table->addColumn('activity', ['class' => 'd-none d-xl-table-cell']);
|
||||
$table->addColumn('description', ['class' => 'd-none']);
|
||||
$table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
|
||||
foreach ($metaColumns as $metaColumn) {
|
||||
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false]);
|
||||
}
|
||||
|
||||
if ($canSeeUsername) {
|
||||
$table->addColumn('username', ['class' => 'd-none d-sm-table-cell', 'orderBy' => false]);
|
||||
}
|
||||
|
||||
$table->addColumn('billable', ['class' => 'text-center d-none w-min', 'orderBy' => false]);
|
||||
$table->addColumn('exported', ['class' => 'text-center d-none w-min', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup();
|
||||
$page->setActionName($this->getActionName());
|
||||
|
||||
return $this->render('timesheet/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'action_single' => $this->getActionNameSingle(),
|
||||
'canSeeUsername' => $canSeeUsername,
|
||||
'canSeeRate' => $canSeeRate,
|
||||
'stats' => $result->getStatistic(),
|
||||
'showSummary' => $this->includeSummary(),
|
||||
'showStartEndTime' => $this->canSeeStartEndTime(),
|
||||
'metaColumns' => $this->findMetaColumns($query, $location),
|
||||
'metaColumns' => $metaColumns,
|
||||
'allowMarkdown' => $this->hasMarkdownSupport(),
|
||||
'editRoute' => $this->getEditRoute()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -121,7 +140,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
protected function edit(Timesheet $entry, Request $request, string $renderTemplate): Response
|
||||
protected function edit(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -140,13 +159,16 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
return $this->render('timesheet/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'route_back' => $this->getTimesheetRoute(),
|
||||
'timesheet' => $entry,
|
||||
'form' => $editForm->createView(),
|
||||
'template' => $this->getTrackingMode()->getEditTemplate(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function create(Request $request, string $renderTemplate): Response
|
||||
protected function create(Request $request): Response
|
||||
{
|
||||
$entry = $this->service->createNewTimesheet($this->getUser());
|
||||
|
||||
@@ -166,17 +188,20 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $createForm);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
return $this->render('timesheet/edit.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'route_back' => $this->getTimesheetRoute(),
|
||||
'timesheet' => $entry,
|
||||
'form' => $createForm->createView(),
|
||||
'template' => $this->getTrackingMode()->getEditTemplate(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function duplicate(Timesheet $timesheet, Request $request, string $renderTemplate): Response
|
||||
protected function duplicate(Timesheet $timesheet, Request $request): Response
|
||||
{
|
||||
$copyTimesheet = clone $timesheet;
|
||||
|
||||
@@ -195,25 +220,26 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
$this->handleFormUpdateException($ex, $form);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
return $this->render('timesheet/edit.html.twig', [
|
||||
'timesheet' => $copyTimesheet,
|
||||
'form' => $form->createView(),
|
||||
'template' => $this->getTrackingMode()->getEditTemplate(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function export(Request $request, ServiceExport $serviceExport): Response
|
||||
{
|
||||
$query = $this->createDefaultQuery();
|
||||
$query->setOrder(TimesheetQuery::ORDER_ASC);
|
||||
$query->setOrder(BaseQuery::ORDER_ASC);
|
||||
|
||||
$form = $this->getExportForm($query);
|
||||
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
$this->ignorePersistedSearch($request);
|
||||
$request->query->set('performSearch', true);
|
||||
}
|
||||
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
@@ -248,6 +274,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('timesheet/layout-export.html.twig', [
|
||||
'page_setup' => new PageSetup('export'),
|
||||
'form' => $form->createView(),
|
||||
'route_back' => $this->getTimesheetRoute(),
|
||||
'exporter' => $serviceExport->getTimesheetExporter(),
|
||||
@@ -255,7 +282,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function multiUpdate(Request $request, string $renderTemplate)
|
||||
protected function multiUpdate(Request $request)
|
||||
{
|
||||
$dto = new TimesheetMultiUpdateDTO();
|
||||
|
||||
@@ -376,9 +403,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
return $this->render('timesheet/multi-update.html.twig', [
|
||||
'page_setup' => $this->createPageSetup(),
|
||||
'form' => $form->createView(),
|
||||
'dto' => $dto,
|
||||
'back' => $this->getTimesheetRoute(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -455,10 +484,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone(),
|
||||
'customer' => true,
|
||||
'create_activity' => $this->isGranted('create_activity'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -467,7 +495,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
* @param int $page
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page)
|
||||
protected function getEditForm(Timesheet $entry, $page): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
@@ -480,12 +508,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||
'include_billable' => $this->isGranted('edit_billable', $entry),
|
||||
'include_user' => $this->includeUserInForms('edit'),
|
||||
'create_activity' => $this->isGranted('create_activity'),
|
||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone(),
|
||||
'customer' => true,
|
||||
]);
|
||||
@@ -493,19 +520,18 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
protected function getToolbarForm(TimesheetQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TimesheetToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(TimesheetToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl($this->getTimesheetRoute(), [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'method' => 'GET',
|
||||
'include_user' => $this->includeUserInForms('toolbar'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getExportForm(TimesheetQuery $query): FormInterface
|
||||
private function getExportForm(TimesheetQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TimesheetExportToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(TimesheetExportToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl($this->getExportRoute()),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'method' => Request::METHOD_POST,
|
||||
@@ -535,7 +561,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
protected function includeSummary(): bool
|
||||
{
|
||||
return (bool) $this->getUser()->getPreferenceValue('timesheet.daily_stats', false, false);
|
||||
return (bool) $this->getUser()->getPreferenceValue('daily_stats', false, false);
|
||||
}
|
||||
|
||||
protected function includeUserInForms(string $formName): bool
|
||||
@@ -586,6 +612,44 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function canSeeRate(): bool
|
||||
{
|
||||
return $this->isGranted('view_rate_own_timesheet');
|
||||
}
|
||||
|
||||
protected function canSeeUsername(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function hasMarkdownSupport(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getTableName(): string
|
||||
{
|
||||
return 'timesheet';
|
||||
}
|
||||
|
||||
protected function getActionName(): string
|
||||
{
|
||||
return 'timesheets';
|
||||
}
|
||||
|
||||
protected function getActionNameSingle(): string
|
||||
{
|
||||
return 'timesheet';
|
||||
}
|
||||
|
||||
protected function createPageSetup(): PageSetup
|
||||
{
|
||||
$page = new PageSetup('timesheet.title');
|
||||
$page->setHelp('timesheet.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
abstract protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface;
|
||||
|
||||
abstract protected function getCreateForm(Timesheet $entry): FormInterface;
|
||||
|
||||
@@ -19,77 +19,61 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/timesheet")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends TimesheetAbstractController
|
||||
#[Route(path: '/timesheet')]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
final class TimesheetController extends TimesheetAbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'timesheet_paginated', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = $this->createDefaultQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
return $this->index($query, $request, 'timesheet', 'timesheet/index.html.twig', TimesheetMetaDisplayEvent::TIMESHEET);
|
||||
return $this->index($query, $request, 'timesheet', 'timesheet_paginated', TimesheetMetaDisplayEvent::TIMESHEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export/", name="timesheet_export", methods={"GET", "POST"})
|
||||
* @Security("is_granted('export_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/export/', name: 'timesheet_export', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('export_own_timesheet')")]
|
||||
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||
{
|
||||
return $this->export($request, $serviceExport);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'timesheet_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', entry)")]
|
||||
public function editAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->edit($entry, $request, 'timesheet/edit.html.twig');
|
||||
return $this->edit($entry, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/duplicate", name="timesheet_duplicate", methods={"GET", "POST"})
|
||||
* @Security("is_granted('duplicate', entry)")
|
||||
*/
|
||||
#[Route(path: '/{id}/duplicate', name: 'timesheet_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('duplicate', entry)")]
|
||||
public function duplicateAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->duplicate($entry, $request, 'timesheet/edit.html.twig');
|
||||
return $this->duplicate($entry, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-update", name="timesheet_multi_update", methods={"POST"})
|
||||
* @Security("is_granted('edit_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/multi-update', name: 'timesheet_multi_update', methods: ['POST'])]
|
||||
#[Security("is_granted('edit_own_timesheet')")]
|
||||
public function multiUpdateAction(Request $request): Response
|
||||
{
|
||||
return $this->multiUpdate($request, 'timesheet/multi-update.html.twig');
|
||||
return $this->multiUpdate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="timesheet_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/multi-delete', name: 'timesheet_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_own_timesheet')")]
|
||||
public function multiDeleteAction(Request $request): Response
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/create', name: 'timesheet_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_own_timesheet')")]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->create($request, 'timesheet/edit.html.twig');
|
||||
return $this->create($request);
|
||||
}
|
||||
|
||||
protected function getCreateForm(Timesheet $entry): FormInterface
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Form\Model\MultiUserTimesheet;
|
||||
use App\Form\TimesheetAdminEditForm;
|
||||
use App\Form\TimesheetMultiUserEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -26,69 +27,51 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/team/timesheet")
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*/
|
||||
class TimesheetTeamController extends TimesheetAbstractController
|
||||
#[Route(path: '/team/timesheet')]
|
||||
#[Security("is_granted('view_other_timesheet')")]
|
||||
final class TimesheetTeamController extends TimesheetAbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_timesheet_paginated', methods: ['GET'])]
|
||||
#[Security("is_granted('view_other_timesheet')")]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = $this->createDefaultQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
return $this->index($query, $request, 'admin_timesheet', 'timesheet-team/index.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);
|
||||
return $this->index($query, $request, 'admin_timesheet', 'admin_timesheet_paginated', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export/", name="admin_timesheet_export", methods={"GET", "POST"})
|
||||
* @Security("is_granted('export_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/export/', name: 'admin_timesheet_export', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('export_other_timesheet')")]
|
||||
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||
{
|
||||
return $this->export($request, $serviceExport);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'admin_timesheet_edit', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('edit', entry)")]
|
||||
public function editAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->edit($entry, $request, 'timesheet-team/edit.html.twig');
|
||||
return $this->edit($entry, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/duplicate", name="admin_timesheet_duplicate", methods={"GET", "POST"})
|
||||
* @Security("is_granted('duplicate', entry)")
|
||||
*/
|
||||
#[Route(path: '/{id}/duplicate', name: 'admin_timesheet_duplicate', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('duplicate', entry)")]
|
||||
public function duplicateAction(Timesheet $entry, Request $request): Response
|
||||
{
|
||||
return $this->duplicate($entry, $request, 'timesheet-team/edit.html.twig');
|
||||
return $this->duplicate($entry, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/create', name: 'admin_timesheet_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_other_timesheet')")]
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
return $this->create($request, 'timesheet-team/edit.html.twig');
|
||||
return $this->create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create_mu", name="admin_timesheet_create_multiuser", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/create_mu', name: 'admin_timesheet_create_multiuser', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_other_timesheet')")]
|
||||
public function createForMultiUserAction(Request $request): Response
|
||||
{
|
||||
$entry = new MultiUserTimesheet();
|
||||
@@ -134,13 +117,16 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
// FIXME I guess this will save timesheets for some users, but then fail only for single users
|
||||
// FIXME we should run in a transaction or disallow to create running timesheets
|
||||
$this->handleFormUpdateException($ex, $createForm);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('timesheet-team/edit.html.twig', [
|
||||
return $this->render('timesheet/edit.html.twig', [
|
||||
'timesheet' => $entry,
|
||||
'form' => $createForm->createView(),
|
||||
'template' => $this->getTrackingMode()->getEditTemplate(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -158,26 +144,20 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'customer' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-update", name="admin_timesheet_multi_update", methods={"POST"})
|
||||
* @Security("is_granted('edit_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/multi-update', name: 'admin_timesheet_multi_update', methods: ['POST'])]
|
||||
#[Security("is_granted('edit_other_timesheet')")]
|
||||
public function multiUpdateAction(Request $request): Response
|
||||
{
|
||||
return $this->multiUpdate($request, 'timesheet-team/multi-update.html.twig');
|
||||
return $this->multiUpdate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="admin_timesheet_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_other_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/multi-delete', name: 'admin_timesheet_multi_delete', methods: ['POST'])]
|
||||
#[Security("is_granted('delete_other_timesheet')")]
|
||||
public function multiDeleteAction(Request $request): Response
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
@@ -261,4 +241,42 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
{
|
||||
return 'TeamTimes';
|
||||
}
|
||||
|
||||
protected function canSeeRate(): bool
|
||||
{
|
||||
return $this->isGranted('view_rate_other_timesheet');
|
||||
}
|
||||
|
||||
protected function canSeeUsername(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasMarkdownSupport(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getTableName(): string
|
||||
{
|
||||
return 'timesheet_admin';
|
||||
}
|
||||
|
||||
protected function getActionName(): string
|
||||
{
|
||||
return 'timesheets_team';
|
||||
}
|
||||
|
||||
protected function getActionNameSingle(): string
|
||||
{
|
||||
return 'timesheet_team';
|
||||
}
|
||||
|
||||
protected function createPageSetup(): PageSetup
|
||||
{
|
||||
$page = new PageSetup('all_times');
|
||||
$page->setHelp('timesheet.html');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,54 +22,30 @@ use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage users in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/user")
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
#[Route(path: '/admin/user')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_user')")]
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
private $encoder;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, UserRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
|
||||
*/
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_user', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_user_paginated', methods: ['GET'])]
|
||||
public function indexAction($page, Request $request): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
@@ -81,15 +57,43 @@ final class UserController extends AbstractController
|
||||
return $this->redirectToRoute('admin_user');
|
||||
}
|
||||
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::USERS);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$table = new DataTable('user_admin', $query);
|
||||
$table->setPagination($entries);
|
||||
$table->setSearchForm($form);
|
||||
$table->setPaginationRoute('admin_user_paginated');
|
||||
$table->setReloadEvents('kimai.userUpdate');
|
||||
|
||||
$table->addColumn('avatar', ['class' => 'alwaysVisible w-avatar', 'title' => null, 'orderBy' => false]);
|
||||
$table->addColumn('user', ['class' => 'alwaysVisible', 'orderBy' => 'user']);
|
||||
$table->addColumn('username', ['class' => 'd-none']);
|
||||
$table->addColumn('alias', ['class' => 'd-none']);
|
||||
$table->addColumn('account_number', ['class' => 'd-none']);
|
||||
$table->addColumn('title', ['class' => 'd-none']);
|
||||
$table->addColumn('email', ['class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('lastLogin', ['class' => 'd-none', 'orderBy' => false]);
|
||||
$table->addColumn('roles', ['class' => 'd-none', 'orderBy' => false]);
|
||||
|
||||
foreach ($event->getPreferences() as $userPreference) {
|
||||
$table->addColumn('mf_' . $userPreference->getName(), ['title' => $userPreference->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'translation_domain' => 'messages', 'data' => $userPreference]);
|
||||
}
|
||||
|
||||
$table->addColumn('team', ['class' => 'text-center w-min', 'orderBy' => false]);
|
||||
$table->addColumn('active', ['class' => 'd-none w-min', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('users');
|
||||
$page->setHelp('users.html');
|
||||
$page->setActionName('users');
|
||||
$page->setDataTable($table);
|
||||
|
||||
return $this->render('user/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'preferences' => $event->getPreferences(),
|
||||
]);
|
||||
}
|
||||
@@ -105,11 +109,9 @@ final class UserController extends AbstractController
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_user')")
|
||||
*/
|
||||
public function createAction(Request $request, SystemConfiguration $config): Response
|
||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('create_user')")]
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
@@ -117,41 +119,28 @@ final class UserController extends AbstractController
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$password = $this->passwordHasher->hashPassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($password);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
$userRepository->saveUser($user);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() !== true) {
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $user->getUsername()]);
|
||||
}
|
||||
|
||||
$firstUser = $user;
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$user->setLanguage($firstUser->getLanguage());
|
||||
$user->setTimezone($firstUser->getTimezone());
|
||||
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
if ($editForm->has('create_more')) {
|
||||
$editForm->get('create_more')->setData(true);
|
||||
}
|
||||
return $this->redirectToRouteAfterCreate('user_profile_edit', ['username' => $user->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
$page = new PageSetup('users');
|
||||
$page->setHelp('users.html');
|
||||
|
||||
return $this->render('user/create.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'user' => $user,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', userToDelete)")
|
||||
*/
|
||||
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository): Response
|
||||
#[Route(path: '/{id}/delete', name: 'admin_user_delete', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('delete', userToDelete)")]
|
||||
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository, UserService $userService): Response
|
||||
{
|
||||
// $userToDelete MUST not be called $user, as $user is always the current user!
|
||||
$stats = $repository->getUserStatistics($userToDelete);
|
||||
@@ -181,7 +170,7 @@ final class UserController extends AbstractController
|
||||
|
||||
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->deleteUser($userToDelete, $deleteForm->get('user')->getData());
|
||||
$userService->deleteUser($userToDelete, $deleteForm->get('user')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
@@ -190,17 +179,19 @@ final class UserController extends AbstractController
|
||||
return $this->redirectToRoute('admin_user');
|
||||
}
|
||||
|
||||
$page = new PageSetup('users');
|
||||
$page->setHelp('users.html');
|
||||
|
||||
return $this->render('user/delete.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'user' => $userToDelete,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="user_export", methods={"GET"})
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'user_export', methods: ['GET'])]
|
||||
#[Security("is_granted('view_user')")]
|
||||
public function exportAction(Request $request, UserExporter $exporter)
|
||||
{
|
||||
$query = new UserQuery();
|
||||
@@ -214,7 +205,7 @@ final class UserController extends AbstractController
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->getRepository()->getUsersForQuery($query);
|
||||
$entries = $this->repository->getUsersForQuery($query);
|
||||
|
||||
$spreadsheet = $exporter->export(
|
||||
$entries,
|
||||
@@ -227,11 +218,10 @@ final class UserController extends AbstractController
|
||||
|
||||
protected function getToolbarForm(UserQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(UserToolbarForm::class, $query, [
|
||||
return $this->createSearchForm(UserToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_user', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -242,7 +232,6 @@ final class UserController extends AbstractController
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => true,
|
||||
'include_preferences' => true,
|
||||
'include_add_more' => true,
|
||||
'include_teams' => $this->isGranted('teams_other_profile'),
|
||||
'include_roles' => $this->isGranted('roles_other_profile'),
|
||||
]);
|
||||
|
||||
@@ -13,16 +13,12 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/widgets")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
#[Route(path: '/widgets')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
|
||||
final class WidgetController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/working-time/{year}/{week}", requirements={"year": "[1-9]\d*", "week": "[0-9]\d*"}, name="widgets_working_time_chart", methods={"GET"})
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
#[Route(path: '/working-time/{year}/{week}', requirements: ['year' => '[1-9]\d*', 'week' => '[0-9]\d*'], name: 'widgets_working_time_chart', methods: ['GET'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function workingtimechartAction($year, $week): Response
|
||||
{
|
||||
return $this->render('widget/paginatedworkingtimechart.html.twig', [
|
||||
|
||||
94
src/Controller/WizardController.php
Normal file
94
src/Controller/WizardController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\User\UserService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route(path: '/wizard')]
|
||||
#[Security("is_granted('IS_AUTHENTICATED_FULLY')")]
|
||||
final class WizardController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/{wizard}', name: 'wizard', methods: ['GET', 'POST'])]
|
||||
#[Security("is_granted('view_own_timesheet')")]
|
||||
public function wizard(Request $request, UserService $userService, string $wizard): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($wizard === 'intro') {
|
||||
$user->setWizardAsSeen('intro');
|
||||
$userService->updateUser($user);
|
||||
|
||||
return $this->render('wizard/intro.html.twig', [
|
||||
'percent' => 0,
|
||||
'next' => 'profile',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($wizard === 'profile') {
|
||||
$data = [
|
||||
'language' => $request->getLocale(),
|
||||
'timezone' => $user->getTimezone(),
|
||||
];
|
||||
|
||||
$form = $this->createFormBuilder($data)
|
||||
->add(UserPreference::LOCALE, LanguageType::class)
|
||||
->add(UserPreference::TIMEZONE, TimezoneType::class)
|
||||
->add(UserPreference::SKIN, SkinType::class, [
|
||||
'attr' => [
|
||||
'onchange' => "document.body.classList.remove('theme-light');document.body.classList.remove('theme-light');"
|
||||
],
|
||||
])
|
||||
->setAction($this->generateUrl('wizard', ['wizard' => 'profile']))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var array<string, string> $data */
|
||||
$data = $form->getData();
|
||||
$user->setLanguage($data[UserPreference::LOCALE]);
|
||||
$user->setTimezone($data[UserPreference::TIMEZONE]);
|
||||
$user->setPreferenceValue(UserPreference::SKIN, $data[UserPreference::SKIN]);
|
||||
$user->setWizardAsSeen('profile');
|
||||
$userService->updateUser($user);
|
||||
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
|
||||
}
|
||||
|
||||
return $this->render('wizard/profile.html.twig', [
|
||||
'percent' => \intval(100 / \count(User::WIZARDS) * 1),
|
||||
'previous' => 'intro',
|
||||
'next' => 'done',
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
// this is a virtual step that is not registered as wizard, but instead should be shown every time
|
||||
// a new wizard is introduced: so we do not register it as "seen"
|
||||
if ($wizard === 'done') {
|
||||
return $this->render('wizard/done.html.twig', [
|
||||
'percent' => 100,
|
||||
'previous' => 'profile',
|
||||
]);
|
||||
}
|
||||
|
||||
throw $this->createNotFoundException('Unknown wizard');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user