added global activities (#259)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,7 +34,6 @@ class Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
127
src/Migrations/Version20181031220003.php
Normal file
127
src/Migrations/Version20181031220003.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user