support custom fields for timesheets, customers, projects and activities (#871)
This commit is contained in:
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Form\API\ActivityApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -37,20 +39,20 @@ class ActivityController extends BaseApiController
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ActivityRepository $repository
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,12 +135,18 @@ class ActivityController extends BaseApiController
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
/** @var Activity $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// make sure the fields are properly setup and we know, which meta fields
|
||||
// should be exposed and which not
|
||||
$event = new ActivityMetaDefinitionEvent($data);
|
||||
$this->dispatcher->dispatch(ActivityMetaDefinitionEvent::class, $event);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
@@ -177,9 +185,7 @@ class ActivityController extends BaseApiController
|
||||
|
||||
$activity = new Activity();
|
||||
|
||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
@@ -241,9 +247,7 @@ class ActivityController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update activity');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||
|
||||
$form->setData($activity);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Form\API\CustomerApiEditForm;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -37,20 +39,20 @@ class CustomerController extends BaseApiController
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param CustomerRepository $repository
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,12 +113,18 @@ class CustomerController extends BaseApiController
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
/** @var Customer $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// make sure the fields are properly setup and we know, which meta fields
|
||||
// should be exposed and which not
|
||||
$event = new CustomerMetaDefinitionEvent($data);
|
||||
$this->dispatcher->dispatch(CustomerMetaDefinitionEvent::class, $event);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
@@ -155,9 +163,7 @@ class CustomerController extends BaseApiController
|
||||
|
||||
$customer = new Customer();
|
||||
|
||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
@@ -219,9 +225,7 @@ class CustomerController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update customer');
|
||||
}
|
||||
|
||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||
|
||||
$form->setData($customer);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
@@ -12,7 +12,8 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\API\ProjectApiEditForm;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -37,20 +39,20 @@ class ProjectController extends BaseApiController
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ProjectRepository $repository
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,10 +119,18 @@ class ProjectController extends BaseApiController
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
/** @var Project $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// make sure the fields are properly setup and we know, which meta fields
|
||||
// should be exposed and which not
|
||||
$event = new ProjectMetaDefinitionEvent($data);
|
||||
$this->dispatcher->dispatch(ProjectMetaDefinitionEvent::class, $event);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
@@ -159,9 +169,7 @@ class ProjectController extends BaseApiController
|
||||
|
||||
$project = new Project();
|
||||
|
||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
@@ -223,9 +231,7 @@ class ProjectController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update project');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||
|
||||
$form->setData($project);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace App\API;
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\API\TimesheetApiEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -234,17 +234,18 @@ class TimesheetController extends BaseApiController
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
/** @var Timesheet $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $timesheet) {
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('view', $timesheet)) {
|
||||
if (!$this->isGranted('view', $data)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to view this timesheet');
|
||||
}
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
@@ -284,13 +285,11 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||
'allow_duration' => false,
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
]);
|
||||
|
||||
@@ -366,13 +365,11 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||
'allow_end_datetime' => $mode->canUpdateTimesWithAPI(),
|
||||
'allow_duration' => false,
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
]);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
@@ -19,7 +20,11 @@ use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -34,10 +39,15 @@ class ActivityController extends AbstractController
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository)
|
||||
public function __construct(ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getRepository(): ActivityRepository
|
||||
@@ -52,7 +62,7 @@ class ActivityController extends AbstractController
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -88,7 +98,7 @@ class ActivityController extends AbstractController
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Project|null $project
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request, ?Project $project = null)
|
||||
{
|
||||
@@ -105,7 +115,7 @@ class ActivityController extends AbstractController
|
||||
* @Security("is_granted('budget', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Activity $activity)
|
||||
{
|
||||
@@ -121,7 +131,7 @@ class ActivityController extends AbstractController
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Activity $activity, Request $request)
|
||||
{
|
||||
@@ -134,7 +144,7 @@ class ActivityController extends AbstractController
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
@@ -193,29 +203,32 @@ class ActivityController extends AbstractController
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
protected function renderActivityForm(Activity $activity, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($activity);
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch(ActivityMetaDefinitionEvent::class, $event);
|
||||
|
||||
$editForm = $this->createEditForm($activity);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->saveActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newActivity = new Activity();
|
||||
$newActivity->setProject($activity->getProject());
|
||||
$editForm = $this->createEditForm($newActivity);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$activity = $newActivity;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newActivity = new Activity();
|
||||
$newActivity->setProject($activity->getProject());
|
||||
$editForm = $this->createEditForm($newActivity);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$activity = $newActivity;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +243,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ActivityQuery $query)
|
||||
{
|
||||
@@ -244,7 +257,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
@@ -19,7 +20,11 @@ use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -38,15 +43,20 @@ class CustomerController extends AbstractController
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* @param CustomerRepository $repository
|
||||
* @param FormConfiguration $configuration
|
||||
*/
|
||||
public function __construct(CustomerRepository $repository, FormConfiguration $configuration)
|
||||
public function __construct(CustomerRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +74,7 @@ class CustomerController extends AbstractController
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -97,7 +107,7 @@ class CustomerController extends AbstractController
|
||||
* @Security("is_granted('create_customer')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -114,7 +124,7 @@ class CustomerController extends AbstractController
|
||||
* @Security("is_granted('budget', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Customer $customer)
|
||||
{
|
||||
@@ -130,7 +140,7 @@ class CustomerController extends AbstractController
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -143,7 +153,7 @@ class CustomerController extends AbstractController
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -195,22 +205,26 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch(CustomerMetaDefinitionEvent::class, $event);
|
||||
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/edit.html.twig', [
|
||||
@@ -221,7 +235,7 @@ class CustomerController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(CustomerQuery $query)
|
||||
{
|
||||
@@ -235,7 +249,7 @@ class CustomerController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Customer $customer)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,9 @@ use App\Repository\Query\ExportQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -76,7 +78,7 @@ class ExportController extends AbstractController
|
||||
* @Security("is_granted('view_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
@@ -105,7 +107,7 @@ class ExportController extends AbstractController
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(Request $request)
|
||||
@@ -126,8 +128,6 @@ class ExportController extends AbstractController
|
||||
|
||||
$renderer = $this->export->getRendererById($type);
|
||||
|
||||
// this code should not be reached, as the query already filters invalid values
|
||||
// when trying to call setType() with an unknown value
|
||||
if (null === $renderer) {
|
||||
throw $this->createNotFoundException('Unknown export renderer');
|
||||
}
|
||||
@@ -141,7 +141,7 @@ class ExportController extends AbstractController
|
||||
* @param ExportQuery $query
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(ExportQuery $query)
|
||||
protected function getEntries(ExportQuery $query): array
|
||||
{
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
@@ -151,9 +151,9 @@ class ExportController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param ExportQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ExportQuery $query)
|
||||
protected function getToolbarForm(ExportQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ExportToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('export', []),
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
@@ -19,7 +20,11 @@ use App\Repository\Query\ProjectQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -34,10 +39,15 @@ class ProjectController extends AbstractController
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ProjectRepository $repository)
|
||||
public function __construct(ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getRepository(): ProjectRepository
|
||||
@@ -52,7 +62,7 @@ class ProjectController extends AbstractController
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -88,7 +98,7 @@ class ProjectController extends AbstractController
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Customer|null $customer
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request, ?Customer $customer = null)
|
||||
{
|
||||
@@ -106,7 +116,7 @@ class ProjectController extends AbstractController
|
||||
* @Security("is_granted('budget', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Project $project)
|
||||
{
|
||||
@@ -122,7 +132,7 @@ class ProjectController extends AbstractController
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -135,7 +145,7 @@ class ProjectController extends AbstractController
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -188,29 +198,32 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
protected function renderProjectForm(Project $project, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($project);
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch(ProjectMetaDefinitionEvent::class, $event);
|
||||
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newProject = new Project();
|
||||
$newProject->setCustomer($project->getCustomer());
|
||||
$editForm = $this->createEditForm($newProject);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_project');
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newProject = new Project();
|
||||
$newProject->setCustomer($project->getCustomer());
|
||||
$editForm = $this->createEditForm($newProject);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +235,7 @@ class ProjectController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param ProjectQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ProjectQuery $query)
|
||||
{
|
||||
@@ -236,7 +249,7 @@ class ProjectController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Project $project)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Controller;
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
@@ -23,6 +24,7 @@ use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -45,17 +47,23 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
* @var TrackingModeService
|
||||
*/
|
||||
protected $trackingModeService;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
UserDateTimeFactory $dateTime,
|
||||
TimesheetConfiguration $configuration,
|
||||
TimesheetRepository $repository,
|
||||
TrackingModeService $service
|
||||
TrackingModeService $service,
|
||||
EventDispatcherInterface $dispatcher
|
||||
) {
|
||||
$this->dateTime = $dateTime;
|
||||
$this->configuration = $configuration;
|
||||
$this->repository = $repository;
|
||||
$this->trackingModeService = $service;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
@@ -127,17 +135,21 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
*/
|
||||
protected function edit(Timesheet $entry, Request $request, string $renderTemplate)
|
||||
{
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->save($entry);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
|
||||
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
@@ -169,6 +181,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$entry->setActivity($activity);
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
$mode->create($entry, $request);
|
||||
|
||||
@@ -176,8 +191,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
try {
|
||||
if (null === $entry->getEnd()) {
|
||||
$this->getRepository()->stopActiveEntries(
|
||||
@@ -185,15 +198,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$this->configuration->getActiveEntriesHardLimit()
|
||||
);
|
||||
}
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->getRepository()->save($entry);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
|
||||
@@ -83,12 +83,6 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->scalarNode('mode')
|
||||
->defaultValue('default')
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
return !in_array($value, ['default', 'duration_only', 'punch']);
|
||||
})
|
||||
->thenInvalid('Chosen timesheet mode is invalid, allowed values: default, duration_only, punch')
|
||||
->end()
|
||||
->end()
|
||||
->booleanNode('markdown_content')
|
||||
->defaultValue(false)
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Table(name="kimai2_activities")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*/
|
||||
class Activity
|
||||
class Activity implements EntityWithMetaFields
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -73,9 +73,17 @@ class Activity
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
/**
|
||||
* @var ActivityMeta[]|Collection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timesheets = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -139,6 +147,55 @@ class Activity
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getVisibleMetaFields(): array
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->meta as $meta) {
|
||||
if ($meta->isVisible()) {
|
||||
$all[] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||
{
|
||||
foreach ($this->meta as $field) {
|
||||
if ($field->getName() === $name) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
$this->meta->add($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$current->merge($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
54
src/Entity/ActivityMeta.php
Normal file
54
src/Entity/ActivityMeta.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_activities_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"activity_id", "name"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ActivityMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
if (!($entity instanceof Activity)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Expected instanceof Activity, received "%s"', get_class($entity))
|
||||
);
|
||||
}
|
||||
$this->activity = $entity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEntity(): ?EntityWithMetaFields
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Table(name="kimai2_customers")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*/
|
||||
class Customer
|
||||
class Customer implements EntityWithMetaFields
|
||||
{
|
||||
public const DEFAULT_CURRENCY = 'EUR';
|
||||
|
||||
@@ -154,9 +154,17 @@ class Customer
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
/**
|
||||
* @var CustomerMeta[]|Collection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->projects = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -352,6 +360,55 @@ class Customer
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getVisibleMetaFields(): array
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->meta as $meta) {
|
||||
if ($meta->isVisible()) {
|
||||
$all[] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||
{
|
||||
foreach ($this->meta as $field) {
|
||||
if ($field->getName() === $name) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
$this->meta->add($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$current->merge($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
54
src/Entity/CustomerMeta.php
Normal file
54
src/Entity/CustomerMeta.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_customers_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"customer_id", "name"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class CustomerMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
if (!($entity instanceof Customer)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Expected instanceof Customer, received "%s"', get_class($entity))
|
||||
);
|
||||
}
|
||||
$this->customer = $entity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEntity(): ?EntityWithMetaFields
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
}
|
||||
25
src/Entity/EntityWithMetaFields.php
Normal file
25
src/Entity/EntityWithMetaFields.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\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
interface EntityWithMetaFields
|
||||
{
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getMetaFields(): Collection;
|
||||
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface;
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields;
|
||||
}
|
||||
136
src/Entity/MetaTableTypeInterface.php
Normal file
136
src/Entity/MetaTableTypeInterface.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
interface MetaTableTypeInterface
|
||||
{
|
||||
/**
|
||||
* Returns the name of this entry.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
|
||||
/**
|
||||
* Sets the name of this entry.
|
||||
*
|
||||
* @param string $name
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setName(string $name): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue();
|
||||
|
||||
/**
|
||||
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||
*
|
||||
* @param mixed|null $value
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setValue($value): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Get the linked entity.
|
||||
*
|
||||
* @return EntityWithMetaFields|null
|
||||
*/
|
||||
public function getEntity(): ?EntityWithMetaFields;
|
||||
|
||||
/**
|
||||
* Set the linked entity of this entry.
|
||||
*
|
||||
* @param EntityWithMetaFields $entity
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* This will merge the current object with the values from the given $meta instance.
|
||||
* It should NOT update the name or value, but only the form settings.
|
||||
*
|
||||
* @param MetaTableTypeInterface $meta
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether this field can be displayed in "public" places like API results or export.
|
||||
*
|
||||
* @param bool $include
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setIsVisible(bool $include): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether this field can be displayed in "public" places like API results or export.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible(): bool;
|
||||
|
||||
/**
|
||||
* Whether this field is required to be filled out in the form.
|
||||
*
|
||||
* @param bool $isRequired
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setIsRequired(bool $isRequired): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether the form field is required.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired(): bool;
|
||||
|
||||
/**
|
||||
* The form type for this field.
|
||||
* If this method returns null, it will not be shown on the form.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getType(): ?string;
|
||||
|
||||
/**
|
||||
* Sets the form type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setType(string $type): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Get all constraints that should be attached to the form type.
|
||||
*
|
||||
* @return Constraint[]
|
||||
*/
|
||||
public function getConstraints(): array;
|
||||
|
||||
/**
|
||||
* Adds a constraint to the form type.
|
||||
*
|
||||
* @param Constraint $constraint
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function addConstraint(Constraint $constraint): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Sets all constraints for the form type, overwriting all previously attached.
|
||||
*
|
||||
* @param Constraint[] $constraints
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setConstraints(array $constraints): MetaTableTypeInterface;
|
||||
}
|
||||
179
src/Entity/MetaTableTypeTrait.php
Normal file
179
src/Entity/MetaTableTypeTrait.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait MetaTableTypeTrait
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\Length(min=2, max=50)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $visible = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $required = false;
|
||||
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
private $constraints = [];
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): MetaTableTypeInterface
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
switch ($this->type) {
|
||||
case CheckboxType::class:
|
||||
return (bool) $this->value;
|
||||
case IntegerType::class:
|
||||
return (int) $this->value;
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setValue($value): MetaTableTypeInterface
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConstraints(array $constraints): MetaTableTypeInterface
|
||||
{
|
||||
$this->constraints = [];
|
||||
|
||||
foreach ($constraints as $constraint) {
|
||||
$this->addConstraint($constraint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $type): MetaTableTypeInterface
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addConstraint(Constraint $constraint): MetaTableTypeInterface
|
||||
{
|
||||
$this->constraints[] = $constraint;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Constraint[]
|
||||
*/
|
||||
public function getConstraints(): array
|
||||
{
|
||||
return $this->constraints;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setIsRequired(bool $isRequired): MetaTableTypeInterface
|
||||
{
|
||||
$this->required = $isRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setIsVisible(bool $visible): MetaTableTypeInterface
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface
|
||||
{
|
||||
$this
|
||||
->setType($meta->getType())
|
||||
->setConstraints($meta->getConstraints())
|
||||
->setIsRequired($meta->isRequired())
|
||||
->setIsVisible($meta->isVisible());
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Table(name="kimai2_projects")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
*/
|
||||
class Project
|
||||
class Project implements EntityWithMetaFields
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -89,10 +89,18 @@ class Project
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @var ProjectMeta[]|Collection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\ProjectMeta", mappedBy="project", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->activities = new ArrayCollection();
|
||||
$this->timesheets = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -190,6 +198,55 @@ class Project
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getVisibleMetaFields(): array
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->meta as $meta) {
|
||||
if ($meta->isVisible()) {
|
||||
$all[] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||
{
|
||||
foreach ($this->meta as $field) {
|
||||
if ($field->getName() === $name) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
$this->meta->add($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$current->merge($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
54
src/Entity/ProjectMeta.php
Normal file
54
src/Entity/ProjectMeta.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_projects_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"project_id", "name"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ProjectMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
if (!($entity instanceof Project)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Expected instanceof Project, received "%s"', get_class($entity))
|
||||
);
|
||||
}
|
||||
$this->project = $entity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEntity(): ?EntityWithMetaFields
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @App\Validator\Constraints\Timesheet
|
||||
*/
|
||||
class Timesheet
|
||||
class Timesheet implements EntityWithMetaFields
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -138,7 +138,14 @@ class Timesheet
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
protected $tags;
|
||||
private $tags;
|
||||
|
||||
/**
|
||||
* @var TimesheetMeta[]|Collection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* Default constructor, initializes collections
|
||||
@@ -146,6 +153,7 @@ class Timesheet
|
||||
public function __construct()
|
||||
{
|
||||
$this->tags = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,6 +413,7 @@ class Timesheet
|
||||
/**
|
||||
* BE WARNED: this method should NOT be used programmatically, there is very likely no reason for it!
|
||||
*
|
||||
* @internal
|
||||
* @deprecated since it was introduced, only meant for the initial migration. Will be removed with 1.0.
|
||||
* @param string $timezone
|
||||
* @return Timesheet
|
||||
@@ -415,4 +424,53 @@ class Timesheet
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getVisibleMetaFields(): array
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->meta as $meta) {
|
||||
if ($meta->isVisible()) {
|
||||
$all[] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||
{
|
||||
foreach ($this->meta as $field) {
|
||||
if ($field->getName() === $name) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
$this->meta->add($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$current->merge($meta);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
54
src/Entity/TimesheetMeta.php
Normal file
54
src/Entity/TimesheetMeta.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_timesheet_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"timesheet_id", "name"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class TimesheetMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Timesheet
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Timesheet", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $timesheet;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Expected instanceof Timesheet, received "%s"', get_class($entity))
|
||||
);
|
||||
}
|
||||
$this->timesheet = $entity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEntity(): ?EntityWithMetaFields
|
||||
{
|
||||
return $this->timesheet;
|
||||
}
|
||||
}
|
||||
34
src/Event/ActivityMetaDefinitionEvent.php
Normal file
34
src/Event/ActivityMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to activities
|
||||
*/
|
||||
final class ActivityMetaDefinitionEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Activity
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
public function __construct(Activity $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Activity
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
/**
|
||||
* The ConfigureAdminMenuEvent is used for populating the administration navigation.
|
||||
*/
|
||||
class ConfigureAdminMenuEvent extends Event
|
||||
final class ConfigureAdminMenuEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.admin_menu_configure';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
/**
|
||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||
*/
|
||||
class ConfigureMainMenuEvent extends Event
|
||||
final class ConfigureMainMenuEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.main_menu_configure';
|
||||
|
||||
|
||||
34
src/Event/CustomerMetaDefinitionEvent.php
Normal file
34
src/Event/CustomerMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to customers
|
||||
*/
|
||||
final class CustomerMetaDefinitionEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
public function __construct(Customer $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Customer
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use App\Entity\User;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
class DashboardEvent extends Event
|
||||
final class DashboardEvent extends Event
|
||||
{
|
||||
public const DASHBOARD = 'app.dashboard';
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if a user profile is loaded and we full data including dynamic user preferences
|
||||
* This event should be used, if a user profile is loaded and want to fill the dynamic user preferences
|
||||
*/
|
||||
class PrepareUserEvent extends Event
|
||||
final class PrepareUserEvent extends Event
|
||||
{
|
||||
public const PREPARE = 'app.prepare_user';
|
||||
|
||||
|
||||
34
src/Event/ProjectMetaDefinitionEvent.php
Normal file
34
src/Event/ProjectMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to projects
|
||||
*/
|
||||
final class ProjectMetaDefinitionEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
public function __construct(Project $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Project
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\EventDispatcher\Event;
|
||||
/**
|
||||
* This event should be used, if system configurations should be changed/added dynamically.
|
||||
*/
|
||||
class SystemConfigurationEvent extends Event
|
||||
final class SystemConfigurationEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.system_configuration';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Event;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
class ThemeEvent extends Event
|
||||
final class ThemeEvent extends Event
|
||||
{
|
||||
public const JAVASCRIPT = 'app.theme.javascript';
|
||||
public const STYLESHEET = 'app.theme.css';
|
||||
|
||||
34
src/Event/TimesheetMetaDefinitionEvent.php
Normal file
34
src/Event/TimesheetMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to timesheets
|
||||
*/
|
||||
final class TimesheetMetaDefinitionEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
public function __construct(Timesheet $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Timesheet
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use Symfony\Component\EventDispatcher\Event;
|
||||
/**
|
||||
* This event should be used, if further user preferences should added dynamically
|
||||
*/
|
||||
class UserPreferenceEvent extends Event
|
||||
final class UserPreferenceEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.user_preferences';
|
||||
|
||||
|
||||
@@ -26,12 +26,10 @@ abstract class AbstractSpreadsheetRenderer
|
||||
* @var DateExtensions
|
||||
*/
|
||||
protected $dateExtension;
|
||||
|
||||
/**
|
||||
* @var Extensions
|
||||
*/
|
||||
protected $extension;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
@@ -42,8 +40,11 @@ abstract class AbstractSpreadsheetRenderer
|
||||
* @param DateExtensions $dateExtension
|
||||
* @param Extensions $extensions
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
||||
{
|
||||
public function __construct(
|
||||
TranslatorInterface $translator,
|
||||
DateExtensions $dateExtension,
|
||||
Extensions $extensions
|
||||
) {
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
@@ -97,6 +98,15 @@ abstract class AbstractSpreadsheetRenderer
|
||||
*/
|
||||
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
|
||||
{
|
||||
$publicMetaFields = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||
$publicMetaFields[] = $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$publicMetaFields = array_unique($publicMetaFields);
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
@@ -112,10 +122,13 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaFieldName));
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
@@ -148,21 +161,32 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$metaField = $timesheet->getMetaField($metaFieldName);
|
||||
$metaFieldValue = '';
|
||||
if (null !== $metaField && $metaField->isVisible()) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow(12, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow(13, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
$cellDurationTotal = $recordsHeaderColumn - 1;
|
||||
$cellRateTotal = $recordsHeaderColumn;
|
||||
|
||||
$sheet->getCellByColumnAndRow(13, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow(13, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
$sheet->setCellValueByColumnAndRow($cellDurationTotal, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow($cellRateTotal, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
|
||||
@@ -15,7 +15,7 @@ use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class HtmlRenderer implements RendererInterface
|
||||
final class HtmlRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
@@ -24,9 +24,6 @@ class HtmlRenderer implements RendererInterface
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
@@ -42,9 +39,17 @@ class HtmlRenderer implements RendererInterface
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
$publicMetaFields = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||
$publicMetaFields[] = $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$content = $this->twig->render('export/renderer/default.html.twig', [
|
||||
'entries' => $timesheets,
|
||||
'query' => $query,
|
||||
'metaFields' => array_unique($publicMetaFields),
|
||||
'summaries' => $this->calculateSummary($timesheets),
|
||||
]);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
class PDFRenderer implements RendererInterface
|
||||
final class PDFRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
|
||||
40
src/Form/API/ActivityApiEditForm.php
Normal file
40
src/Form/API/ActivityApiEditForm.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Form\ActivityEditForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ActivityApiEditForm extends ActivityEditForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->remove('metaFields');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
'create_more' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
src/Form/API/CustomerApiEditForm.php
Normal file
39
src/Form/API/CustomerApiEditForm.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Form\CustomerEditForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerApiEditForm extends CustomerEditForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->remove('metaFields');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
40
src/Form/API/ProjectApiEditForm.php
Normal file
40
src/Form/API/ProjectApiEditForm.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Form\ProjectEditForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectApiEditForm extends ProjectEditForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->remove('metaFields');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
'create_more' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
37
src/Form/API/TimesheetApiEditForm.php
Normal file
37
src/Form/API/TimesheetApiEditForm.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Form\TimesheetEditForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TimesheetApiEditForm extends TimesheetEditForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->remove('metaFields');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
'allow_duration' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Form\Type\ColorPickerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
@@ -47,6 +48,8 @@ trait EntityFormTrait
|
||||
;
|
||||
}
|
||||
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
$builder
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserType;
|
||||
@@ -134,6 +135,8 @@ class TimesheetEditForm extends AbstractType
|
||||
$this->addTags($builder);
|
||||
$this->addRates($builder, $currency, $options);
|
||||
$this->addUser($builder, $options);
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
$this->addExported($builder, $options);
|
||||
}
|
||||
|
||||
|
||||
60
src/Form/Type/EntityMetaDefinitionType.php
Normal file
60
src/Form/Type/EntityMetaDefinitionType.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\MetaTableTypeInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to edit entity meta field.
|
||||
*/
|
||||
class EntityMetaDefinitionType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) {
|
||||
/** @var MetaTableTypeInterface $definition */
|
||||
$definition = $event->getData();
|
||||
|
||||
if (!($definition instanceof MetaTableTypeInterface)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevents unconfigured values from showing up in the form
|
||||
if (null === $definition->getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $definition->getType(), [
|
||||
'label' => $definition->getName(),
|
||||
'constraints' => $definition->getConstraints(),
|
||||
'required' => $definition->isRequired(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MetaTableTypeInterface::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
src/Form/Type/MetaFieldsCollectionType.php
Normal file
39
src/Form/Type/MetaFieldsCollectionType.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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 Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to edit entity meta fields.
|
||||
*/
|
||||
class MetaFieldsCollectionType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'entry_type' => EntityMetaDefinitionType::class,
|
||||
'entry_options' => ['label' => false],
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
'label' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return CollectionType::class;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ trait RendererTrait
|
||||
{
|
||||
$customer = $model->getCustomer();
|
||||
$project = $model->getQuery()->getProject();
|
||||
$activity = $model->getQuery()->getActivity();
|
||||
$currency = $model->getCalculator()->getCurrency();
|
||||
|
||||
$values = [
|
||||
@@ -108,6 +109,20 @@ trait RendererTrait
|
||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||
];
|
||||
|
||||
if (null !== $activity) {
|
||||
$values = array_merge($values, [
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $project) {
|
||||
$values = array_merge($values, [
|
||||
'project.id' => $project->getId(),
|
||||
@@ -115,6 +130,12 @@ trait RendererTrait
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
@@ -129,6 +150,12 @@ trait RendererTrait
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
@@ -169,7 +196,7 @@ trait RendererTrait
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
return [
|
||||
$values = [
|
||||
'entry.row' => '',
|
||||
'entry.description' => $description,
|
||||
'entry.amount' => $amount,
|
||||
@@ -196,6 +223,14 @@ trait RendererTrait
|
||||
'entry.customer' => $customer->getName(),
|
||||
'entry.customer_id' => $customer->getId(),
|
||||
];
|
||||
|
||||
foreach ($timesheet->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
83
src/Migrations/Version20190617100845.php
Normal file
83
src/Migrations/Version20190617100845.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Creates meta tables to store custom fields for entities
|
||||
*
|
||||
* @version 1.0
|
||||
*/
|
||||
final class Version20190617100845 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Creates meta tables to store custom fields for entities';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$timesheetMeta = $schema->createTable('kimai2_timesheet_meta');
|
||||
$timesheetMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$timesheetMeta->addColumn('timesheet_id', 'integer', ['notnull' => true]);
|
||||
$timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$timesheetMeta->setPrimaryKey(['id']);
|
||||
$timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE');
|
||||
$timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06');
|
||||
$timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE');
|
||||
|
||||
$customerMeta = $schema->createTable('kimai2_customers_meta');
|
||||
$customerMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$customerMeta->addColumn('customer_id', 'integer', ['notnull' => true]);
|
||||
$customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$customerMeta->setPrimaryKey(['id']);
|
||||
$customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3');
|
||||
$customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06');
|
||||
$customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3');
|
||||
|
||||
$projectMeta = $schema->createTable('kimai2_projects_meta');
|
||||
$projectMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$projectMeta->addColumn('project_id', 'integer', ['notnull' => true]);
|
||||
$projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$projectMeta->setPrimaryKey(['id']);
|
||||
$projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C');
|
||||
$projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06');
|
||||
$projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C');
|
||||
|
||||
$activityMeta = $schema->createTable('kimai2_activities_meta');
|
||||
$activityMeta->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$activityMeta->addColumn('activity_id', 'integer', ['notnull' => true]);
|
||||
$activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
|
||||
$activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
|
||||
$activityMeta->setPrimaryKey(['id']);
|
||||
$activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096');
|
||||
$activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06');
|
||||
$activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_timesheet_meta');
|
||||
$schema->dropTable('kimai2_customers_meta');
|
||||
$schema->dropTable('kimai2_projects_meta');
|
||||
$schema->dropTable('kimai2_activities_meta');
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,18 @@ use Pagerfanta\Pagerfanta;
|
||||
|
||||
class ActivityRepository extends AbstractRepository
|
||||
{
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveActivity(Activity $activity)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return null|Activity
|
||||
|
||||
@@ -22,6 +22,18 @@ use Pagerfanta\Pagerfanta;
|
||||
|
||||
class CustomerRepository extends AbstractRepository
|
||||
{
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveCustomer(Customer $customer)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return null|Customer
|
||||
|
||||
@@ -82,7 +82,6 @@ final class TimesheetIdLoader implements LoaderInterface
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
/*
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'meta')
|
||||
->from(Timesheet::class, 't')
|
||||
@@ -90,6 +89,5 @@ final class TimesheetIdLoader implements LoaderInterface
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,18 @@ use Pagerfanta\Pagerfanta;
|
||||
*/
|
||||
class ProjectRepository extends AbstractRepository
|
||||
{
|
||||
/**
|
||||
* @param Project $project
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveProject(Project $project)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return null|Project
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Entity\Timesheet;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class DurationFixedStartMode implements TrackingModeInterface
|
||||
class DurationFixedBeginMode implements TrackingModeInterface
|
||||
{
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
@@ -62,7 +62,7 @@ class DurationFixedStartMode implements TrackingModeInterface
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'duration_fixed_start';
|
||||
return 'duration_fixed_begin';
|
||||
}
|
||||
|
||||
public function canSeeBeginAndEndTimes(): bool
|
||||
@@ -11,7 +11,7 @@ namespace App\Timesheet;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Timesheet\TrackingMode\DefaultMode;
|
||||
use App\Timesheet\TrackingMode\DurationFixedStartMode;
|
||||
use App\Timesheet\TrackingMode\DurationFixedBeginMode;
|
||||
use App\Timesheet\TrackingMode\DurationOnlyMode;
|
||||
use App\Timesheet\TrackingMode\PunchInOutMode;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
@@ -43,7 +43,7 @@ class TrackingModeService
|
||||
new DefaultMode($this->dateTime, $this->configuration),
|
||||
new PunchInOutMode(),
|
||||
new DurationOnlyMode($this->dateTime, $this->configuration),
|
||||
new DurationFixedStartMode($this->dateTime, $this->configuration),
|
||||
new DurationFixedBeginMode($this->dateTime, $this->configuration),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user