added global activities (#259)

This commit is contained in:
Kevin Papst
2018-11-06 23:10:34 +01:00
committed by GitHub
parent c66dfbc653
commit db7de3aac1
70 changed files with 1168 additions and 264 deletions

View File

@@ -13,7 +13,10 @@ namespace App\API;
use App\Entity\Activity;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityQuery;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation as API;
@@ -55,12 +58,31 @@ class ActivityController extends Controller
* description="Returns the collection of all existing activities",
* @SWG\Schema(ref=@API\Model(type=Activity::class)),
* )
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, only global activities will be returned.")
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities")
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Pass true to fetch only global activities")
*
* @return Response
*/
public function cgetAction()
public function cgetAction(ParamFetcherInterface $paramFetcher)
{
$data = $this->repository->findAll();
$query = new ActivityQuery();
$query->setOrderGlobalsFirst(true)
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS);
if (null !== ($globals = $paramFetcher->get('globals'))) {
$query->setGlobalsOnly(true);
}
if (null !== ($project = $paramFetcher->get('project'))) {
$query->setProject($project);
}
if (null !== ($visible = $paramFetcher->get('visible'))) {
$query->setVisibility($visible);
}
$data = $this->repository->findByQuery($query);
$view = new View($data, 200);
return $this->viewHandler->handle($view);

View File

@@ -13,7 +13,10 @@ namespace App\API;
use App\Entity\Project;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectQuery;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
@@ -55,12 +58,26 @@ class ProjectController extends Controller
* description="Returns the collection of all existing projects",
* @SWG\Schema(ref=@Model(type=Project::class)),
* )
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects")
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects")
*
* @param ParamFetcherInterface $paramFetcher
* @return Response
*/
public function cgetAction()
public function cgetAction(ParamFetcherInterface $paramFetcher)
{
$data = $this->repository->findAll();
$query = new ProjectQuery();
$query->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS);
if (null !== ($customer = $paramFetcher->get('customer'))) {
$query->setCustomer($customer);
}
if (null !== ($visible = $paramFetcher->get('visible'))) {
$query->setVisibility($visible);
}
$data = $this->repository->findByQuery($query);
$view = new View($data, 200);
return $this->viewHandler->handle($view);

View File

@@ -63,8 +63,8 @@ class TimesheetEntity
$this->start = $entry->getBegin();
$this->title = $entry->getActivity()->getName();
$this->description = $entry->getDescription();
$this->customer = $entry->getActivity()->getProject()->getCustomer()->getName();
$this->project = $entry->getActivity()->getProject()->getName();
$this->customer = $entry->getProject()->getCustomer()->getName();
$this->project = $entry->getProject()->getName();
$this->activity = $entry->getActivity()->getName();
if (null === $entry->getEnd()) {

View File

@@ -37,12 +37,11 @@ class ActivityController extends AbstractController
public function recentActivitiesAction()
{
$user = $this->getUser();
// TODO make days configurable
$activeEntries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
$entries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
return $this->render(
'navbar/recent-activities.html.twig',
['activities' => $activeEntries]
['entries' => $entries]
);
}
}

View File

