added user-specific rates (#1455)

This commit is contained in:
Kevin Papst
2020-02-10 20:29:43 +01:00
committed by GitHub
parent 465d7166d4
commit 52c7437076
83 changed files with 2054 additions and 520 deletions

View File

@@ -11,17 +11,19 @@ namespace App\Controller;
use App\Configuration\FormConfiguration;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Event\ActivityMetaDefinitionEvent;
use App\Event\ActivityMetaDisplayEvent;
use App\Form\ActivityEditForm;
use App\Form\ActivityRateForm;
use App\Form\Toolbar\ActivityToolbarForm;
use App\Form\Type\ActivityType;
use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\ActivityQuery;
use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -37,7 +39,7 @@ use Symfony\Component\Routing\Annotation\Route;
* @Route(path="/admin/activity")
* @Security("is_granted('view_activity')")
*/
class ActivityController extends AbstractController
final class ActivityController extends AbstractController
{
/**
* @var ActivityRepository
@@ -50,7 +52,7 @@ class ActivityController extends AbstractController
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
private $dispatcher;
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
{
@@ -59,11 +61,6 @@ class ActivityController extends AbstractController
$this->dispatcher = $dispatcher;
}
protected function getRepository(): ActivityRepository
{
return $this->repository;
}
/**
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
@@ -88,7 +85,7 @@ class ActivityController extends AbstractController
}
/* @var $entries Pagerfanta */
$entries = $this->getRepository()->getPagerfantaForQuery($query);
$entries = $this->repository->getPagerfantaForQuery($query);
return $this->render('activity/index.html.twig', [
'entries' => $entries,
@@ -110,6 +107,85 @@ class ActivityController extends AbstractController
return $event->getFields();
}
/**
* @Route(path="/{id}/details", name="activity_details", methods={"GET", "POST"})
* @Security("is_granted('view', activity)")
*/
public function detailsAction(Activity $activity, ActivityRateRepository $rateRepository)
{
$event = new ActivityMetaDefinitionEvent($activity);
$this->dispatcher->dispatch($event);
$stats = null;
$rates = [];
if ($this->isGranted('edit', $activity)) {
$rates = $rateRepository->getRatesForActivity($activity);
}
if ($this->isGranted('budget', $activity)) {
$stats = $this->repository->getActivityStatistics($activity);
}
return $this->render('activity/details.html.twig', [
'activity' => $activity,
'stats' => $stats,
'rates' => $rates
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_activity_rate_delete", methods={"GET"})
* @Security("is_granted('edit', activity)")
*/
public function deleteRateAction(Activity $activity, ActivityRate $rate, ActivityRateRepository $repository)
{
if ($rate->getActivity() !== $activity) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid activity']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_activity_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', activity)")
*/
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository)
{
$rate = new ActivityRate();
$rate->setActivity($activity);
$form = $this->createForm(ActivityRateForm::class, $rate, [
'action' => $this->generateUrl('admin_activity_rate_add', ['id' => $activity->getId()]),
'method' => 'POST',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$repository->saveRate($rate);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
} catch (\Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->render('activity/rates.html.twig', [
'activity' => $activity,
'form' => $form->createView()
]);
}
/**
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
* @Route(path="/create/{project}", name="admin_activity_create_with_project", methods={"GET", "POST"})
@@ -129,25 +205,6 @@ class ActivityController extends AbstractController
return $this->renderActivityForm($activity, $request);
}
/**
* @Route(path="/{id}/budget", name="admin_activity_budget", methods={"GET"})
* @Security("is_granted('budget', activity)")
*
* @param Activity $activity
* @return Response
*/
public function budgetAction(Activity $activity)
{
$stats = $this->getRepository()->getActivityStatistics($activity);
// TODO sent event with stats
return $this->render('activity/budget.html.twig', [
'activity' => $activity,
'stats' => $stats,
]);
}
/**
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', activity)")
@@ -171,7 +228,7 @@ class ActivityController extends AbstractController
*/
public function deleteAction(Activity $activity, Request $request)
{
$stats = $this->getRepository()->getActivityStatistics($activity);
$stats = $this->repository->getActivityStatistics($activity);
$deleteForm = $this->createFormBuilder(null, [
'attr' => [
@@ -199,9 +256,9 @@ class ActivityController extends AbstractController
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
try {
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
$this->repository->deleteActivity($activity, $deleteForm->get('activity')->getData());
$this->flashSuccess('action.delete.success');
} catch (ORMException $ex) {
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
@@ -233,7 +290,7 @@ class ActivityController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->getRepository()->saveActivity($activity);
$this->repository->saveActivity($activity);
$this->flashSuccess('action.update.success');
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
@@ -245,7 +302,7 @@ class ActivityController extends AbstractController
} else {
return $this->redirectToRoute('admin_activity');
}
} catch (ORMException $ex) {
} catch (\Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}

View File

@@ -12,22 +12,25 @@ namespace App\Controller;
use App\Configuration\FormConfiguration;
use App\Entity\Customer;
use App\Entity\CustomerComment;
use App\Entity\CustomerRate;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Rate;
use App\Entity\Team;
use App\Event\CustomerMetaDefinitionEvent;
use App\Event\CustomerMetaDisplayEvent;
use App\Form\CustomerCommentForm;
use App\Form\CustomerEditForm;
use App\Form\CustomerRateForm;
use App\Form\CustomerTeamPermissionForm;
use App\Form\Toolbar\CustomerToolbarForm;
use App\Form\Type\CustomerType;
use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\CustomerQuery;
use App\Repository\Query\ProjectQuery;
use App\Repository\TeamRepository;
use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -257,7 +260,7 @@ final class CustomerController extends AbstractController
* @Route(path="/{id}/details", name="customer_details", methods={"GET", "POST"})
* @Security("is_granted('view', customer)")
*/
public function detailsAction(Customer $customer, TeamRepository $teamRepository)
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository)
{
$event = new CustomerMetaDefinitionEvent($customer);
$this->dispatcher->dispatch($event);
@@ -270,9 +273,13 @@ final class CustomerController extends AbstractController
$comments = null;
$teams = null;
$projects = null;
$rates = [];
if ($this->isGranted('edit', $customer) && $this->isGranted('create_team')) {
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
if ($this->isGranted('edit', $customer)) {
if ($this->isGranted('create_team')) {
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
}
$rates = $rateRepository->getRatesForCustomer($customer);
}
if (null !== $customer->getTimezone()) {
@@ -304,6 +311,59 @@ final class CustomerController extends AbstractController
'team' => $defaultTeam,
'teams' => $teams,
'now' => new \DateTime('now', $timezone),
'rates' => $rates
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_customer_rate_delete", methods={"GET"})
* @Security("is_granted('edit', customer)")
*/
public function deleteRateAction(Customer $customer, CustomerRate $rate, CustomerRateRepository $repository)
{
if ($rate->getCustomer() !== $customer) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid customer']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_customer_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', customer)")
*/
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository)
{
$rate = new CustomerRate();
$rate->setCustomer($customer);
$form = $this->createForm(CustomerRateForm::class, $rate, [
'action' => $this->generateUrl('admin_customer_rate_add', ['id' => $customer->getId()]),
'method' => 'POST',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$repository->saveRate($rate);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
} catch (\Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->render('customer/rates.html.twig', [
'customer' => $customer,
'form' => $form->createView()
]);
}
@@ -352,7 +412,7 @@ final class CustomerController extends AbstractController
try {
$this->repository->deleteCustomer($customer, $deleteForm->get('customer')->getData());
$this->flashSuccess('action.delete.success');
} catch (ORMException $ex) {
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
@@ -383,7 +443,7 @@ final class CustomerController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
} catch (ORMException $ex) {
} catch (\Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}

View File

@@ -14,15 +14,19 @@ use App\Entity\Customer;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\ProjectComment;
use App\Entity\ProjectRate;
use App\Entity\Rate;
use App\Entity\Team;
use App\Event\ProjectMetaDefinitionEvent;
use App\Event\ProjectMetaDisplayEvent;
use App\Form\ProjectCommentForm;
use App\Form\ProjectEditForm;
use App\Form\ProjectRateForm;
use App\Form\ProjectTeamPermissionForm;
use App\Form\Toolbar\ProjectToolbarForm;
use App\Form\Type\ProjectType;
use App\Repository\ActivityRepository;
use App\Repository\ProjectRateRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityQuery;
use App\Repository\Query\ProjectFormTypeQuery;
@@ -261,7 +265,7 @@ final class ProjectController extends AbstractController
* @Route(path="/{id}/details", name="project_details", methods={"GET", "POST"})
* @Security("is_granted('view', project)")
*/
public function detailsAction(Project $project, TeamRepository $teamRepository)
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository)
{
$event = new ProjectMetaDefinitionEvent($project);
$this->dispatcher->dispatch($event);
@@ -272,9 +276,13 @@ final class ProjectController extends AbstractController
$attachments = [];
$comments = null;
$teams = null;
$rates = [];
if ($this->isGranted('edit', $project) && $this->isGranted('create_team')) {
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
if ($this->isGranted('edit', $project)) {
if ($this->isGranted('create_team')) {
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
}
$rates = $rateRepository->getRatesForProject($project);
}
if ($this->isGranted('budget', $project)) {
@@ -301,6 +309,59 @@ final class ProjectController extends AbstractController
'stats' => $stats,
'team' => $defaultTeam,
'teams' => $teams,
'rates' => $rates
]);
}
/**
* @Route(path="/{id}/rate_delete/{rate}", name="admin_project_rate_delete", methods={"GET"})
* @Security("is_granted('edit', project)")
*/
public function deleteRateAction(Project $project, ProjectRate $rate, ProjectRateRepository $repository)
{
if ($rate->getProject() !== $project) {
$this->flashError('action.delete.error', ['%reason%' => 'Invalid project']);
} else {
try {
$repository->deleteRate($rate);
} catch (\Exception $ex) {
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
}
/**
* @Route(path="/{id}/rate", name="admin_project_rate_add", methods={"GET", "POST"})
* @Security("is_granted('edit', project)")
*/
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository)
{
$rate = new ProjectRate();
$rate->setProject($project);
$form = $this->createForm(ProjectRateForm::class, $rate, [
'action' => $this->generateUrl('admin_project_rate_add', ['id' => $project->getId()]),
'method' => 'POST',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$repository->saveRate($rate);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
} catch (\Exception $ex) {
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
}
}
return $this->render('project/rates.html.twig', [
'project' => $project,
'form' => $form->createView()
]);
}

View File

@@ -74,7 +74,6 @@ class Activity implements EntityWithMetaFields
private $visible = true;
// keep the trait include exactly here, for placing the column at the correct position
use RatesTrait;
use ColorTrait;
use BudgetTrait;
@@ -138,6 +137,11 @@ class Activity implements EntityWithMetaFields
return $this;
}
public function isGlobal(): bool
{
return $this->project === null;
}
public function isVisible(): bool
{
return $this->visible;

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Table(name="kimai2_activities_rates",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"user_id", "activity_id"}),
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository")
* @UniqueEntity({"user", "activity"}, ignoreNull=false)
*/
class ActivityRate implements RateInterface
{
use Rate;
/**
* @var Activity
*
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $activity;
public function setActivity(?Activity $activity): ActivityRate
{
$this->activity = $activity;
return $this;
}
public function getActivity(): ?Activity
{
return $this->activity;
}
public function getScore(): int
{
return 5;
}
}

View File

@@ -174,7 +174,6 @@ class Customer implements EntityWithMetaFields
private $timezone;
// keep the trait include exactly here, for placing the column at the correct position
use RatesTrait;
use ColorTrait;
use BudgetTrait;

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Table(name="kimai2_customers_rates",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"user_id", "customer_id"}),
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository")
* @UniqueEntity({"user", "customer"}, ignoreNull=false)
*/
class CustomerRate implements RateInterface
{
use Rate;
/**
* @var Customer
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $customer;
public function setCustomer(?Customer $customer): CustomerRate
{
$this->customer = $customer;
return $this;
}
public function getCustomer(): ?Customer
{
return $this->customer;
}
public function getScore(): int
{
return 1;
}
}

View File

@@ -105,7 +105,6 @@ class Project implements EntityWithMetaFields
private $visible = true;
// keep the trait include exactly here, for placing the column at the correct position
use RatesTrait;
use ColorTrait;
use BudgetTrait;

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Table(name="kimai2_projects_rates",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"user_id", "project_id"}),
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository")
* @UniqueEntity({"user", "project"}, ignoreNull=false)
*/
class ProjectRate implements RateInterface
{
use Rate;
/**
* @var Project
*
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull
*/
private $project;
public function setProject(?Project $project): ProjectRate
{
$this->project = $project;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function getScore(): int
{
return 3;
}
}

92
src/Entity/Rate.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
trait Rate
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
*/
private $user;
/**
* @var float
*
* @ORM\Column(name="rate", type="float", nullable=false)
* @Assert\GreaterThanOrEqual(0)
*/
private $rate = 0.00;
/**
* @var bool
*
* @ORM\Column(name="fixed", type="boolean", nullable=false)
* @Assert\NotNull()
*/
private $isFixed = false;
/**
* Get entry id, returns null for new entities which were not persisted.
*
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setRate(float $rate): self
{
$this->rate = $rate;
return $this;
}
public function getRate(): float
{
return $this->rate;
}
public function isFixed(): bool
{
return $this->isFixed;
}
public function setIsFixed(bool $isFixed): self
{
$this->isFixed = $isFixed;
return $this;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
interface RateInterface
{
public function getUser(): ?User;
public function getRate(): float;
public function isFixed(): bool;
public function getScore(): int;
}

View File

@@ -1,70 +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\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
trait RatesTrait
{
/**
* @var float
*
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $fixedRate = null;
/**
* @var float
*
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $hourlyRate = null;
/**
* @return float
*/
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
/**
* @param float $fixedRate
* @return self
*/
public function setFixedRate(?float $fixedRate)
{
$this->fixedRate = $fixedRate;
return $this;
}
/**
* @return float
*/
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
/**
* @param float $hourlyRate
* @return self
*/
public function setHourlyRate(?float $hourlyRate)
{
$this->hourlyRate = $hourlyRate;
return $this;
}
}

View File

@@ -130,8 +130,21 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
*/
private $rate = 0.00;
// keep the trait include exactly here, for placing the column at the correct position
use RatesTrait;
/**
* @var float
*
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $fixedRate = null;
/**
* @var float
*
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $hourlyRate = null;
/**
* @var bool
@@ -452,6 +465,30 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
return self::CATEGORY_WORK;
}
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
public function setFixedRate(?float $fixedRate): Timesheet
{
$this->fixedRate = $fixedRate;
return $this;
}
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
public function setHourlyRate(?float $hourlyRate): Timesheet
{
$this->hourlyRate = $hourlyRate;
return $this;
}
/**
* @internal only here for symfony forms
* @return Collection|MetaTableTypeInterface[]

View File

@@ -95,19 +95,19 @@ final class MenuSubscriber implements EventSubscriberInterface
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'admin_customer_budget', 'admin_customer_edit', 'admin_customer_delete']);
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
$menu->addChild($customers);
}
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $this->getIcon('project'));
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'admin_project_budget', 'admin_project_edit', 'admin_project_delete']);
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
$menu->addChild($projects);
}
if ($auth->isGranted('view_activity')) {
$activities = new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], $this->getIcon('activity'));
$activities->setChildRoutes(['admin_activity_create', 'admin_activity_budget', 'admin_activity_edit', 'admin_activity_delete']);
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
$menu->addChild($activities);
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\ActivityRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityRateForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currency = null;
if ($options['data']) {
/** @var ActivityRate $rate */
$rate = $options['data'];
if (null !== $rate->getActivity() && !$rate->getActivity()->isGlobal()) {
$currency = $rate->getActivity()->getProject()->getCustomer()->getCurrency();
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ActivityRate::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'expand_users' => true,
'csrf_token_id' => 'admin_customer_rate_edit',
'attr' => [
'data-form-event' => 'kimai.activityUpdate'
],
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\CustomerRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerRateForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currency = null;
if ($options['data']) {
/** @var CustomerRate $rate */
$rate = $options['data'];
if (null !== $customer = $rate->getCustomer()) {
$currency = $customer->getCurrency();
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CustomerRate::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'expand_users' => true,
'csrf_token_id' => 'admin_customer_rate_edit',
'attr' => [
'data-form-event' => 'kimai.customerUpdate'
],
]);
}
}

View File

@@ -11,8 +11,6 @@ namespace App\Form;
use App\Form\Type\ColorPickerType;
use App\Form\Type\DurationType;
use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType;
use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -26,12 +24,6 @@ trait EntityFormTrait
$currency = $options['currency'];
$builder
->add('color', ColorPickerType::class)
->add('fixedRate', FixedRateType::class, [
'currency' => $currency,
])
->add('hourlyRate', HourlyRateType::class, [
'currency' => $currency,
])
;
if ($options['include_budget']) {

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\ProjectRate;
use App\Entity\Rate;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectRateForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currency = null;
if ($options['data']) {
/** @var ProjectRate $rate */
$rate = $options['data'];
if (null !== $customer = $rate->getProject()->getCustomer()) {
$currency = $customer->getCurrency();
}
}
$builder
->add('user', UserType::class, [
'required' => false,
])
->add('rate', MoneyType::class, [
'label' => 'label.rate',
'attr' => [
'autofocus' => 'autofocus'
],
'currency' => $currency,
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate'
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ProjectRate::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'expand_users' => true,
'csrf_token_id' => 'admin_project_rate_edit',
'attr' => [
'data-form-event' => 'kimai.projectUpdate'
],
]);
}
}

View File

@@ -29,12 +29,6 @@ class InvoiceModelActivityHydrator implements InvoiceModelHydrator
'activity.id' => $activity->getId(),
'activity.name' => $activity->getName(),
'activity.comment' => $activity->getComment(),
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
'activity.fixed_rate_plain' => $activity->getFixedRate(),
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
];
foreach ($activity->getVisibleMetaFields() as $metaField) {

View File

@@ -36,12 +36,6 @@ class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage(),
'customer.comment' => $customer->getComment(),
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
'customer.fixed_rate_plain' => $customer->getFixedRate(),
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
];
foreach ($customer->getVisibleMetaFields() as $metaField) {

View File

@@ -33,12 +33,6 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
'project.start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
'project.end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
'project.fixed_rate_plain' => $project->getFixedRate(),
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
'project.hourly_rate_plain' => $project->getHourlyRate(),
'project.budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
'project.budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
'project.budget_money_plain' => $project->getBudget(),

View File

@@ -14,6 +14,11 @@ namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Adds language and decimal_duration column to invoice template table
*
* @version 1.8
*/
final class Version20200204124425 extends AbstractMigration
{
public function getDescription(): string

View File

@@ -0,0 +1,71 @@
<?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;
/**
* Adds the rate table, which allows to define user specific rate rules
*
* @version 1.8
*/
final class Version20200205115243 extends AbstractMigration
{
public function getDescription(): string
{
return 'Adds the rate table, which allows to define user specific rate rules';
}
public function up(Schema $schema): void
{
$customerRates = $schema->createTable('kimai2_customers_rates');
$customerRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$customerRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
$customerRates->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => false]);
$customerRates->addColumn('rate', 'float', ['notnull' => true]);
$customerRates->addColumn('fixed', 'boolean', ['notnull' => true]);
$customerRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AECA76ED395');
$customerRates->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_82AB0AEC9395C3F3');
$customerRates->addUniqueIndex(['user_id', 'customer_id'], 'UNIQ_82AB0AECA76ED3959395C3F3');
$customerRates->setPrimaryKey(['id']);
$projectRates = $schema->createTable('kimai2_projects_rates');
$projectRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$projectRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
$projectRates->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => false]);
$projectRates->addColumn('rate', 'float', ['notnull' => true]);
$projectRates->addColumn('fixed', 'boolean', ['notnull' => true]);
$projectRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55A76ED395');
$projectRates->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_41535D55166D1F9C');
$projectRates->addUniqueIndex(['user_id', 'project_id'], 'UNIQ_41535D55A76ED395166D1F9C');
$projectRates->setPrimaryKey(['id']);
$activityRates = $schema->createTable('kimai2_activities_rates');
$activityRates->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$activityRates->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => false]);
$activityRates->addColumn('activity_id', 'integer', ['length' => 11, 'notnull' => false]);
$activityRates->addColumn('rate', 'float', ['notnull' => true]);
$activityRates->addColumn('fixed', 'boolean', ['notnull' => true]);
$activityRates->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BEA76ED395');
$activityRates->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4A7F11BE81C06096');
$activityRates->addUniqueIndex(['user_id', 'activity_id'], 'UNIQ_4A7F11BEA76ED39581C06096');
$activityRates->setPrimaryKey(['id']);
}
public function down(Schema $schema): void
{
$schema->dropTable('kimai2_activities_rates');
$schema->dropTable('kimai2_projects_rates');
$schema->dropTable('kimai2_customers_rates');
}
}

View File

@@ -0,0 +1,72 @@
<?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;
/**
* Migrates the data from entity tables to user specific rate tables
*
* @version 1.8
*/
final class Version20200205115244 extends AbstractMigration
{
public function getDescription(): string
{
return 'Migrates the data from entity tables to user specific rate tables';
}
public function up(Schema $schema): void
{
$migrates = [
['kimai2_activities', 'activity_id', 'kimai2_activities_rates'],
['kimai2_projects', 'project_id', 'kimai2_projects_rates'],
['kimai2_customers', 'customer_id', 'kimai2_customers_rates'],
];
foreach ($migrates as $migrateOpts) {
$tableName = $migrateOpts[0];
$fieldName = $migrateOpts[1];
$targetTable = $migrateOpts[2];
$rules = $this->connection->prepare(
'SELECT id, fixed_rate, hourly_rate FROM ' . $tableName . ' WHERE fixed_rate IS NOT NULL OR hourly_rate IS NOT NULL'
);
$rules->execute();
foreach ($rules->fetchAll() as $rateRule) {
$isFixed = $rateRule['fixed_rate'] !== null;
$rate = $rateRule['fixed_rate'] ?? $rateRule['hourly_rate'];
$params = ['user_id' => null, $fieldName => $rateRule['id'], 'rate' => $rate, 'fixed' => $isFixed];
$this->connection->insert($targetTable, $params, ['fixed' => \PDO::PARAM_BOOL]);
}
}
$schema->getTable('kimai2_customers')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
$schema->getTable('kimai2_projects')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
$schema->getTable('kimai2_activities')->dropColumn('fixed_rate')->dropColumn('hourly_rate');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE kimai2_customers ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE kimai2_projects ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE kimai2_activities ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
}
}

View File

@@ -0,0 +1,62 @@
<?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\Repository;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException;
class ActivityRateRepository extends EntityRepository
{
public function saveRate(ActivityRate $rate)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($rate);
$entityManager->flush();
}
public function deleteRate(ActivityRate $rate)
{
$em = $this->getEntityManager();
$em->beginTransaction();
try {
$em->remove($rate);
$em->flush();
$em->commit();
} catch (ORMException $ex) {
$em->rollback();
throw $ex;
}
}
/**
* @param Activity $activity
* @return ActivityRate[]
*/
public function getRatesForActivity(Activity $activity): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, a')
->from(ActivityRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.activity', 'a')
->andWhere(
$qb->expr()->eq('r.activity', ':activity')
)
->addOrderBy('u.alias')
->setParameter('activity', $activity)
;
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,62 @@
<?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\Repository;
use App\Entity\Customer;
use App\Entity\CustomerRate;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException;
class CustomerRateRepository extends EntityRepository
{
public function saveRate(CustomerRate $rate)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($rate);
$entityManager->flush();
}
public function deleteRate(CustomerRate $rate)
{
$em = $this->getEntityManager();
$em->beginTransaction();
try {
$em->remove($rate);
$em->flush();
$em->commit();
} catch (ORMException $ex) {
$em->rollback();
throw $ex;
}
}
/**
* @param Customer $customer
* @return CustomerRate[]
*/
public function getRatesForCustomer(Customer $customer): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, c')
->from(CustomerRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.customer', 'c')
->andWhere(
$qb->expr()->eq('r.customer', ':customer')
)
->addOrderBy('u.alias')
->setParameter('customer', $customer)
;
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,62 @@
<?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\Repository;
use App\Entity\Project;
use App\Entity\ProjectRate;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException;
class ProjectRateRepository extends EntityRepository
{
public function saveRate(ProjectRate $rate)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($rate);
$entityManager->flush();
}
public function deleteRate(ProjectRate $rate)
{
$em = $this->getEntityManager();
$em->beginTransaction();
try {
$em->remove($rate);
$em->flush();
$em->commit();
} catch (ORMException $ex) {
$em->rollback();
throw $ex;
}
}
/**
* @param Project $project
* @return ProjectRate[]
*/
public function getRatesForProject(Project $project): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, p')
->from(ProjectRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.project', 'p')
->andWhere(
$qb->expr()->eq('r.project', ':project')
)
->addOrderBy('u.alias')
->setParameter('project', $project)
;
return $qb->getQuery()->getResult();
}
}

View File

@@ -10,6 +10,10 @@
namespace App\Repository;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\CustomerRate;
use App\Entity\ProjectRate;
use App\Entity\RateInterface;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\Statistic\Day;
@@ -834,4 +838,73 @@ class TimesheetRepository extends EntityRepository
return $field;
}
/**
* @param Timesheet $timesheet
* @return RateInterface[]
*/
public function findMatchingRates(Timesheet $timesheet): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, a')
->from(ActivityRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.activity', 'a')
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('r.user', ':user'),
$qb->expr()->isNull('r.user')
),
$qb->expr()->orX(
$qb->expr()->eq('r.activity', ':activity'),
$qb->expr()->isNull('r.activity')
)
)
->setParameter('user', $timesheet->getUser())
->setParameter('activity', $timesheet->getActivity())
;
$results = $qb->getQuery()->getResult();
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, p')
->from(ProjectRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.project', 'p')
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('r.user', ':user'),
$qb->expr()->isNull('r.user')
),
$qb->expr()->orX(
$qb->expr()->eq('r.project', ':project'),
$qb->expr()->isNull('r.project')
)
)
->setParameter('user', $timesheet->getUser())
->setParameter('project', $timesheet->getProject())
;
$results = array_merge($results, $qb->getQuery()->getResult());
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('r, u, c')
->from(CustomerRate::class, 'r')
->leftJoin('r.user', 'u')
->leftJoin('r.customer', 'c')
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('r.user', ':user'),
$qb->expr()->isNull('r.user')
),
$qb->expr()->orX(
$qb->expr()->eq('r.customer', ':customer'),
$qb->expr()->isNull('r.customer')
)
)
->setParameter('user', $timesheet->getUser())
->setParameter('customer', $timesheet->getProject()->getCustomer())
;
$results = array_merge($results, $qb->getQuery()->getResult());
return $results;
}
}

View File

@@ -9,8 +9,11 @@
namespace App\Timesheet\Calculator;
use App\Entity\Rate;
use App\Entity\RateInterface;
use App\Entity\Timesheet;
use App\Entity\UserPreference;
use App\Repository\TimesheetRepository;
use App\Timesheet\CalculatorInterface;
use App\Timesheet\Util;
@@ -22,15 +25,16 @@ class RateCalculator implements CalculatorInterface
/**
* @var array
*/
protected $rates;
private $rates;
/**
* RateCalculator constructor.
* @param array $rates
* @var TimesheetRepository
*/
public function __construct(array $rates)
private $repository;
public function __construct(array $rates, TimesheetRepository $repository)
{
$this->rates = $rates;
$this->repository = $repository;
}
/**
@@ -44,7 +48,21 @@ class RateCalculator implements CalculatorInterface
return;
}
$fixedRate = $this->findFixedRate($record);
$fixedRate = $record->getFixedRate();
$hourlyRate = $record->getHourlyRate();
if (null === $fixedRate && null === $hourlyRate) {
$rate = $this->getBestFittingRate($record);
if (null !== $rate) {
if ($rate->isFixed()) {
$fixedRate = $rate->getRate();
} else {
$hourlyRate = $rate->getRate();
}
}
}
if (null !== $fixedRate) {
$record->setRate($fixedRate);
$record->setFixedRate($fixedRate);
@@ -52,74 +70,40 @@ class RateCalculator implements CalculatorInterface
return;
}
$hourlyRate = $this->findHourlyRate($record);
if (null === $hourlyRate) {
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
}
$factor = $this->getRateFactor($record);
$hourlyRate = (float) ($hourlyRate * $factor);
$rate = 0;
$totalRate = 0;
if (null !== $record->getDuration()) {
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
$totalRate = Util::calculateRate($hourlyRate, $record->getDuration());
}
$record->setHourlyRate($hourlyRate);
$record->setRate($rate);
$record->setRate($totalRate);
}
/**
* @param Timesheet $record
* @return float
*/
protected function findHourlyRate(Timesheet $record)
private function getBestFittingRate(Timesheet $timesheet): ?RateInterface
{
if (null !== $record->getHourlyRate()) {
return $record->getHourlyRate();
}
$activity = $record->getActivity();
if (null !== $activity->getHourlyRate()) {
return $activity->getHourlyRate();
}
$project = $record->getProject();
if (null !== $project) {
if (null !== $project->getHourlyRate()) {
return $project->getHourlyRate();
$rates = $this->repository->findMatchingRates($timesheet);
/** @var RateInterface[] $sorted */
$sorted = [];
foreach ($rates as $rate) {
$score = $rate->getScore();
if (null !== $rate->getUser() && $timesheet->getUser() === $rate->getUser()) {
++$score;
}
$customer = $project->getCustomer();
if (null !== $customer->getHourlyRate()) {
return $customer->getHourlyRate();
}
$sorted[$score] = $rate;
}
return (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
}
if (!empty($sorted)) {
ksort($sorted);
/**
* @param Timesheet $record
* @return float|null
*/
protected function findFixedRate(Timesheet $record)
{
if (null !== $record->getFixedRate()) {
return $record->getFixedRate();
}
$activity = $record->getActivity();
if (null !== $activity->getFixedRate()) {
return $activity->getFixedRate();
}
$project = $record->getProject();
if (null !== $project) {
if (null !== $project->getFixedRate()) {
return $project->getFixedRate();
}
$customer = $project->getCustomer();
if (null !== $customer->getFixedRate()) {
return $customer->getFixedRate();
}
return end($sorted);
}
return null;