weekly quick-entry form (#2793)
This commit is contained in:
@@ -115,7 +115,8 @@ final class ReloadCommand extends Command
|
||||
$io->warning(
|
||||
[
|
||||
'Cache could not be rebuilt.',
|
||||
'Please run the cache commands manually:',
|
||||
'Please run these commands to rebuild the cache manually:',
|
||||
'rm -r var/cache/*' . PHP_EOL .
|
||||
'bin/console cache:clear --env=' . $environment . PHP_EOL .
|
||||
'bin/console cache:warmup --env=' . $environment
|
||||
]
|
||||
|
||||
@@ -29,7 +29,6 @@ use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Exception;
|
||||
@@ -314,6 +313,13 @@ final class ActivityController extends AbstractController
|
||||
{
|
||||
$stats = $statisticService->getActivityStatistics($activity);
|
||||
|
||||
$options = [
|
||||
'projects' => $activity->getProject(),
|
||||
'query_builder_for_user' => true,
|
||||
'ignore_activity' => $activity,
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.activityDelete',
|
||||
@@ -321,17 +327,7 @@ final class ActivityController extends AbstractController
|
||||
'data-msg-error' => 'action.delete.error',
|
||||
]
|
||||
])
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
$query = new ActivityFormTypeQuery();
|
||||
$query->addProject($activity->getProject());
|
||||
$query->setActivityToIgnore($activity);
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->add('activity', ActivityType::class, $options)
|
||||
->setAction($this->generateUrl('admin_activity_delete', ['id' => $activity->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
@@ -30,7 +30,6 @@ use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRateRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
@@ -384,14 +383,8 @@ final class CustomerController extends AbstractController
|
||||
]
|
||||
])
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setCustomerToIgnore($customer);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
'ignore_customer' => $customer,
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_customer_delete', ['id' => $customer->getId()]))
|
||||
|
||||
@@ -35,7 +35,6 @@ use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
@@ -428,15 +427,9 @@ final class ProjectController extends AbstractController
|
||||
]
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->addCustomer($project->getCustomer());
|
||||
$query->setProjectToIgnore($project);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'ignore_project' => $project,
|
||||
'customers' => $project->getCustomer(),
|
||||
'query_builder_for_user' => true,
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_project_delete', ['id' => $project->getId()]))
|
||||
|
||||
234
src/Controller/QuickEntryController.php
Normal file
234
src/Controller/QuickEntryController.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\QuickEntryForm;
|
||||
use App\Model\QuickEntryModel;
|
||||
use App\Model\QuickEntryWeek;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
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('view_own_timesheet')")
|
||||
*/
|
||||
class QuickEntryController extends AbstractController
|
||||
{
|
||||
private $configuration;
|
||||
private $timesheetService;
|
||||
private $repository;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration, TimesheetService $timesheetService, TimesheetRepository $repository)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{begin}", name="quick_entry", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit_own_timesheet')")
|
||||
*/
|
||||
public function quickEntry(Request $request, ?string $begin = null)
|
||||
{
|
||||
$mode = $this->timesheetService->getActiveTrackingMode();
|
||||
|
||||
if (!$mode->canEditDuration() && !$mode->canEditEnd()) {
|
||||
$this->flashError('Not allowed');
|
||||
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
|
||||
$factory = $this->getDateTimeFactory();
|
||||
if ($begin === null) {
|
||||
$begin = $factory->createDateTime();
|
||||
} else {
|
||||
$begin = $factory->createDateTime($begin);
|
||||
}
|
||||
|
||||
$startWeek = $factory->getStartOfWeek($begin);
|
||||
$endWeek = $factory->getEndOfWeek($begin);
|
||||
$user = $this->getUser();
|
||||
|
||||
$tmpDay = clone $startWeek;
|
||||
$week = [];
|
||||
while ($tmpDay < $endWeek) {
|
||||
$nextDay = clone $tmpDay;
|
||||
$week[$nextDay->format('Y-m-d')] = ['day' => $nextDay];
|
||||
$tmpDay = $tmpDay->modify('+1 day');
|
||||
}
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query->setBegin($startWeek);
|
||||
$query->setEnd($endWeek);
|
||||
$query->setName('quickEntryForm');
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
$result = $this->repository->getTimesheetResult($query);
|
||||
|
||||
$rows = [];
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($result->getResults(true) as $timesheet) {
|
||||
$i = 0;
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId();
|
||||
$day = $timesheet->getBegin()->format('Y-m-d');
|
||||
|
||||
while (\array_key_exists($id, $rows) && \array_key_exists('entry', $rows[$id]['days'][$day])) {
|
||||
$i++;
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId() . '_' . $i;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($id, $rows)) {
|
||||
$rows[$id] = [
|
||||
'days' => $week,
|
||||
'project' => $timesheet->getProject(),
|
||||
'activity' => $timesheet->getActivity()
|
||||
];
|
||||
}
|
||||
|
||||
$rows[$id]['days'][$day]['entry'] = $timesheet;
|
||||
}
|
||||
|
||||
ksort($rows);
|
||||
|
||||
// attach recent activities
|
||||
$timesheets = $this->repository->getRecentActivities($this->getUser(), null, 5);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId();
|
||||
if (\array_key_exists($id, $rows)) {
|
||||
continue;
|
||||
}
|
||||
$rows[$id] = [
|
||||
'days' => $week,
|
||||
'project' => $timesheet->getProject(),
|
||||
'activity' => $timesheet->getActivity()
|
||||
];
|
||||
}
|
||||
|
||||
$beginTime = $this->configuration->getTimesheetDefaultBeginTime();
|
||||
|
||||
/** @var QuickEntryModel[] $models */
|
||||
$models = [];
|
||||
foreach ($rows as $id => $row) {
|
||||
$model = new QuickEntryModel($user, $row['project'], $row['activity']);
|
||||
foreach ($row['days'] as $dayId => $day) {
|
||||
if (!\array_key_exists('entry', $day)) {
|
||||
$tmp = new Timesheet();
|
||||
$tmp->setUser($user);
|
||||
$tmp->setProject($row['project']);
|
||||
$tmp->setActivity($row['activity']);
|
||||
$tmp->setBegin(clone $day['day']);
|
||||
$tmp->getBegin()->modify($beginTime);
|
||||
$model->addTimesheet($tmp);
|
||||
} else {
|
||||
$model->addTimesheet($day['entry']);
|
||||
}
|
||||
}
|
||||
$models[] = $model;
|
||||
}
|
||||
|
||||
// create prototype model
|
||||
$empty = new QuickEntryModel($user);
|
||||
foreach ($week as $dayId => $day) {
|
||||
$tmp = new Timesheet();
|
||||
$tmp->setUser($user);
|
||||
$tmp->setBegin(clone $day['day']);
|
||||
$tmp->getBegin()->modify($beginTime);
|
||||
$empty->addTimesheet($tmp);
|
||||
}
|
||||
|
||||
// add empty rows for simpler starting
|
||||
$minRows = 3;
|
||||
if (\count($models) < $minRows) {
|
||||
$newRows = $minRows - \count($models);
|
||||
for ($a = 0; $a < $newRows; $a++) {
|
||||
$model = new QuickEntryModel();
|
||||
foreach ($week as $dayId => $day) {
|
||||
$tmp = new Timesheet();
|
||||
$tmp->setUser($user);
|
||||
$tmp->setBegin(clone $day['day']);
|
||||
$tmp->getBegin()->modify($beginTime);
|
||||
$model->addTimesheet($tmp);
|
||||
}
|
||||
|
||||
$models[] = $model;
|
||||
}
|
||||
}
|
||||
|
||||
$formModel = new QuickEntryWeek($startWeek, $models);
|
||||
|
||||
$form = $this->createForm(QuickEntryForm::class, $formModel, [
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'prototype_data' => $empty,
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var QuickEntryWeek $data */
|
||||
$data = $form->getData();
|
||||
|
||||
$saveTimesheets = [];
|
||||
$deleteTimesheets = [];
|
||||
|
||||
foreach ($data->getRows() as $tmpModel) {
|
||||
foreach ($tmpModel->getTimesheets() as $timesheet) {
|
||||
if ($timesheet->getId() !== null) {
|
||||
if ($timesheet->getDuration(false) === null || $timesheet->getEnd() === null) {
|
||||
$deleteTimesheets[] = $timesheet;
|
||||
} else {
|
||||
$saveTimesheets[] = $timesheet;
|
||||
}
|
||||
} else {
|
||||
if ($timesheet->getDuration() !== null) {
|
||||
$saveTimesheets[] = $timesheet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isGranted('delete_own_timesheet') && \count($deleteTimesheets) > 0) {
|
||||
try {
|
||||
$this->timesheetService->deleteMultipleTimesheets($deleteTimesheets);
|
||||
|
||||
return $this->redirectToRoute('quick_entry', ['begin' => $begin->format('Y-m-d')]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
$this->logException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($saveTimesheets) > 0) {
|
||||
try {
|
||||
$this->timesheetService->updateMultipleTimesheets($saveTimesheets);
|
||||
|
||||
return $this->redirectToRoute('quick_entry', ['begin' => $begin->format('Y-m-d')]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error');
|
||||
$this->logException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('quick-entry/index.html.twig', [
|
||||
'days' => $week,
|
||||
'week' => $rows,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
{
|
||||
$this->begin = $begin;
|
||||
$this->timezone = $begin->getTimezone()->getName();
|
||||
// make sure that the original date is always
|
||||
// make sure that the original date is always kept in UTC
|
||||
$this->date = new DateTime($begin->format('Y-m-d 00:00:00'), new DateTimeZone('UTC'));
|
||||
|
||||
return $this;
|
||||
@@ -421,12 +421,13 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
/**
|
||||
* Do not rely on the results of this method for running records.
|
||||
*
|
||||
* @param bool $calculate
|
||||
* @return int|null
|
||||
*/
|
||||
public function getDuration(): ?int
|
||||
public function getDuration(bool $calculate = true): ?int
|
||||
{
|
||||
// only auto calculate if manually set duration is null - the result is important for eg. validations
|
||||
if ($this->duration === null && $this->begin !== null && $this->end !== null) {
|
||||
if ($calculate && $this->duration === null && $this->begin !== null && $this->end !== null) {
|
||||
return $this->end->getTimestamp() - $this->begin->getTimestamp();
|
||||
}
|
||||
|
||||
|
||||
29
src/EventSubscriber/Actions/QuickEntrySubscriber.php
Normal file
29
src/EventSubscriber/Actions/QuickEntrySubscriber.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\EventSubscriber\Actions;
|
||||
|
||||
use App\Event\PageActionsEvent;
|
||||
|
||||
class QuickEntrySubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
public static function getActionName(): string
|
||||
{
|
||||
return 'weekly_times';
|
||||
}
|
||||
|
||||
public function onActions(PageActionsEvent $event): void
|
||||
{
|
||||
if ($this->isGranted('view_own_timesheet')) {
|
||||
$event->addBack($this->path('timesheet'));
|
||||
}
|
||||
|
||||
$event->addHelp($this->documentationLink('weekly-times.html'));
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class TimesheetsSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('create_own_timesheet')) {
|
||||
$event->addCreate($this->path('timesheet_create'));
|
||||
$event->addAction('quick_entry', ['url' => $this->path('quick_entry'), 'class' => 'create-ts', 'icon' => 'weekly-times']);
|
||||
}
|
||||
|
||||
$event->addHelp($this->documentationLink('timesheet.html'));
|
||||
|
||||
@@ -12,8 +12,6 @@ namespace App\EventSubscriber;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use App\Twig\IconExtension;
|
||||
use App\Utils\MenuItemModel;
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
@@ -22,19 +20,11 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
*/
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
private $security;
|
||||
/**
|
||||
* @var IconExtension
|
||||
*/
|
||||
private $icons;
|
||||
|
||||
public function __construct(AuthorizationCheckerInterface $security)
|
||||
{
|
||||
$this->security = $security;
|
||||
$this->icons = new IconExtension();
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
@@ -52,67 +42,62 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->configureMainMenu($event->getMenu());
|
||||
$this->configureAdminMenu($event->getAdminMenu());
|
||||
$this->configureSystemMenu($event->getSystemMenu());
|
||||
}
|
||||
$icons = new IconExtension();
|
||||
|
||||
private function configureMainMenu(SidebarMenuEvent $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
// ------------------- main menu -------------------
|
||||
$menu = $event->getMenu();
|
||||
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$timesheets = new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], $this->getIcon('timesheet'));
|
||||
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
|
||||
$timesheets = new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], $icons->icon('timesheet'));
|
||||
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update', 'quick_entry']);
|
||||
$menu->addItem($timesheets);
|
||||
|
||||
$menu->addItem(
|
||||
new MenuItemModel('calendar', 'calendar.title', 'calendar', [], $this->getIcon('calendar'))
|
||||
new MenuItemModel('calendar', 'calendar.title', 'calendar', [], $icons->icon('calendar'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$invoice = new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $this->getIcon('invoice'));
|
||||
$invoice = new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $icons->icon('invoice'));
|
||||
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create', 'admin_invoice_template_copy', 'admin_invoice_list', 'admin_invoice_document_upload']);
|
||||
$menu->addItem($invoice);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('create_export')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], $this->getIcon('export'))
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], $icons->icon('export'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function configureAdminMenu(MenuItemInterface $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
// ------------------- admin menu -------------------
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$timesheets = new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], $this->getIcon('timesheet-team'));
|
||||
$timesheets = new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], $icons->icon('timesheet-team'));
|
||||
$timesheets->setChildRoutes(['admin_timesheet_export', 'admin_timesheet_edit', 'admin_timesheet_create', 'admin_timesheet_multi_update']);
|
||||
$menu->addChild($timesheets);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_reporting')) {
|
||||
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], $this->getIcon('reporting'));
|
||||
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], $icons->icon('reporting'));
|
||||
$reporting->setChildRoutes(['report_user_week', 'report_user_month', 'report_weekly_users', 'report_monthly_users', 'report_project_view']);
|
||||
$menu->addChild($reporting);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
|
||||
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $icons->icon('customer'));
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $this->getIcon('project'));
|
||||
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $icons->icon('project'));
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
|
||||
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $this->getIcon('activity'));
|
||||
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $icons->icon('activity'));
|
||||
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$menu->addChild($activities);
|
||||
}
|
||||
@@ -122,50 +107,43 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('tags', 'menu.tags', 'tags', [], 'fas fa-tags')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function configureSystemMenu(MenuItemInterface $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
// ------------------- system menu -------------------
|
||||
$menu = $event->getSystemMenu();
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$users = new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], $this->getIcon('users'));
|
||||
$users = new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], $icons->icon('users'));
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences']);
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('role_permissions')) {
|
||||
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], $this->getIcon('permissions'));
|
||||
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], $icons->icon('permissions'));
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_team')) {
|
||||
$teams = new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $this->getIcon('team'));
|
||||
$teams = new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $icons->icon('team'));
|
||||
$teams->setChildRoutes(['admin_team_create', 'admin_team_edit']);
|
||||
$menu->addChild($teams);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('plugins')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $this->getIcon('plugin'))
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $icons->icon('plugin'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_configuration')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], $this->getIcon('configuration'))
|
||||
new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], $icons->icon('configuration'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_information')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('doctor', 'menu.doctor', 'doctor', [], $this->getIcon('doctor'))
|
||||
new MenuItemModel('doctor', 'menu.doctor', 'doctor', [], $icons->icon('doctor'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getIcon(string $icon)
|
||||
{
|
||||
return $this->icons->icon($icon, $icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,15 @@ class DurationStringToSecondsTransformer implements DataTransformerInterface
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->formatter->parseDurationString($formatToInt);
|
||||
$seconds = $this->formatter->parseDurationString($formatToInt);
|
||||
|
||||
// DateTime throws if a duration with too many seconds is passed and an amount of so
|
||||
// many seconds is likely not required in a time-tracking application ;-)
|
||||
if ($seconds > 315360000000000) {
|
||||
throw new TransformationFailedException('Maximum duration exceeded.');
|
||||
}
|
||||
|
||||
return $seconds;
|
||||
} catch (\Exception $e) {
|
||||
throw new TransformationFailedException($e->getMessage());
|
||||
}
|
||||
|
||||
@@ -68,13 +68,23 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
$apiData['route_params'] = [];
|
||||
}
|
||||
|
||||
$formPrefix = $form->getParent()->getName();
|
||||
if (!empty($formPrefix)) {
|
||||
$formPrefix .= '_';
|
||||
$formPrefixes = [];
|
||||
$parent = $form->getParent();
|
||||
do {
|
||||
$formPrefixes[] = $parent->getName();
|
||||
} while (($parent = $parent->getParent()) !== null);
|
||||
|
||||
$formPrefix = implode('_', array_reverse($formPrefixes));
|
||||
|
||||
$formField = $formPrefix;
|
||||
if (!empty($formField)) {
|
||||
$formField .= '_';
|
||||
}
|
||||
$formField .= $apiData['select'];
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'data-related-select' => $formPrefix . $apiData['select'],
|
||||
'data-form-prefix' => $formPrefix,
|
||||
'data-related-select' => $formField,
|
||||
'data-api-url' => $this->router->generate($apiData['route'], $apiData['route_params']),
|
||||
]);
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -29,18 +25,16 @@ use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Helper functions to manage dependent customer-project-activity fields.
|
||||
*
|
||||
* If you always want to show the list of all available projects/activities, use the form types directly.
|
||||
*/
|
||||
trait FormTrait
|
||||
{
|
||||
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null)
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
'customers' => $customer,
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
@@ -49,30 +43,29 @@ trait FormTrait
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null)
|
||||
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null, array $options = [])
|
||||
{
|
||||
$builder->add('project', ProjectType::class, [
|
||||
$options = array_merge([
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
'query_builder_for_user' => true,
|
||||
'join_customer' => true
|
||||
], $options);
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
$builder->add('project', ProjectType::class, array_merge($options, [
|
||||
'projects' => $project,
|
||||
'customers' => $customer,
|
||||
]));
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder, $project, $customer, $isNew) {
|
||||
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options) {
|
||||
$data = $event->getData();
|
||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
$event->getForm()->add('project', ProjectType::class, array_merge($options, [
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
|
||||
// is there a better wa to prevent starting a record with a hidden project ?
|
||||
@@ -90,44 +83,36 @@ trait FormTrait
|
||||
}
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
$query->setWithCustomer(true);
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null)
|
||||
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null, array $options = [])
|
||||
{
|
||||
$builder->add('activity', ActivityType::class, [
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($builder, $activity, $project) {
|
||||
$query = new ActivityFormTypeQuery($activity, $project);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
$options = array_merge(['placeholder' => '', 'query_builder_for_user' => true], $options);
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
$options['projects'] = $project;
|
||||
$options['activities'] = $activity;
|
||||
|
||||
$builder->add('activity', ActivityType::class, $options);
|
||||
|
||||
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder, $activity) {
|
||||
function (FormEvent $event) use ($options) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['project']) || empty($data['project'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, [
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($builder, $data, $activity) {
|
||||
$query = new ActivityFormTypeQuery($activity, $data['project']);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
$options['projects'] = $data['project'];
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
$event->getForm()->add('activity', ActivityType::class, $options);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,7 @@ use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -83,12 +78,8 @@ class TimesheetMultiUpdate extends AbstractType
|
||||
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
'customers' => $customer,
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
@@ -103,28 +94,20 @@ class TimesheetMultiUpdate extends AbstractType
|
||||
$projectOptions['group_by'] = null;
|
||||
}
|
||||
|
||||
$builder
|
||||
->add(
|
||||
'project',
|
||||
ProjectType::class,
|
||||
array_merge($projectOptions, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
])
|
||||
);
|
||||
$builder->add('project', ProjectType::class, array_merge($projectOptions, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'customers' => $customer,
|
||||
'projects' => $project,
|
||||
'query_builder_for_user' => true,
|
||||
]));
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
// TODO replace me with FormTrait
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder, $project, $customer) {
|
||||
function (FormEvent $event) use ($project, $customer) {
|
||||
$data = $event->getData();
|
||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||
@@ -134,45 +117,37 @@ class TimesheetMultiUpdate extends AbstractType
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'customers' => $customer,
|
||||
'projects' => $project,
|
||||
'query_builder_for_user' => true,
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('activity', ActivityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
|
||||
// TODO respect user (team permission)
|
||||
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $project));
|
||||
},
|
||||
])
|
||||
;
|
||||
$activityOptions = [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'activities' => $activity,
|
||||
'query_builder_for_user' => true,
|
||||
];
|
||||
|
||||
$builder->add('activity', ActivityType::class, array_merge($activityOptions, [
|
||||
'projects' => $project,
|
||||
]));
|
||||
|
||||
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
|
||||
// TODO replace me with FormTrait
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($activity) {
|
||||
function (FormEvent $event) use ($activityOptions) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['project']) || empty($data['project'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data, $activity) {
|
||||
// TODO respect user (team permission)
|
||||
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $data['project']));
|
||||
},
|
||||
]);
|
||||
$event->getForm()->add('activity', ActivityType::class, array_merge($activityOptions, [
|
||||
'projects' => $data['project'],
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -90,12 +88,8 @@ class ProjectEditForm extends AbstractType
|
||||
]))
|
||||
->add('customer', CustomerType::class, [
|
||||
'placeholder' => (null === $id && null === $customer) ? '' : false,
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'customers' => $customer,
|
||||
'query_builder_for_user' => true,
|
||||
]);
|
||||
|
||||
$this->addCommonFields($builder, $options);
|
||||
|
||||
89
src/Form/QuickEntryForm.php
Normal file
89
src/Form/QuickEntryForm.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Form\Type\QuickEntryWeekType;
|
||||
use App\Form\Type\WeekPickerType;
|
||||
use App\Model\QuickEntryWeek;
|
||||
use App\Validator\Constraints\QuickEntryModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\All;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
|
||||
class QuickEntryForm extends AbstractType
|
||||
{
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$startDate = new \DateTime();
|
||||
if ($builder->getData() !== null) {
|
||||
/** @var QuickEntryWeek $data */
|
||||
$data = $builder->getData();
|
||||
$startDate = $data->getDate();
|
||||
}
|
||||
|
||||
$builder->add('date', WeekPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
'label' => false,
|
||||
]);
|
||||
|
||||
$builder->add('rows', CollectionType::class, [
|
||||
'label' => false,
|
||||
'entry_type' => QuickEntryWeekType::class,
|
||||
'entry_options' => [
|
||||
'label' => false,
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'start_date' => $startDate,
|
||||
'empty_data' => function (FormInterface $form) use ($options) {
|
||||
return clone $options['prototype_data'];
|
||||
},
|
||||
'prototype_data' => clone $options['prototype_data'],
|
||||
],
|
||||
'prototype_data' => $options['prototype_data'],
|
||||
'allow_add' => true,
|
||||
'constraints' => [
|
||||
new Valid(),
|
||||
new All(['constraints' => [new QuickEntryModel()]])
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'timesheet_quick_edit',
|
||||
'data_class' => QuickEntryWeek::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'prototype_data' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -78,10 +79,30 @@ class ActivityType extends AbstractType
|
||||
'choice_label' => [$this, 'choiceLabel'],
|
||||
'group_by' => [$this, 'groupBy'],
|
||||
'choice_attr' => [$this, 'choiceAttr'],
|
||||
'query_builder' => function (ActivityRepository $repo) {
|
||||
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery());
|
||||
},
|
||||
'query_builder_for_user' => true,
|
||||
// @var Project|Project[]|int|int[]|null
|
||||
'projects' => null,
|
||||
// @var Activity|Activity[]|int|int[]|null
|
||||
'activities' => null,
|
||||
// @var Activity|null
|
||||
'ignore_activity' => null,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (ActivityRepository $repo) use ($options) {
|
||||
$query = new ActivityFormTypeQuery($options['activities'], $options['projects']);
|
||||
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
if (null !== $options['ignore_activity']) {
|
||||
$query->setActivityToIgnore($options['ignore_activity']);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,15 +44,23 @@ class CustomerType extends AbstractType
|
||||
'end_date_param' => '%end%',
|
||||
'ignore_date' => false,
|
||||
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
|
||||
// @var Customer|null
|
||||
'ignore_customer' => null,
|
||||
// @var Customer|Customer[]|null
|
||||
'customers' => null,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (CustomerRepository $repo) use ($options) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query = new CustomerFormTypeQuery($options['customers']);
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
if (null !== $options['ignore_customer']) {
|
||||
$query->setCustomerToIgnore($options['ignore_customer']);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -38,6 +38,12 @@ class DurationType extends AbstractType
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$class = 'duration-input';
|
||||
if (isset($view->vars['attr']['class'])) {
|
||||
$class .= ' ' . $view->vars['attr']['class'];
|
||||
}
|
||||
$view->vars['attr']['class'] = $class;
|
||||
|
||||
if ($options['preset_hours'] === null || $options['preset_minutes'] === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class InitialViewType extends AbstractType
|
||||
'dashboard' => 'menu.homepage',
|
||||
'timesheet' => 'menu.timesheet',
|
||||
'calendar' => 'calendar.title',
|
||||
'quick_entry' => 'quick_entry.title',
|
||||
'my_profile' => 'profile.title',
|
||||
'admin_timesheet' => 'menu.admin_timesheet',
|
||||
'invoice' => 'menu.invoice',
|
||||
@@ -49,6 +50,7 @@ class InitialViewType extends AbstractType
|
||||
'admin_customer' => 'view_customer',
|
||||
'admin_project' => 'view_project',
|
||||
'admin_activity' => 'view_activity',
|
||||
'quick_entry' => 'view_own_timesheet',
|
||||
];
|
||||
|
||||
private $voter;
|
||||
|
||||
@@ -68,21 +68,44 @@ class ProjectType extends AbstractType
|
||||
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
||||
'ignore_date' => false,
|
||||
'join_customer' => false,
|
||||
// @var Project|null
|
||||
'ignore_project' => null,
|
||||
// @var Customer|Customer[]|int|int[]|null
|
||||
'customers' => null,
|
||||
// @var Project|Project[]|int|int[]|null
|
||||
'projects' => null,
|
||||
// @var DateTime|null
|
||||
'project_date_start' => null,
|
||||
// @var DateTime|null
|
||||
'project_date_end' => null,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (ProjectRepository $repo) use ($options) {
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query = new ProjectFormTypeQuery($options['projects'], $options['customers']);
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
if (true === $options['ignore_date']) {
|
||||
$query->setIgnoreDate(true);
|
||||
} else {
|
||||
if ($options['project_date_start'] !== null) {
|
||||
$query->setProjectStart($options['project_date_start']);
|
||||
}
|
||||
if ($options['project_date_end'] !== null) {
|
||||
$query->setProjectEnd($options['project_date_end']);
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $options['join_customer']) {
|
||||
$query->setWithCustomer(true);
|
||||
}
|
||||
|
||||
if (null !== $options['ignore_project']) {
|
||||
$query->setProjectToIgnore($options['ignore_project']);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
});
|
||||
|
||||
108
src/Form/Type/QuickEntryTimesheetType.php
Normal file
108
src/Form/Type/QuickEntryTimesheetType.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class QuickEntryTimesheetType extends AbstractType
|
||||
{
|
||||
private $security;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$durationOptions = [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'placeholder' => '0:00',
|
||||
],
|
||||
];
|
||||
|
||||
$duration = $options['duration_minutes'];
|
||||
if ($duration !== null && (int) $duration > 0) {
|
||||
$durationOptions = array_merge($durationOptions, [
|
||||
'preset_minutes' => $duration
|
||||
]);
|
||||
}
|
||||
|
||||
$duration = $options['duration_hours'];
|
||||
if ($duration !== null && (int) $duration > 0) {
|
||||
$durationOptions = array_merge($durationOptions, [
|
||||
'preset_hours' => $duration,
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add('duration', DurationType::class, $durationOptions);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::POST_SET_DATA,
|
||||
function (FormEvent $event) use ($durationOptions) {
|
||||
/** @var Timesheet|null $data */
|
||||
$data = $event->getData();
|
||||
if (null === $data || null === $data->getEnd()) {
|
||||
$event->getForm()->get('duration')->setData(null);
|
||||
}
|
||||
|
||||
if (null !== $data && !$this->security->isGranted('edit', $data)) {
|
||||
$event->getForm()->add('duration', DurationType::class, array_merge(['disabled' => true], $durationOptions));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// make sure that duration is mapped back to end field
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
$duration = $data->getDuration(false);
|
||||
try {
|
||||
if (null !== $duration) {
|
||||
$end = clone $data->getBegin();
|
||||
$end->modify('+ ' . $duration . ' seconds');
|
||||
$data->setEnd($end);
|
||||
} else {
|
||||
$data->setDuration(null);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$event->getForm()->addError(new FormError($e->getMessage()));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Timesheet::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'duration_minutes' => null,
|
||||
'duration_hours' => 10,
|
||||
]);
|
||||
}
|
||||
}
|
||||
180
src/Form/Type/QuickEntryWeekType.php
Normal file
180
src/Form/Type/QuickEntryWeekType.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Model\QuickEntryModel;
|
||||
use App\Validator\Constraints\QuickEntryTimesheet;
|
||||
use DateTime;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\All;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
|
||||
class QuickEntryWeekType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$projectOptions = [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'join_customer' => true,
|
||||
'query_builder_for_user' => true,
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true
|
||||
];
|
||||
|
||||
$builder->add('project', ProjectType::class, $projectOptions);
|
||||
|
||||
$projectFunction = function (FormEvent $event) use ($projectOptions) {
|
||||
/** @var QuickEntryModel|null $data */
|
||||
$data = $event->getData();
|
||||
if ($data === null || $data->getProject() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$begin = clone $data->getFirstEntry()->getBegin();
|
||||
$begin->setTime(0, 0, 0);
|
||||
$projectOptions['project_date_start'] = $begin;
|
||||
|
||||
$end = clone $data->getLatestEntry()->getBegin();
|
||||
$end->setTime(23, 59, 59);
|
||||
$projectOptions['project_date_end'] = $begin;
|
||||
$projectOptions['projects'] = [$data->getProject()];
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, $projectOptions);
|
||||
};
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, $projectFunction);
|
||||
|
||||
$activityOptions = [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'query_builder_for_user' => true,
|
||||
];
|
||||
|
||||
$builder->add('activity', ActivityType::class, $activityOptions);
|
||||
|
||||
$activityFunction = function (FormEvent $event) use ($activityOptions) {
|
||||
/** @var QuickEntryModel|null $data */
|
||||
$data = $event->getData();
|
||||
if ($data === null || $data->getActivity() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activityOptions['activities'] = [$data->getActivity()];
|
||||
$activityOptions['projects'] = [$data->getProject()];
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, $activityOptions);
|
||||
};
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, $activityFunction);
|
||||
|
||||
$builder->add('timesheets', CollectionType::class, [
|
||||
'entry_type' => QuickEntryTimesheetType::class,
|
||||
'label' => false,
|
||||
'entry_options' => [
|
||||
'label' => false,
|
||||
'compound' => true,
|
||||
'timezone' => $options['timezone'],
|
||||
'duration_minutes' => $options['duration_minutes'],
|
||||
'duration_hours' => $options['duration_hours'],
|
||||
],
|
||||
'allow_add' => true,
|
||||
'constraints' => [
|
||||
new Valid(),
|
||||
new All(['constraints' => [new QuickEntryTimesheet()]])
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
if ($event->getData() === null) {
|
||||
$event->setData(clone $options['prototype_data']);
|
||||
}
|
||||
});
|
||||
|
||||
$builder->addModelTransformer(new CallbackTransformer(
|
||||
function ($transformValue) use ($options) {
|
||||
/** @var QuickEntryModel|null $transformValue */
|
||||
if ($transformValue === null || $transformValue->isPrototype()) {
|
||||
return $transformValue;
|
||||
}
|
||||
|
||||
$project = $transformValue->getProject();
|
||||
$activity = $transformValue->getActivity();
|
||||
|
||||
// this case needs to be handled by the validator
|
||||
if ($project === null || $activity === null) {
|
||||
return $transformValue;
|
||||
}
|
||||
|
||||
foreach ($transformValue->getTimesheets() as $timesheet) {
|
||||
$timesheet->setUser($transformValue->getUser() ?? $options['user']);
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setActivity($activity);
|
||||
}
|
||||
|
||||
return $transformValue;
|
||||
},
|
||||
function ($reverseTransformValue) {
|
||||
return $reverseTransformValue;
|
||||
}
|
||||
));
|
||||
|
||||
// make sure that duration is mapped back to end field
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
/** @var QuickEntryModel $data */
|
||||
$data = $event->getData();
|
||||
$newRecords = $data->getNewTimesheet();
|
||||
|
||||
$user = $data->getUser();
|
||||
$project = $data->getProject();
|
||||
$activity = $data->getActivity();
|
||||
|
||||
foreach ($newRecords as $record) {
|
||||
if ($user !== null) {
|
||||
$record->setUser($user);
|
||||
}
|
||||
if ($project !== null) {
|
||||
$record->setProject($project);
|
||||
}
|
||||
if ($activity !== null) {
|
||||
$record->setActivity($activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => QuickEntryModel::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'duration_minutes' => null,
|
||||
'duration_hours' => 10,
|
||||
'start_date' => new DateTime(),
|
||||
'prototype_data' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
195
src/Model/QuickEntryModel.php
Normal file
195
src/Model/QuickEntryModel.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class QuickEntryModel
|
||||
{
|
||||
private $user;
|
||||
private $project;
|
||||
private $activity;
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*/
|
||||
private $timesheets = [];
|
||||
|
||||
public function __construct(?User $user = null, ?Project $project = null, ?Activity $activity = null)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->project = $project;
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
public function isPrototype(): bool
|
||||
{
|
||||
if ($this->hasExistingTimesheet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasNewTimesheet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getUser() === null && $this->getProject() === null && $this->getActivity() === null;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function setActivity(?Activity $activity): void
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
public function hasExistingTimesheet(): bool
|
||||
{
|
||||
foreach ($this->timesheets as $timesheet) {
|
||||
if ($timesheet->getId() !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getNewTimesheet(): array
|
||||
{
|
||||
$new = [];
|
||||
|
||||
foreach ($this->timesheets as $timesheet) {
|
||||
if ($timesheet->getId() === null && $timesheet->getDuration(false) !== null) {
|
||||
$new[] = $timesheet;
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function hasNewTimesheet(): bool
|
||||
{
|
||||
return \count($this->getNewTimesheet()) > 0;
|
||||
}
|
||||
|
||||
public function hasTimesheetWithDuration(): bool
|
||||
{
|
||||
foreach ($this->timesheets as $timesheet) {
|
||||
if ($timesheet->getDuration(false) !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets(): array
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
public function addTimesheet(Timesheet $timesheet): void
|
||||
{
|
||||
$this->timesheets[] = $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
*/
|
||||
public function setTimesheets(array $timesheets): void
|
||||
{
|
||||
$this->timesheets = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$this->addTimesheet($timesheet);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLatestEntry(): ?Timesheet
|
||||
{
|
||||
$latest = null;
|
||||
|
||||
foreach ($this->timesheets as $timesheet) {
|
||||
if ($timesheet->getBegin() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latest === null) {
|
||||
$latest = $timesheet;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latest->getBegin() < $timesheet->getBegin()) {
|
||||
$latest = $timesheet;
|
||||
}
|
||||
}
|
||||
|
||||
return $latest;
|
||||
}
|
||||
|
||||
public function getFirstEntry(): ?Timesheet
|
||||
{
|
||||
$first = null;
|
||||
|
||||
foreach ($this->timesheets as $timesheet) {
|
||||
if ($timesheet->getBegin() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($first === null) {
|
||||
$first = $timesheet;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($first->getBegin() > $timesheet->getBegin()) {
|
||||
$first = $timesheet;
|
||||
}
|
||||
}
|
||||
|
||||
return $first;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$records = $this->timesheets;
|
||||
$this->timesheets = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$this->timesheets[] = clone $record;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Model/QuickEntryWeek.php
Normal file
50
src/Model/QuickEntryWeek.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class QuickEntryWeek
|
||||
{
|
||||
private $date;
|
||||
private $rows;
|
||||
|
||||
/**
|
||||
* @param \DateTime $startDate
|
||||
* @param QuickEntryModel[] $rows
|
||||
*/
|
||||
public function __construct(\DateTime $startDate, array $rows)
|
||||
{
|
||||
$this->date = $startDate;
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
public function getDate(): \DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QuickEntryModel[]
|
||||
*/
|
||||
public function getRows(): array
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QuickEntryModel[] $rows
|
||||
*/
|
||||
public function setRows(array $rows): void
|
||||
{
|
||||
$this->rows = $rows;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
@@ -160,16 +161,26 @@ class ActivityRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false)
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): void
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams, $globalsOnly);
|
||||
if ($permissions->count() > 0) {
|
||||
$qb->andWhere($permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): Andx
|
||||
{
|
||||
$andX = $qb->expr()->andX();
|
||||
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
// make sure that admins see all activities
|
||||
if (null !== $user && $user->canSeeAllData()) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
@@ -177,33 +188,33 @@ class ActivityRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere('SIZE(a.teams) = 0');
|
||||
$andX->add('SIZE(a.teams) = 0');
|
||||
if (!$globalsOnly) {
|
||||
$qb->andWhere('SIZE(p.teams) = 0');
|
||||
$qb->andWhere('SIZE(c.teams) = 0');
|
||||
$andX->add('SIZE(p.teams) = 0');
|
||||
$andX->add('SIZE(c.teams) = 0');
|
||||
}
|
||||
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
$orActivity = $qb->expr()->orX(
|
||||
'SIZE(a.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'a.teams')
|
||||
);
|
||||
$qb->andWhere($orActivity);
|
||||
$andX->add($orActivity);
|
||||
|
||||
if (!$globalsOnly) {
|
||||
$orProject = $qb->expr()->orX(
|
||||
'SIZE(p.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||
);
|
||||
$qb->andWhere($orProject);
|
||||
$andX->add($orProject);
|
||||
|
||||
$orCustomer = $qb->expr()->orX(
|
||||
'SIZE(c.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($orCustomer);
|
||||
$andX->add($orCustomer);
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique(array_map(function (Team $team) {
|
||||
@@ -211,6 +222,8 @@ class ActivityRepository extends EntityRepository
|
||||
}, $teams)));
|
||||
|
||||
$qb->setParameter('teams', $ids);
|
||||
|
||||
return $andX;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,9 +255,9 @@ class ActivityRepository extends EntityRepository
|
||||
->addOrderBy('a.name', 'ASC')
|
||||
;
|
||||
|
||||
$where = $qb->expr()->andX();
|
||||
$mainQuery = $qb->expr()->andX();
|
||||
|
||||
$where->add($qb->expr()->eq('a.visible', ':visible'));
|
||||
$mainQuery->add($qb->expr()->eq('a.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
|
||||
|
||||
if (!$query->isGlobalsOnly()) {
|
||||
@@ -254,7 +267,7 @@ class ActivityRepository extends EntityRepository
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c');
|
||||
|
||||
$where->add(
|
||||
$mainQuery->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('a.project'),
|
||||
$qb->expr()->andX(
|
||||
@@ -268,9 +281,9 @@ class ActivityRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if ($query->isGlobalsOnly()) {
|
||||
$where->add($qb->expr()->isNull('a.project'));
|
||||
$mainQuery->add($qb->expr()->isNull('a.project'));
|
||||
} elseif ($query->hasProjects()) {
|
||||
$where->add(
|
||||
$mainQuery->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('a.project'),
|
||||
$qb->expr()->in('a.project', ':project')
|
||||
@@ -279,28 +292,30 @@ class ActivityRepository extends EntityRepository
|
||||
$qb->setParameter('project', $query->getProjects());
|
||||
}
|
||||
|
||||
if (null !== $query->getActivityToIgnore()) {
|
||||
$qb->andWhere($qb->expr()->neq('a.id', ':ignored'));
|
||||
$qb->setParameter('ignored', $query->getActivityToIgnore());
|
||||
$permissions = $this->getPermissionCriteria($qb, $query->getUser(), $query->getTeams(), $query->isGlobalsOnly());
|
||||
if ($permissions->count() > 0) {
|
||||
$mainQuery->add($permissions);
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams(), $query->isGlobalsOnly());
|
||||
$outerQuery = $qb->expr()->orX();
|
||||
|
||||
$or = $qb->expr()->orX();
|
||||
|
||||
// this must always be the last part before the or
|
||||
$or->add($where);
|
||||
|
||||
// this must always be the last part of the query
|
||||
if ($query->hasActivities()) {
|
||||
$or->add($qb->expr()->in('a.id', ':activity'));
|
||||
$outerQuery->add($qb->expr()->in('a.id', ':activity'));
|
||||
$qb->setParameter('activity', $query->getActivities());
|
||||
}
|
||||
|
||||
if ($or->count() > 0) {
|
||||
$qb->andWhere($or);
|
||||
if (null !== $query->getActivityToIgnore()) {
|
||||
$mainQuery = $qb->expr()->andX(
|
||||
$mainQuery,
|
||||
$qb->expr()->neq('a.id', ':ignored')
|
||||
);
|
||||
$qb->setParameter('ignored', $query->getActivityToIgnore());
|
||||
}
|
||||
|
||||
$outerQuery->add($mainQuery);
|
||||
|
||||
$qb->andWhere($outerQuery);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
@@ -157,14 +158,24 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
|
||||
if ($permissions->count() > 0) {
|
||||
$qb->andWhere($permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
|
||||
{
|
||||
$andX = $qb->expr()->andX();
|
||||
|
||||
// make sure that all queries without a user see all customers
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
// make sure that admins see all customers
|
||||
if (null !== $user && $user->canSeeAllData()) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
@@ -172,22 +183,24 @@ class CustomerRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere('SIZE(c.teams) = 0');
|
||||
$andX->add('SIZE(c.teams) = 0');
|
||||
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
$or = $qb->expr()->orX(
|
||||
'SIZE(c.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($or);
|
||||
$andX->add($or);
|
||||
|
||||
$ids = array_values(array_unique(array_map(function (Team $team) {
|
||||
return $team->getId();
|
||||
}, $teams)));
|
||||
|
||||
$qb->setParameter('teams', $ids);
|
||||
|
||||
return $andX;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,21 +229,33 @@ class CustomerRepository extends EntityRepository
|
||||
->from(Customer::class, 'c')
|
||||
->orderBy('c.name', 'ASC');
|
||||
|
||||
// TODO this where and the next if($query->hasCustomers()) should go into their own $qb->expr()->orX()
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
|
||||
$mainQuery = $qb->expr()->andX();
|
||||
|
||||
$mainQuery->add($qb->expr()->eq('c.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
|
||||
|
||||
$permissions = $this->getPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
if ($permissions->count() > 0) {
|
||||
$mainQuery->add($permissions);
|
||||
}
|
||||
|
||||
$outerQuery = $qb->expr()->orX();
|
||||
|
||||
if ($query->hasCustomers()) {
|
||||
$qb->orWhere($qb->expr()->in('c.id', ':customer'))
|
||||
->setParameter('customer', $query->getCustomers());
|
||||
$outerQuery->add($qb->expr()->in('c.id', ':customer'));
|
||||
$qb->setParameter('customer', $query->getCustomers());
|
||||
}
|
||||
|
||||
if (null !== $query->getCustomerToIgnore()) {
|
||||
$qb->andWhere($qb->expr()->neq('c.id', ':ignored'));
|
||||
$mainQuery = $qb->expr()->andX(
|
||||
$mainQuery,
|
||||
$qb->expr()->neq('c.id', ':ignored')
|
||||
);
|
||||
$qb->setParameter('ignored', $query->getCustomerToIgnore());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
$outerQuery->add($mainQuery);
|
||||
$qb->andWhere($outerQuery);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
@@ -182,16 +183,26 @@ class ProjectRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
|
||||
if ($permissions->count() > 0) {
|
||||
$qb->andWhere($permissions);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
|
||||
{
|
||||
$andX = $qb->expr()->andX();
|
||||
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
// make sure that admins see all projects
|
||||
if (null !== $user && $user->canSeeAllData()) {
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
@@ -199,29 +210,31 @@ class ProjectRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if (empty($teams)) {
|
||||
$qb->andWhere('SIZE(c.teams) = 0');
|
||||
$qb->andWhere('SIZE(p.teams) = 0');
|
||||
$andX->add('SIZE(c.teams) = 0');
|
||||
$andX->add('SIZE(p.teams) = 0');
|
||||
|
||||
return;
|
||||
return $andX;
|
||||
}
|
||||
|
||||
$orProject = $qb->expr()->orX(
|
||||
'SIZE(p.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||
);
|
||||
$qb->andWhere($orProject);
|
||||
$andX->add($orProject);
|
||||
|
||||
$orCustomer = $qb->expr()->orX(
|
||||
'SIZE(c.teams) = 0',
|
||||
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||
);
|
||||
$qb->andWhere($orCustomer);
|
||||
$andX->add($orCustomer);
|
||||
|
||||
$ids = array_values(array_unique(array_map(function (Team $team) {
|
||||
return $team->getId();
|
||||
}, $teams)));
|
||||
|
||||
$qb->setParameter('teams', $ids);
|
||||
|
||||
return $andX;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,57 +272,46 @@ class ProjectRepository extends EntityRepository
|
||||
$qb->addSelect('c');
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
|
||||
if (!$query->isIgnoreDate()) {
|
||||
$now = new DateTime();
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':start'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':start'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
)
|
||||
)
|
||||
)->setParameter('start', $now);
|
||||
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':end'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':end'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
)
|
||||
)
|
||||
)->setParameter('end', $now);
|
||||
}
|
||||
$mainQuery = $qb->expr()->andX();
|
||||
|
||||
$mainQuery->add($qb->expr()->eq('p.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
|
||||
|
||||
$mainQuery->add($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
$qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
|
||||
|
||||
if ($query->hasProjects()) {
|
||||
$qb->orWhere($qb->expr()->in('p.id', ':project'))
|
||||
->setParameter('project', $query->getProjects());
|
||||
if (!$query->isIgnoreDate()) {
|
||||
$andx = $this->addProjectStartAndEndDate($qb, $query->getProjectStart(), $query->getProjectEnd());
|
||||
$mainQuery->add($andx);
|
||||
}
|
||||
|
||||
if ($query->hasCustomers()) {
|
||||
$qb->andWhere($qb->expr()->in('p.customer', ':customer'))
|
||||
->setParameter('customer', $query->getCustomers());
|
||||
$mainQuery->add($qb->expr()->in('p.customer', ':customer'));
|
||||
$qb->setParameter('customer', $query->getCustomers());
|
||||
}
|
||||
|
||||
$permissions = $this->getPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
if ($permissions->count() > 0) {
|
||||
$mainQuery->add($permissions);
|
||||
}
|
||||
|
||||
$outerQuery = $qb->expr()->orX();
|
||||
|
||||
if ($query->hasProjects()) {
|
||||
$outerQuery->add($qb->expr()->in('p.id', ':project'));
|
||||
$qb->setParameter('project', $query->getProjects());
|
||||
}
|
||||
|
||||
if (null !== $query->getProjectToIgnore()) {
|
||||
$qb->andWhere($qb->expr()->neq('p.id', ':ignored'));
|
||||
$mainQuery = $qb->expr()->andX(
|
||||
$mainQuery,
|
||||
$qb->expr()->neq('p.id', ':ignored')
|
||||
);
|
||||
$qb->setParameter('ignored', $query->getProjectToIgnore());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
$outerQuery->add($mainQuery);
|
||||
$qb->andWhere($outerQuery);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
@@ -369,39 +371,8 @@ class ProjectRepository extends EntityRepository
|
||||
// begin < to and end = null
|
||||
// begin > from and end < to
|
||||
// ... and more ...
|
||||
|
||||
$begin = $query->getProjectStart();
|
||||
$end = $query->getProjectEnd();
|
||||
|
||||
if (null !== $begin) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':start'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':start'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
)
|
||||
)
|
||||
)->setParameter('start', $query->getProjectStart());
|
||||
}
|
||||
|
||||
if (null !== $end) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':end'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':end'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
)
|
||||
)
|
||||
)->setParameter('end', $query->getProjectEnd());
|
||||
}
|
||||
$times = $this->addProjectStartAndEndDate($qb, $query->getProjectStart(), $query->getProjectEnd());
|
||||
$qb->andWhere($times);
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
@@ -440,6 +411,45 @@ class ProjectRepository extends EntityRepository
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addProjectStartAndEndDate(QueryBuilder $qb, ?DateTime $begin, ?DateTime $end): Andx
|
||||
{
|
||||
$and = $qb->expr()->andX();
|
||||
|
||||
if (null !== $begin) {
|
||||
$and->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':start'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':start'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
)
|
||||
)
|
||||
);
|
||||
$qb->setParameter('start', $begin);
|
||||
}
|
||||
|
||||
if (null !== $end) {
|
||||
$and->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':end'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':end'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
)
|
||||
)
|
||||
);
|
||||
$qb->setParameter('end', $end);
|
||||
}
|
||||
|
||||
return $and;
|
||||
}
|
||||
|
||||
public function countProjectsForQuery(ProjectQuery $query): int
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
@@ -14,6 +14,14 @@ use App\Entity\Project;
|
||||
|
||||
final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
{
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
private $projectStart;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
private $projectEnd;
|
||||
/**
|
||||
* @var Project|null
|
||||
*/
|
||||
@@ -22,8 +30,8 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
private $withCustomer = false;
|
||||
|
||||
/**
|
||||
* @param Project|int|null $project
|
||||
* @param Customer|int|null $customer
|
||||
* @param Project|int|null|array<int>|array<Project> $project
|
||||
* @param Customer|int|null|array<int>|array<Customer> $customer
|
||||
*/
|
||||
public function __construct($project = null, $customer = null)
|
||||
{
|
||||
@@ -40,13 +48,25 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
}
|
||||
$this->setCustomers($customer);
|
||||
}
|
||||
|
||||
$this->projectStart = $this->projectEnd = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether customers should be joined
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function withCustomer(): bool
|
||||
{
|
||||
return $this->withCustomer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly join the customer
|
||||
*
|
||||
* @param bool $withCustomer
|
||||
*/
|
||||
public function setWithCustomer(bool $withCustomer): void
|
||||
{
|
||||
$this->withCustomer = $withCustomer;
|
||||
@@ -60,11 +80,9 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
return $this->projectToIgnore;
|
||||
}
|
||||
|
||||
public function setProjectToIgnore(Project $projectToIgnore): ProjectFormTypeQuery
|
||||
public function setProjectToIgnore(Project $projectToIgnore): void
|
||||
{
|
||||
$this->projectToIgnore = $projectToIgnore;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isIgnoreDate(): bool
|
||||
@@ -72,10 +90,28 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
return $this->ignoreDate;
|
||||
}
|
||||
|
||||
public function setIgnoreDate(bool $ignoreDate): ProjectFormTypeQuery
|
||||
public function setIgnoreDate(bool $ignoreDate): void
|
||||
{
|
||||
$this->ignoreDate = $ignoreDate;
|
||||
}
|
||||
|
||||
return $this;
|
||||
public function getProjectStart(): ?\DateTime
|
||||
{
|
||||
return $this->projectStart;
|
||||
}
|
||||
|
||||
public function setProjectStart(?\DateTime $projectStart): void
|
||||
{
|
||||
$this->projectStart = $projectStart;
|
||||
}
|
||||
|
||||
public function getProjectEnd(): ?\DateTime
|
||||
{
|
||||
return $this->projectEnd;
|
||||
}
|
||||
|
||||
public function setProjectEnd(?\DateTime $projectEnd): void
|
||||
{
|
||||
$this->projectEnd = $projectEnd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,7 +1052,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @param User|null $user
|
||||
* @param DateTime|null $startFrom
|
||||
* @param int $limit
|
||||
* @return array|mixed
|
||||
* @return Timesheet[]
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function getRecentActivities(User $user = null, DateTime $startFrom = null, int $limit = 10)
|
||||
|
||||
@@ -99,6 +99,7 @@ final class IconExtension extends AbstractExtension
|
||||
'users' => 'fas fa-user-friends',
|
||||
'visibility' => 'far fa-eye',
|
||||
'warning' => 'fas fa-exclamation-triangle',
|
||||
'weekly-times' => 'fas fa-th',
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
];
|
||||
|
||||
|
||||
@@ -80,6 +80,14 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
|
||||
return ($day === 0 || $day === 6);
|
||||
}),
|
||||
new TwigTest('today', function ($dateTime) {
|
||||
if (!$dateTime instanceof \DateTime) {
|
||||
return false;
|
||||
}
|
||||
$compare = new \DateTime('now', $dateTime->getTimezone());
|
||||
|
||||
return $compare->format('Y-m-d') === $dateTime->format('Y-m-d');
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProjectValidator extends ConstraintValidator
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof ProjectConstraint)) {
|
||||
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\Project');
|
||||
throw new UnexpectedTypeException($constraint, ProjectConstraint::class);
|
||||
}
|
||||
|
||||
if (!\is_object($value) || !($value instanceof Project)) {
|
||||
|
||||
25
src/Validator/Constraints/QuickEntryModel.php
Normal file
25
src/Validator/Constraints/QuickEntryModel.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS"})
|
||||
*/
|
||||
class QuickEntryModel extends Constraint
|
||||
{
|
||||
public const ACTIVITY_REQUIRED = 'quick-entry-model-01';
|
||||
public const PROJECT_REQUIRED = 'quick-entry-model-02';
|
||||
|
||||
public $messageActivityRequired = 'An activity needs to be selected.';
|
||||
public $messageProjectRequired = 'A project needs to be selected.';
|
||||
}
|
||||
56
src/Validator/Constraints/QuickEntryModelValidator.php
Normal file
56
src/Validator/Constraints/QuickEntryModelValidator.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Model\QuickEntryModel;
|
||||
use App\Validator\Constraints\QuickEntryModel as QuickEntryModelConstraint;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class QuickEntryModelValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!$constraint instanceof QuickEntryModelConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, QuickEntryModelConstraint::class);
|
||||
}
|
||||
|
||||
if (!\is_object($value) || !($value instanceof QuickEntryModel)) {
|
||||
throw new UnexpectedTypeException($value, QuickEntryModel::class);
|
||||
}
|
||||
|
||||
/** @var QuickEntryModel $model */
|
||||
$model = $value;
|
||||
|
||||
if ($model->isPrototype()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($model->hasExistingTimesheet() || $model->hasNewTimesheet()) {
|
||||
if ($model->getActivity() === null) {
|
||||
$this->context->buildViolation($constraint->messageActivityRequired)
|
||||
->atPath('activity')
|
||||
->setCode(QuickEntryModelConstraint::ACTIVITY_REQUIRED)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($model->getProject() === null) {
|
||||
$this->context->buildViolation($constraint->messageProjectRequired)
|
||||
->atPath('project')
|
||||
->setCode(QuickEntryModelConstraint::PROJECT_REQUIRED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Validator/Constraints/QuickEntryTimesheet.php
Normal file
20
src/Validator/Constraints/QuickEntryTimesheet.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS"})
|
||||
*/
|
||||
class QuickEntryTimesheet extends Constraint
|
||||
{
|
||||
}
|
||||
61
src/Validator/Constraints/QuickEntryTimesheetValidator.php
Normal file
61
src/Validator/Constraints/QuickEntryTimesheetValidator.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Validator\Constraints\QuickEntryTimesheet as QuickEntryTimesheetConstraint;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class QuickEntryTimesheetValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
private $constraints;
|
||||
|
||||
/**
|
||||
* @param Constraint[] $constraints
|
||||
*/
|
||||
public function __construct(iterable $constraints)
|
||||
{
|
||||
$this->constraints = $constraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!$constraint instanceof QuickEntryTimesheetConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, QuickEntryTimesheetConstraint::class);
|
||||
}
|
||||
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
/** @var TimesheetEntity $timesheet */
|
||||
$timesheet = $value;
|
||||
|
||||
if ($timesheet->getId() === null && $timesheet->getDuration(false) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->constraints as $constraint) {
|
||||
$this->context
|
||||
->getValidator()
|
||||
->inContext($this->context)
|
||||
->atPath('duration')
|
||||
->validate($timesheet, $constraint, [Constraint::DEFAULT_GROUP]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,22 +18,32 @@ use Symfony\Component\Validator\Constraint;
|
||||
*/
|
||||
class Timesheet extends Constraint
|
||||
{
|
||||
public const MISSING_BEGIN_ERROR = 'kimai-timesheet-81';
|
||||
public const END_BEFORE_BEGIN_ERROR = 'kimai-timesheet-82';
|
||||
public const MISSING_ACTIVITY_ERROR = 'kimai-timesheet-84';
|
||||
public const MISSING_PROJECT_ERROR = 'kimai-timesheet-85';
|
||||
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'kimai-timesheet-86';
|
||||
public const DISABLED_ACTIVITY_ERROR = 'kimai-timesheet-87';
|
||||
public const DISABLED_PROJECT_ERROR = 'kimai-timesheet-88';
|
||||
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
|
||||
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
|
||||
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_BEGIN_ERROR instead */
|
||||
public const MISSING_BEGIN_ERROR = TimesheetBasic::MISSING_BEGIN_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::END_BEFORE_BEGIN_ERROR instead */
|
||||
public const END_BEFORE_BEGIN_ERROR = TimesheetBasic::END_BEFORE_BEGIN_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_ACTIVITY_ERROR instead */
|
||||
public const MISSING_ACTIVITY_ERROR = TimesheetBasic::MISSING_ACTIVITY_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_PROJECT_ERROR instead */
|
||||
public const MISSING_PROJECT_ERROR = TimesheetBasic::MISSING_PROJECT_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR instead */
|
||||
public const ACTIVITY_PROJECT_MISMATCH_ERROR = TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_ACTIVITY_ERROR instead */
|
||||
public const DISABLED_ACTIVITY_ERROR = TimesheetBasic::DISABLED_ACTIVITY_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_PROJECT_ERROR instead */
|
||||
public const DISABLED_PROJECT_ERROR = TimesheetBasic::DISABLED_PROJECT_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_CUSTOMER_ERROR instead */
|
||||
public const DISABLED_CUSTOMER_ERROR = TimesheetBasic::DISABLED_CUSTOMER_ERROR;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::PROJECT_NOT_STARTED instead */
|
||||
public const PROJECT_NOT_STARTED = TimesheetBasic::PROJECT_NOT_STARTED;
|
||||
/** @deprecated since 1.15.3 - use TimesheetBasic::PROJECT_ALREADY_ENDED instead */
|
||||
public const PROJECT_ALREADY_ENDED = TimesheetBasic::PROJECT_ALREADY_ENDED;
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
||||
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
|
||||
self::MISSING_ACTIVITY_ERROR => 'A timesheet must have an activity.',
|
||||
self::MISSING_PROJECT_ERROR => 'A timesheet must have a project.',
|
||||
self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.',
|
||||
self::MISSING_PROJECT_ERROR => 'A project needs to be selected.',
|
||||
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch, project specific activity and timesheet project are different.',
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
|
||||
50
src/Validator/Constraints/TimesheetBasic.php
Normal file
50
src/Validator/Constraints/TimesheetBasic.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
|
||||
*/
|
||||
class TimesheetBasic extends TimesheetConstraint
|
||||
{
|
||||
public const MISSING_BEGIN_ERROR = 'kimai-timesheet-81';
|
||||
public const END_BEFORE_BEGIN_ERROR = 'kimai-timesheet-82';
|
||||
public const MISSING_ACTIVITY_ERROR = 'kimai-timesheet-84';
|
||||
public const MISSING_PROJECT_ERROR = 'kimai-timesheet-85';
|
||||
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'kimai-timesheet-86';
|
||||
public const DISABLED_ACTIVITY_ERROR = 'kimai-timesheet-87';
|
||||
public const DISABLED_PROJECT_ERROR = 'kimai-timesheet-88';
|
||||
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
|
||||
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
|
||||
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
||||
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
|
||||
self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.',
|
||||
self::MISSING_PROJECT_ERROR => 'A project needs to be selected.',
|
||||
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch, project specific activity and timesheet project are different.',
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
||||
self::PROJECT_NOT_STARTED => 'The project has not started at that time.',
|
||||
self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.',
|
||||
];
|
||||
|
||||
public $message = 'This timesheet has invalid settings.';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
172
src/Validator/Constraints/TimesheetBasicValidator.php
Normal file
172
src/Validator/Constraints/TimesheetBasicValidator.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetBasicValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($timesheet, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof TimesheetBasic)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetBasic::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$this->validateBeginAndEnd($timesheet, $this->context);
|
||||
$this->validateActivityAndProject($timesheet, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
if (null === $begin) {
|
||||
$context->buildViolation('You must submit a begin date.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::MISSING_BEGIN_ERROR)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $end && $begin > $end) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::END_BEFORE_BEGIN_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === ($activity = $timesheet->getActivity())) {
|
||||
$context->buildViolation('An activity needs to be selected.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::MISSING_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === ($project = $timesheet->getProject())) {
|
||||
$context->buildViolation('A project needs to be selected.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::MISSING_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $activity || null === $project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $activity->getProject() && $activity->getProject() !== $project) {
|
||||
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
$timesheetEnd = $timesheet->getEnd();
|
||||
$newOrStarted = null === $timesheetEnd || $timesheet->getId() === null;
|
||||
|
||||
if ($newOrStarted && !$activity->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::DISABLED_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($newOrStarted && !$project->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::DISABLED_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($newOrStarted && !$project->getCustomer()->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::DISABLED_CUSTOMER_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
$pathStart = 'begin';
|
||||
$pathEnd = 'end';
|
||||
|
||||
$projectBegin = $project->getStart();
|
||||
$projectEnd = $project->getEnd();
|
||||
|
||||
if (null === $projectBegin && null === $projectEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timesheetStart = $timesheet->getBegin();
|
||||
$timesheetEnd = $timesheet->getEnd();
|
||||
|
||||
if (null !== $timesheetStart && $pathStart !== null) {
|
||||
if (null !== $projectBegin && $timesheetStart->getTimestamp() < $projectBegin->getTimestamp()) {
|
||||
$context->buildViolation('The project has not started at that time.')
|
||||
->atPath($pathStart)
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::PROJECT_NOT_STARTED)
|
||||
->addViolation();
|
||||
} elseif (null !== $projectEnd && $timesheetStart->getTimestamp() > $projectEnd->getTimestamp()) {
|
||||
$context->buildViolation('The project is finished at that time.')
|
||||
->atPath($pathStart)
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::PROJECT_ALREADY_ENDED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $timesheetEnd && $pathEnd !== null) {
|
||||
if (null !== $projectEnd && $timesheetEnd->getTimestamp() > $projectEnd->getTimestamp()) {
|
||||
$context->buildViolation('The project is finished at that time.')
|
||||
->atPath($pathEnd)
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::PROJECT_ALREADY_ENDED)
|
||||
->addViolation();
|
||||
} elseif (null !== $projectBegin && $timesheetEnd->getTimestamp() < $projectBegin->getTimestamp()) {
|
||||
$context->buildViolation('The project has not started at that time.')
|
||||
->atPath($pathEnd)
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetBasic::PROJECT_NOT_STARTED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,11 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
$duration = $timesheet->getEnd()->getTimestamp() - $timesheet->getBegin()->getTimestamp();
|
||||
}
|
||||
|
||||
// this validator needs a project to calculate the rates
|
||||
if ($timesheet->getProject() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timeRate = $this->rateService->calculate($timesheet);
|
||||
$rate = $timeRate->getRate();
|
||||
|
||||
|
||||
30
src/Validator/Constraints/TimesheetExported.php
Normal file
30
src/Validator/Constraints/TimesheetExported.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
final class TimesheetExported extends TimesheetConstraint
|
||||
{
|
||||
public const TIMESHEET_EXPORTED = 'kimai-timesheet-exported-01';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::TIMESHEET_EXPORTED => 'This timesheet is already exported.',
|
||||
];
|
||||
|
||||
public $message = 'This timesheet is already exported.';
|
||||
/**
|
||||
* @var \DateTime|string|null
|
||||
*/
|
||||
public $now;
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
55
src/Validator/Constraints/TimesheetExportedValidator.php
Normal file
55
src/Validator/Constraints/TimesheetExportedValidator.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetExportedValidator extends ConstraintValidator
|
||||
{
|
||||
private $security;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($timesheet, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof TimesheetExported)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetExported::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if (!$timesheet->isExported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('edit_exported_timesheet')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->context->buildViolation(TimesheetExported::getErrorName(TimesheetExported::TIMESHEET_EXPORTED))
|
||||
->atPath('exported')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetExported::TIMESHEET_EXPORTED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,15 @@ namespace App\Validator\Constraints;
|
||||
final class TimesheetLongRunning extends TimesheetConstraint
|
||||
{
|
||||
public const LONG_RUNNING = 'kimai-timesheet-long-running-01';
|
||||
public const MAXIMUM = 'kimai-timesheet-long-running-02';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::LONG_RUNNING => 'TIMESHEET_LONG_RUNNING',
|
||||
self::MAXIMUM => 'MAXIMUM',
|
||||
];
|
||||
|
||||
public $message = 'Maximum duration of {{ value }} hours exceeded.';
|
||||
public $maximumMessage = 'Maximum duration exceeded.';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
|
||||
@@ -42,6 +42,18 @@ final class TimesheetLongRunningValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
// one year is currently the maximum that can be logged (which is already not logically)
|
||||
// the database column could hold more data, but let's limit it here
|
||||
if ($timesheet->getDuration() > 31536000) {
|
||||
$this->context->buildViolation($constraint->maximumMessage)
|
||||
->setTranslationDomain('validators')
|
||||
->atPath('duration')
|
||||
->setCode(TimesheetLongRunning::MAXIMUM)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
|
||||
|
||||
if ($maxMinutes <= 0) {
|
||||
|
||||
@@ -28,7 +28,7 @@ class TimesheetMultiUpdate extends Constraint
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_ACTIVITY_ERROR => 'You need to choose an activity, if the project should be changed.',
|
||||
self::MISSING_PROJECT_ERROR => 'A timesheet must have a project.',
|
||||
self::MISSING_PROJECT_ERROR => 'A project needs to be selected.',
|
||||
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch: chosen project does not match the activity project.',
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
|
||||
@@ -13,7 +13,6 @@ use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetValidator extends ConstraintValidator
|
||||
@@ -45,10 +44,6 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$this->validateBeginAndEnd($timesheet, $this->context);
|
||||
$this->validateActivityAndProject($timesheet, $this->context);
|
||||
$this->validateActiveLimit($timesheet, $this->context);
|
||||
|
||||
foreach ($this->constraints as $constraint) {
|
||||
$this->context
|
||||
->getValidator()
|
||||
@@ -56,142 +51,4 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
->validate($timesheet, $constraint, [Constraint::DEFAULT_GROUP]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActiveLimit(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
// TODO check active entries against hard_limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
if (null === $begin) {
|
||||
$context->buildViolation('You must submit a begin date.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::MISSING_BEGIN_ERROR)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $end && $begin > $end) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::END_BEFORE_BEGIN_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === ($activity = $timesheet->getActivity())) {
|
||||
$context->buildViolation('A timesheet must have an activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === ($project = $timesheet->getProject())) {
|
||||
$context->buildViolation('A timesheet must have a project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $activity || null === $project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $activity->getProject() && $activity->getProject() !== $project) {
|
||||
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
$timesheetEnd = $timesheet->getEnd();
|
||||
|
||||
if (null === $timesheetEnd && !$activity->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $timesheetEnd && !$project->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $timesheetEnd && !$project->getCustomer()->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_CUSTOMER_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
$projectBegin = $project->getStart();
|
||||
$projectEnd = $project->getEnd();
|
||||
|
||||
if (null !== $projectBegin || null !== $projectEnd) {
|
||||
$timesheetStart = $timesheet->getBegin();
|
||||
$timesheetEnd = $timesheet->getEnd();
|
||||
|
||||
if (null !== $timesheetStart) {
|
||||
if (null !== $projectBegin && $timesheetStart->getTimestamp() < $projectBegin->getTimestamp()) {
|
||||
$context->buildViolation('The project has not started at that time.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::PROJECT_NOT_STARTED)
|
||||
->addViolation();
|
||||
} elseif (null !== $projectEnd && $timesheetStart->getTimestamp() > $projectEnd->getTimestamp()) {
|
||||
$context->buildViolation('The project is finished at that time.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::PROJECT_ALREADY_ENDED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $timesheetEnd) {
|
||||
if (null !== $projectEnd && $timesheetEnd->getTimestamp() > $projectEnd->getTimestamp()) {
|
||||
$context->buildViolation('The project is finished at that time.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::PROJECT_ALREADY_ENDED)
|
||||
->addViolation();
|
||||
} elseif (null !== $projectBegin && $timesheetEnd->getTimestamp() < $projectBegin->getTimestamp()) {
|
||||
$context->buildViolation('The project has not started at that time.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::PROJECT_NOT_STARTED)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user