@@ -9,7 +9,6 @@
namespace App\Controller;
use App\Entity\Activity;
use App\Entity\Timesheet;
use App\Form\TimesheetEditForm;
use App\Form\Toolbar\TimesheetToolbarForm;
@@ -19,6 +18,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Controller used to manage timesheets.
@@ -143,20 +143,36 @@ class TimesheetController extends AbstractController
}
/**
* The route to start a running entry.
* The route to re-start a timesheet entry.
*
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
* @Security("is_granted('start', activity)")
* @Security("is_granted('start', timesheet)")
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function startAction(Activity $activity)
public function startAction(ValidatorInterface $validator, Timesheet $timesheet)
{
$user = $this->getUser();
try {
$this->getRepository()->startRecording($user, $activity);
$this->flashSuccess('timesheet.start.success');
$entry = new Timesheet();
$entry
->setBegin(new \DateTime())
->setUser($user)
->setActivity($timesheet->getActivity())
->setProject($timesheet->getProject())
;
$errors = $validator->validate($entry);
if (count($errors) > 0) {
$this->flashError('timesheet.start.error', ['%reason%' => $errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage()]);
} else {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry);
$entityManager->flush();
$this->flashSuccess('timesheet.start.success');
}
} catch (\Exception $ex) {
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
}

View File

@@ -30,6 +30,8 @@ class CustomerFixtures extends Fixture
public const MAX_CUSTOMERS = 15;
public const MIN_BUDGET = 0;
public const MAX_BUDGET = 100000;
public const MIN_GLOBAL_ACTIVITIES = 5;
public const MAX_GLOBAL_ACTIVITIES = 50;
public const MIN_PROJECTS_PER_CUSTOMER = 10;
public const MAX_PROJECTS_PER_CUSTOMER = 50;
public const MIN_ACTIVITIES_PER_PROJECT = 0;
@@ -66,6 +68,16 @@ class CustomerFixtures extends Fixture
$manager->flush();
$manager->clear();
}
$amountGlobalActivities = rand(self::MIN_GLOBAL_ACTIVITIES, self::MAX_GLOBAL_ACTIVITIES);
for ($c = 1; $c <= $amountGlobalActivities; $c++) {
$visibleActivity = 0 != $a % 3;
$activity = $this->createActivity($faker, null, $visibleActivity);
$manager->persist($activity);
}
$manager->flush();
$manager->clear();
}
/**
@@ -113,11 +125,11 @@ class CustomerFixtures extends Fixture
/**
* @param Generator $faker
* @param Project $project
* @param Project|null $project
* @param bool $visible
* @return Activity
*/
private function createActivity(Generator $faker, Project $project, $visible)
private function createActivity(Generator $faker, ?Project $project, $visible)
{
$entry = new Activity();
$entry

View File

@@ -10,6 +10,7 @@
namespace App\DataFixtures;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Entity\UserPreference;
@@ -56,6 +57,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
{
$allUser = $this->getAllUsers($manager);
$activities = $this->getAllActivities($manager);
$projects = $this->getAllProjects($manager);
$faker = Factory::create();
@@ -80,6 +82,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
$entry = $this->createTimesheetEntry(
$user,
$activities[array_rand($activities)],
$projects[array_rand($projects)],
$description
);
@@ -98,6 +101,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
$entry = $this->createTimesheetEntry(
$user,
$activities[array_rand($activities)],
$projects[array_rand($projects)],
null,
false
);
@@ -126,6 +130,22 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
return $all;
}
/**
* @param ObjectManager $manager
* @return Project[]
*/
protected function getAllProjects(ObjectManager $manager)
{
$all = [];
/* @var Project[] $entries */
$entries = $manager->getRepository(Project::class)->findAll();
foreach ($entries as $temp) {
$all[$temp->getId()] = $temp;
}
return $all;
}
/**
* @param ObjectManager $manager
* @return Activity[]
@@ -133,7 +153,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
protected function getAllActivities(ObjectManager $manager)
{
$all = [];
/* @var User[] $entries */
/* @var Activity[] $entries */
$entries = $manager->getRepository(Activity::class)->findAll();
foreach ($entries as $temp) {
$all[$temp->getId()] = $temp;
@@ -142,7 +162,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
return $all;
}
private function createTimesheetEntry(User $user, Activity $activity, $description, $setEndDate = true)
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, $setEndDate = true)
{
$start = new \DateTime();
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
@@ -151,6 +171,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
$entry = new Timesheet();
$entry
->setActivity($activity)
->setProject($activity->getProject() ?? $project)
->setDescription($description)
->setUser($user)
->setBegin($start);

View File

@@ -34,7 +34,6 @@ class Activity
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
* @ORM\JoinColumn(onDelete="CASCADE")
* @Assert\NotNull()
*/
private $project;

View File

@@ -33,7 +33,7 @@ class Project
* @var Customer
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="projects")
* @ORM\JoinColumn(onDelete="CASCADE")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $customer;
@@ -101,6 +101,13 @@ class Project
*/
private $hourlyRate = null;
/**
* @var Timesheet[]
*
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="project")
*/
private $timesheets;
/**
* @return int
*/
@@ -118,8 +125,8 @@ class Project
}
/**
* @param $customer
* @return $this
* @param Customer $customer
* @return Project
*/
public function setCustomer($customer)
{
@@ -129,8 +136,6 @@ class Project
}
/**
* Set name
*
* @param string $name
* @return Project
*/
@@ -206,6 +211,25 @@ class Project
return $this->budget;
}
/**
* @param Timesheet[] $timesheets
* @return Project
*/
public function setTimesheets($timesheets)
{
$this->timesheets = $timesheets;
return $this;
}
/**
* @return Timesheet[]
*/
public function getTimesheets()
{
return $this->timesheets;
}
/**
* @param Activity[] $activities
* @return Project

View File

@@ -64,7 +64,7 @@ class Timesheet
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE")
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $user;
@@ -73,11 +73,20 @@ class Timesheet
* @var Activity
*
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="timesheets")
* @ORM\JoinColumn(onDelete="CASCADE")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $activity;
/**
* @var Project
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="timesheets")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $project;
/**
* @var string
*
@@ -232,6 +241,25 @@ class Timesheet
return $this->activity;
}
/**
* @return Project
*/
public function getProject()
{
return $this->project;
}
/**
* @param Project $project
* @return Timesheet
*/
public function setProject(Project $project)
{
$this->project = $project;
return $this;
}
/**
* Set description
*
@@ -317,6 +345,8 @@ class Timesheet
}
/**
* These validations are used in places, where we don't use a form yet (like the API).
*
* @param ExecutionContextInterface $context
* @param mixed $payload
*
@@ -324,6 +354,27 @@ class Timesheet
*/
public function validate(ExecutionContextInterface $context, $payload)
{
if (null === $this->getActivity()) {
$context->buildViolation('A timesheet must have an activity.')
->atPath('activity')
->setTranslationDomain('validators')
->addViolation();
}
if (null === $this->getProject()) {
$context->buildViolation('A timesheet must have a project.')
->atPath('project')
->setTranslationDomain('validators')
->addViolation();
}
if (null !== $this->getActivity() && null !== $this->getProject() && null !== $this->getActivity()->getProject() && $this->getActivity()->getProject() !== $this->getProject()) {
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
->atPath('project')
->setTranslationDomain('validators')
->addViolation();
}
if (null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
$context->buildViolation('End date must not be earlier then start date.')
->atPath('end')

View File

@@ -10,8 +10,10 @@
namespace App\Form;
use App\Entity\Activity;
use App\Form\Type\CustomerType;
use App\Form\Type\ProjectType;
use App\Form\Type\YesNoType;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -35,8 +37,11 @@ class ActivityEditForm extends AbstractType
$entry = $options['data'];
$project = null;
if ($entry->getId() !== null) {
$customer = null;
if (null !== $entry->getProject()) {
$project = $entry->getProject();
$customer = $project->getCustomer();
}
$builder
@@ -49,9 +54,21 @@ class ActivityEditForm extends AbstractType
'label' => 'label.comment',
'required' => false,
])
// entity type: project
->add('customer', CustomerType::class, [
'label' => 'label.customer',
'query_builder' => function (CustomerRepository $repo) use ($customer) {
return $repo->builderForEntityType($customer);
},
'required' => false,
'mapped' => false,
'attr' => [
'data-related-select' => $this->getBlockPrefix() . '_project',
'data-api-url' => ['get_projects', ['customer' => '-s-']],
],
])
->add('project', ProjectType::class, [
'label' => 'label.project',
'required' => false,
'query_builder' => function (ProjectRepository $repo) use ($project) {
return $repo->builderForEntityType($project);
},

View File

@@ -10,15 +10,21 @@
namespace App\Form;
use App\Entity\Timesheet;
use App\Form\Type\ActivityGroupedWithCustomerNameType;
use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType;
use App\Form\Type\DurationType;
use App\Form\Type\ProjectType;
use App\Form\Type\UserType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -34,9 +40,12 @@ class TimesheetEditForm extends AbstractType
/** @var Timesheet $entry */
$entry = $options['data'];
$activity = null;
if ($entry->getId() !== null) {
$activity = $entry->getActivity();
$activity = $entry->getActivity();
$project = $entry->getProject();
$customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer();
if (null === $project && null !== $activity) {
$project = $activity->getProject();
}
if (null === $entry->getEnd() || !$options['duration_only']) {
@@ -65,7 +74,32 @@ class TimesheetEditForm extends AbstractType
}
$builder
->add('activity', ActivityGroupedWithCustomerNameType::class, [
->add('customer', CustomerType::class, [
'label' => 'label.customer',
'query_builder' => function (CustomerRepository $repo) use ($customer) {
return $repo->builderForEntityType($customer);
},
'data' => $customer ? $customer : '',
'required' => false,
'mapped' => false,
'attr' => [
'data-related-select' => $this->getBlockPrefix() . '_project',
'data-api-url' => ['get_projects', ['customer' => '-s-']],
],
])
->add('project', ProjectType::class, [
'required' => true,
'placeholder' => '',
'label' => 'label.project',
'query_builder' => function (ProjectRepository $repo) use ($project) {
return $repo->builderForEntityType($project);
},
'attr' => [
'data-related-select' => $this->getBlockPrefix() . '_activity',
'data-api-url' => ['get_activities', ['project' => '-s-']],
],
])
->add('activity', ActivityType::class, [
'label' => 'label.activity',
'query_builder' => function (ActivityRepository $repo) use ($activity) {
return $repo->builderForEntityType($activity);
@@ -84,6 +118,38 @@ class TimesheetEditForm extends AbstractType
'required' => false,
])
;
/*
$builder->get('customer')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
$customer = $event->getForm()->getData();
$event->getForm()->getParent()->add('project', ProjectType::class, [
'required' => true,
'placeholder' => '',
'label' => 'label.project',
'query_builder' => function (ProjectRepository $repo) use ($customer) {
return $repo->builderForEntityType(null, $customer);
},
'attr' => [
'data-related-select' => $this->getBlockPrefix() . '_activity',
'data-api-url' => ['get_activities', ['project' => '-s-']],
],
]);
}
);
*/
$builder->get('project')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
$project = $event->getForm()->getData();
$event->getForm()->getParent()->add('activity', ActivityType::class, [
'label' => 'label.activity',
'query_builder' => function (ActivityRepository $repo) use ($project) {
return $repo->builderForEntityType(null, $project);
},
]);
}
);
if ($options['include_user']) {
$builder->add('user', UserType::class);

View File

@@ -150,7 +150,7 @@ abstract class AbstractToolbarForm extends AbstractType
'required' => false,
'query_builder' => function (ProjectRepository $repo) use ($data) {
$qb = $repo->builderForEntityType();
$qb->where('p.customer = :customer')->setParameter('customer', $data['customer']);
$qb->andWhere('p.customer = :customer')->setParameter('customer', $data['customer']);
return $qb;
},
@@ -176,8 +176,7 @@ abstract class AbstractToolbarForm extends AbstractType
'group_by' => null,
'required' => false,
'query_builder' => function (ActivityRepository $repo) use ($data) {
$qb = $repo->builderForEntityType();
$qb->where('a.project = :project')->setParameter('project', $data['project']);
$qb = $repo->builderForEntityType(null, $data['project']);
return $qb;
},

View File

@@ -45,6 +45,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
'entryState.running' => TimesheetQuery::STATE_RUNNING,
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
],
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
]);
}

View File

@@ -1,38 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Entity\Activity;
/**
* Custom form field type to select an activity which are grouped by their Projects, preceeded by their customer names.
*/
class ActivityGroupedWithCustomerNameType extends ActivityType
{
/**
* @param Activity $activity
* @param $key
* @param $index
* @return string
*/
public function groupBy(Activity $activity, $key, $index)
{
return $activity->getProject()->getCustomer()->getName();
}
/**
* @param Activity $activity
* @return string
*/
public function choiceLabel(Activity $activity)
{
return $activity->getProject()->getName() . ': ' . $activity->getName();
}
}

View File

@@ -25,7 +25,11 @@ class ActivityType extends AbstractType
*/
public function groupBy(Activity $activity, $key, $index)
{
return '[' . $activity->getProject()->getId() . '] ' . $activity->getProject()->getName();
if (null === $activity->getProject()) {
return null;
}
return $activity->getProject()->getName();
}
/**
@@ -47,8 +51,17 @@ class ActivityType extends AbstractType
'choice_label' => [$this, 'choiceLabel'],
'group_by' => [$this, 'groupBy'],
'query_builder' => function (ActivityRepository $repo) {
return $repo->builderForEntityType(null);
return $repo->builderForEntityType();
},
'choice_attr' => function (Activity $activity, $key, $value) {
$attributes = [];
if (null !== $activity->getProject()) {
$attributes['data-project'] = $activity->getProject()->getId();
}
return $attributes;
},
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
]);
}

View File

@@ -32,6 +32,7 @@ class CustomerType extends AbstractType
'query_builder' => function (CustomerRepository $repo) {
return $repo->builderForEntityType(null);
},
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
]);
}

View File

@@ -26,6 +26,7 @@ class PageSizeType extends AbstractType
$resolver->setDefaults([
'label' => 'label.pageSize',
'choices' => [10 => 10, 25 => 25, 50 => 50, 75 => 75, 100 => 100],
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
]);
}

View File

@@ -35,6 +35,7 @@ class ProjectType extends AbstractType
'query_builder' => function (ProjectRepository $repo) {
return $repo->builderForEntityType(null);
},
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
]);
}

View File

@@ -31,6 +31,7 @@ class VisibilityType extends AbstractType
'yes' => VisibilityQuery::SHOW_VISIBLE,
'no' => VisibilityQuery::SHOW_HIDDEN,
],
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
]);
}

View File

@@ -0,0 +1,127 @@
<?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;
/**
* Migration that adds the project column to timesheets table, to allow global activities.
* It also fixes foreign-key columns on timesheet and projects table, which are not allowed.
*/
final class Version20181031220003 extends AbstractMigration
{
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$projects = $this->getTableName('projects');
$activities = $this->getTableName('activities');
$users = $this->getTableName('users');
$customers = $this->getTableName('customers');
if ($platform === 'sqlite') {
// project table
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
$this->addSql('DROP TABLE ' . $projects);
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, order_number CLOB DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects);
$this->addSql('DROP TABLE __temp__' . $projects);
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
// timesheet table
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, project_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
} else {
// project table
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL');
// timesheet table
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
}
// update timesheet table and insert project_id from activity table
$this->addSql('UPDATE ' . $timesheet . ' SET project_id = (SELECT project_id FROM ' . $activities . ' WHERE id = activity_id)');
// now update the timesheet table and disallow null values for all required columns (that was a bug before)
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
}
}
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$projects = $this->getTableName('projects');
if ($platform === 'sqlite') {
// project table
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
$this->addSql('DROP TABLE ' . $projects);
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, customer_id INTEGER DEFAULT NULL)');
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects);
$this->addSql('DROP TABLE __temp__' . $projects);
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
// timesheet table
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
} else {
// project table
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL');
// timesheet table
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet);
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
}
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Repository;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\ActivityStatistic;
@@ -35,7 +36,7 @@ class ActivityRepository extends AbstractRepository
/**
* @param User|null $user
* @param \DateTime|null $startFrom
* @return mixed
* @return Timesheet[]
*/
public function getRecentActivities(User $user = null, \DateTime $startFrom = null)
{
@@ -45,7 +46,7 @@ class ActivityRepository extends AbstractRepository
->distinct()
->from(Timesheet::class, 't')
->join('t.activity', 'a')
->join('a.project', 'p')
->join('t.project', 'p')
->join('p.customer', 'c')
->andWhere($qb->expr()->isNotNull('t.end'))
->andWhere('a.visible = 1')
@@ -66,15 +67,7 @@ class ActivityRepository extends AbstractRepository
->setParameter('begin', $startFrom);
}
$results = $qb->getQuery()->getResult();
$activities = [];
/* @var Timesheet $entry */
foreach ($results as $entry) {
$activities[] = $entry->getActivity();
}
return $activities;
return $qb->getQuery()->getResult();
}
/**
@@ -118,14 +111,21 @@ class ActivityRepository extends AbstractRepository
/**
* Returns a query builder that is used for ActivityType and your own 'query_builder' option.
*
* @param Activity|null $entity
* @param Activity|string|null $activity
* @param Project|string|null $project
* @return \Doctrine\ORM\QueryBuilder
*/
public function builderForEntityType(Activity $entity = null)
public function builderForEntityType($activity = null, $project = null)
{
$query = new ActivityQuery();
$query->setHiddenEntity($entity);
$query->setHiddenEntity($activity);
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
$query->setProject($project);
$query->setOrderGlobalsFirst(true);
if (null === $activity && $project === null) {
$query->setGlobalsOnly(true);
}
return $this->findByQuery($query);
}
@@ -140,34 +140,68 @@ class ActivityRepository extends AbstractRepository
$qb->select('a', 'p', 'c')
->from(Activity::class, 'a')
->join('a.project', 'p')
->join('p.customer', 'c')
->leftJoin('a.project', 'p')
->leftJoin('p.customer', 'c')
->orderBy('a.' . $query->getOrderBy(), $query->getOrder());
$where = $qb->expr()->andX();
if (ActivityQuery::SHOW_VISIBLE == $query->getVisibility()) {
$where->add('a.visible = :visible');
if (!$query->isExclusiveVisibility()) {
$qb->andWhere('c.visible = 1');
$qb->andWhere('p.visible = 1');
}
$qb->andWhere('a.visible = 1');
/** @var Activity $entity */
$entity = $query->getHiddenEntity();
if (null !== $entity) {
$qb->orWhere('a.id = :activity')->setParameter('activity', $entity);
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('c.visible', ':visible'),
$qb->expr()->isNull('c.visible')
)
);
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('p.visible', ':visible'),
$qb->expr()->isNull('p.visible')
)
);
}
$qb->setParameter('visible', 1);
} elseif (ActivityQuery::SHOW_HIDDEN == $query->getVisibility()) {
$qb->andWhere('a.visible = 0');
$where->add('a.visible = :visible');
$qb->setParameter('visible', 0);
}
if (null !== $query->getProject()) {
$qb->andWhere('a.project = :project')
->setParameter('project', $query->getProject());
if ($query->isGlobalsOnly()) {
$where->add($qb->expr()->isNull('a.project'));
} elseif (null !== $query->getProject()) {
$where->add(
$qb->expr()->orX(
$qb->expr()->eq('a.project', ':project'),
$qb->expr()->isNull('a.project')
)
);
$qb->setParameter('project', $query->getProject());
} elseif (null !== $query->getCustomer()) {
$qb->andWhere('p.customer = :customer')
->setParameter('customer', $query->getCustomer());
$where->add('p.customer = :customer');
$qb->setParameter('customer', $query->getCustomer());
}
$or = $qb->expr()->orX();
// this must always be the last part before the or
$or->add($where);
// this must always be the last part of the query
/** @var Activity $entity */
$entity = $query->getHiddenEntity();
if (null !== $entity) {
$or->add($qb->expr()->eq('a.id', ':activity'));
$qb->setParameter('activity', $entity);
}
if ($query->isOrderGlobalsFirst()) {
$qb->orderBy('a.project', 'ASC');
}
$qb->andWhere($or);
return $this->getBaseQueryResult($qb, $query);
}
}

