support custom fields for timesheets, customers, projects and activities (#871)

This commit is contained in:
Kevin Papst
2019-06-26 00:39:05 +02:00
committed by GitHub
parent 994c671fd8
commit d8621f0b7a
103 changed files with 2567 additions and 238 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
]);

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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', []),

View File

@@ -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)
{

View File

@@ -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, [

View File

@@ -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)

View File

@@ -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
*/

View 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;
}
}

View File

@@ -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
*/

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}
}

View File

@@ -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
*/

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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';

View File

@@ -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';

View 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;
}
}

View File

@@ -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';

View File

@@ -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';

View 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;
}
}

View File

@@ -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';

View File

@@ -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';

View 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;
}
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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),
]);

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View File

@@ -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',

View File

@@ -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);
}

View 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,
]);
}
}

View 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;
}
}

View File

@@ -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;
}
/**

View 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');
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
*/
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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),
];
}