View File

@@ -60,7 +60,7 @@ class CustomerRepository extends AbstractRepository
->join(Project::class, 'p')
->join(Customer::class, 'c')
->andWhere('t.activity = a.id')
->andWhere('a.project = p.id')
->andWhere('t.project = p.id')
->andWhere('p.customer = c.id')
->andWhere('c.id = :customer')
;

View File

@@ -10,6 +10,7 @@
namespace App\Repository;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Model\ProjectStatistic;
@@ -41,7 +42,7 @@ class ProjectRepository extends AbstractRepository
}
/**
* Retrieves statistics for one activity.
* Retrieves statistics for one project.
*
* @param Project $project
* @return ProjectStatistic
@@ -55,7 +56,7 @@ class ProjectRepository extends AbstractRepository
->addSelect('COUNT(DISTINCT(a.id)) as activityAmount')
->from(Activity::class, 'a')
->join(Timesheet::class, 't')
->where('a.project = :project')
->where('t.project = :project')
->andWhere('t.activity = a.id')
;
@@ -79,12 +80,14 @@ class ProjectRepository extends AbstractRepository
* Returns a query builder that is used for ProjectType and your own 'query_builder' option.
*
* @param Project|null $entity
* @return \Doctrine\ORM\QueryBuilder
* @param Customer|null $customer
* @return array|QueryBuilder|Pagerfanta
*/
public function builderForEntityType(Project $entity = null)
public function builderForEntityType(Project $entity = null, Customer $customer = null)
{
$query = new ProjectQuery();
$query->setHiddenEntity($entity);
$query->setCustomer($customer);
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
return $this->findByQuery($query);

View File

@@ -17,12 +17,58 @@ use App\Entity\Project;
class ActivityQuery extends ProjectQuery
{
/**
* @var Project
* @var Project|int
*/
protected $project;
/**
* @var bool
*/
protected $orderGlobalsFirst = false;
/**
* @var bool
*/
protected $globalsOnly = false;
/**
* @return Project
* @return bool
*/
public function isOrderGlobalsFirst(): bool
{
return $this->orderGlobalsFirst;
}
/**
* @param bool $orderGlobalsFirst
* @return ActivityQuery
*/
public function setOrderGlobalsFirst(bool $orderGlobalsFirst)
{
$this->orderGlobalsFirst = $orderGlobalsFirst;
return $this;
}
/**
* @return bool
*/
public function isGlobalsOnly(): bool
{
return $this->globalsOnly;
}
/**
* @param bool $globalsOnly
* @return ActivityQuery
*/
public function setGlobalsOnly(bool $globalsOnly)
{
$this->globalsOnly = $globalsOnly;
return $this;
}
/**
* @return Project|int
*/
public function getProject()
{
@@ -30,10 +76,10 @@ class ActivityQuery extends ProjectQuery
}
/**
* @param Project $project
* @param Project|int $project
* @return $this
*/
public function setProject(Project $project = null)
public function setProject($project = null)
{
$this->project = $project;

View File

@@ -140,7 +140,7 @@ class BaseQuery
}
/**
* @param string $resultType
* @param $resultType
* @return $this
* @throws \InvalidArgumentException
*/
@@ -166,7 +166,7 @@ class BaseQuery
}
/**
* @param object $hiddenEntity
* @param object|string $hiddenEntity
* @return BaseQuery
*/
public function setHiddenEntity($hiddenEntity)

View File

@@ -17,12 +17,12 @@ use App\Entity\Customer;
class ProjectQuery extends VisibilityQuery
{
/**
* @var Customer
* @var Customer|int
*/
protected $customer;
/**
* @return Customer
* @return Customer|int
*/
public function getCustomer()
{
@@ -30,10 +30,10 @@ class ProjectQuery extends VisibilityQuery
}
/**
* @param Customer $customer
* @param Customer|int $customer
* @return $this
*/
public function setCustomer(Customer $customer = null)
public function setCustomer($customer = null)
{
$this->customer = $customer;

View File

@@ -9,7 +9,6 @@
namespace App\Repository;
use App\Entity\Activity;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\Statistic\Month;
@@ -54,28 +53,6 @@ class TimesheetRepository extends AbstractRepository
return true;
}
/**
* @param User $user
* @param Activity $activity
* @return Timesheet
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function startRecording(User $user, Activity $activity)
{
$entry = new Timesheet();
$entry
->setBegin(new DateTime())
->setUser($user)
->setActivity($activity);
$entityManager = $this->getEntityManager();
$entityManager->persist($entry);
$entityManager->flush();
return $entry;
}
/**
* @param string $type
* @param DateTime|null $begin
@@ -223,17 +200,17 @@ class TimesheetRepository extends AbstractRepository
$qb->andWhere($qb->expr()->isNotNull('t.end'));
}
if (null !== $user) {
$qb->andWhere('t.user = :user')
->setParameter('user', $user);
}
$qb
->orderBy('year', 'DESC')
->addOrderBy('month', 'ASC')
->groupBy('year')
->addGroupBy('month');
if (null !== $user) {
$qb->andWhere('t.user = :user')
->setParameter('user', $user);
}
$years = [];
foreach ($qb->getQuery()->execute() as $statRow) {
$curYear = $statRow['year'];
@@ -267,7 +244,7 @@ class TimesheetRepository extends AbstractRepository
$qb->select('t', 'a', 'p', 'c')
->from(Timesheet::class, 't')
->join('t.activity', 'a')
->join('a.project', 'p')
->join('t.project', 'p')
->join('p.customer', 'c')
->where($qb->expr()->gt('t.begin', '0'))
->andWhere($qb->expr()->isNull('t.end'))
@@ -293,10 +270,10 @@ class TimesheetRepository extends AbstractRepository
$qb->select('t', 'a', 'p', 'c', 'u')
->from(Timesheet::class, 't')
->join('t.activity', 'a')
->join('t.user', 'u')
->join('a.project', 'p')
->join('p.customer', 'c')
->leftJoin('t.activity', 'a')
->leftJoin('t.user', 'u')
->leftJoin('t.project', 'p')
->leftJoin('p.customer', 'c')
->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
if (null !== $query->getUser()) {
@@ -323,7 +300,7 @@ class TimesheetRepository extends AbstractRepository
$qb->andWhere('t.activity = :activity')
->setParameter('activity', $query->getActivity());
} elseif (null !== $query->getProject()) {
$qb->andWhere('a.project = :project')
$qb->andWhere('t.project = :project')
->setParameter('project', $query->getProject());
} elseif (null !== $query->getCustomer()) {
$qb->andWhere('p.customer = :customer')

View File

@@ -74,7 +74,7 @@ class RateCalculator implements CalculatorInterface
return $activity->getHourlyRate();
}
$project = $activity->getProject();
$project = $record->getProject();
if (null !== $project) {
if (null !== $project->getHourlyRate()) {
return $project->getHourlyRate();
@@ -104,7 +104,7 @@ class RateCalculator implements CalculatorInterface
return $activity->getFixedRate();
}
$project = $activity->getProject();
$project = $record->getProject();
if (null !== $project) {
if (null !== $project->getFixedRate()) {
return $project->getFixedRate();

View File

@@ -44,10 +44,6 @@ class TimesheetVoter extends AbstractVoter
return false;
}
if ($subject instanceof Activity && self::START == $attribute) {
return true;
}
if (!$subject instanceof Timesheet) {
return false;
}
@@ -71,38 +67,18 @@ class TimesheetVoter extends AbstractVoter
switch ($attribute) {
case self::STOP:
if (!$subject instanceof Timesheet) {
return false;
}
return $this->canStop($subject, $user, $token);
case self::START:
if (!$subject instanceof Activity) {
return false;
}
return $this->canStart($subject, $user, $token);
case self::VIEW:
if (!$subject instanceof Timesheet) {
return false;
}
return $this->canView($subject, $user, $token);
case self::EDIT:
if (!$subject instanceof Timesheet) {
return false;
}
return $this->canEdit($subject, $user, $token);
case self::DELETE:
if (!$subject instanceof Timesheet) {
return false;
}
return $this->canDelete($subject, $user, $token);
}
@@ -122,23 +98,21 @@ class TimesheetVoter extends AbstractVoter
}
/**
* @param Activity $activity
* @param Timesheet $timesheet
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canStart(Activity $activity, User $user, TokenInterface $token)
protected function canStart(Timesheet $timesheet, User $user, TokenInterface $token)
{
// we could check the amount of active entries
// if a teamlead starts an entry for another user, check that this user is part of his team
if (!$activity->getVisible()) {
if (!$timesheet->getActivity()->getVisible() || !$timesheet->getProject()->getVisible()) {
return false;
}
if (!$activity->getProject()->getVisible()) {
return false;
}
if (!$activity->getProject()->getCustomer()->getVisible()) {
if (!$timesheet->getProject()->getCustomer()->getVisible()) {
return false;
}