Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -9,8 +9,20 @@
|
||||
|
||||
namespace App\Reporting\CustomerMonthlyProjects;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Reporting\AbstractUserList;
|
||||
|
||||
final class CustomerMonthlyProjects extends AbstractUserList
|
||||
{
|
||||
private ?Customer $customer = null;
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(?Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,31 +9,32 @@
|
||||
|
||||
namespace App\Reporting\CustomerMonthlyProjects;
|
||||
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\MonthPickerType;
|
||||
use App\Form\Type\ReportSumType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerMonthlyProjectsForm extends AbstractType
|
||||
final class CustomerMonthlyProjectsForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'width' => false,
|
||||
]);
|
||||
|
||||
$builder->add('date', MonthPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
]);
|
||||
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerMonthlyProjects::class,
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<?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\Reporting\CustomerMonthlyProjects;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
|
||||
final class CustomerMonthlyProjectsRepository
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository, private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User[] $users
|
||||
* @param Customer|null $customer
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
public function getGroupedByCustomerProjectActivityUser(DateTime $begin, DateTime $end, array $users, ?Customer $customer): array
|
||||
{
|
||||
$stats = [];
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('IDENTITY(t.project) as project')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('user', $users)
|
||||
->groupBy('project')
|
||||
->addGroupBy('activity')
|
||||
->addGroupBy('user')
|
||||
;
|
||||
|
||||
if ($customer !== null) {
|
||||
$qb2 = $this->entityManager->createQueryBuilder();
|
||||
|
||||
$qb2->select('p.id')
|
||||
->from(Project::class, 'p')
|
||||
->andWhere($qb2->expr()->eq('p.customer', ':customer'))
|
||||
->setParameter('customer', $customer->getId())
|
||||
;
|
||||
$projectIds = $qb2->getQuery()->getSingleColumnResult();
|
||||
|
||||
$qb
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
}
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$projectIds = [];
|
||||
$activityIds = [];
|
||||
$userIds = [];
|
||||
|
||||
foreach ($results as $row) {
|
||||
$projectId = $row['project'];
|
||||
$activityId = $row['activity'];
|
||||
$userId = $row['user'];
|
||||
|
||||
$projectIds[$projectId] = $projectId;
|
||||
$activityIds[$activityId] = $activityId;
|
||||
$userIds[$userId] = $userId;
|
||||
|
||||
if (!isset($stats[$projectId])) {
|
||||
$stats[$projectId] = [
|
||||
'id' => $projectId,
|
||||
'customer' => '',
|
||||
'customer_id' => null,
|
||||
'name' => null,
|
||||
'activities' => [],
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
'max_users' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['internalRate'] += (int) $row['internalRate'];
|
||||
|
||||
if (!isset($stats[$projectId]['activities'][$activityId])) {
|
||||
$stats[$projectId]['activities'][$activityId] = [
|
||||
'id' => $activityId,
|
||||
'name' => null,
|
||||
'users' => [],
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['activities'][$activityId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['activities'][$activityId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['activities'][$activityId]['internalRate'] += (int) $row['internalRate'];
|
||||
|
||||
if (!isset($stats[$projectId]['activities'][$activityId]['users'][$userId])) {
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId] = [
|
||||
'id' => $userId,
|
||||
'name' => null,
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['internalRate'] += (int) $row['internalRate'];
|
||||
}
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('a.id, a.name')
|
||||
->from(Activity::class, 'a', 'a.id')
|
||||
->where($qb->expr()->in('a.id', ':id'))
|
||||
->setParameter('id', array_values($activityIds))
|
||||
;
|
||||
$activities = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('p.id, p.name, c.id as customer_id, c.name as customer, c.currency')
|
||||
->from(Project::class, 'p', 'p.id')
|
||||
->leftJoin(Customer::class, 'c', Join::WITH, 'c.id = p.customer')
|
||||
->where($qb->expr()->in('p.id', ':id'))
|
||||
->setParameter('id', array_values($projectIds))
|
||||
;
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u', 'u.id')
|
||||
->where($qb->expr()->in('u.id', ':id'))
|
||||
->setParameter('id', array_values($userIds))
|
||||
;
|
||||
$users = $qb->getQuery()->getResult();
|
||||
|
||||
foreach (array_keys($stats) as $pid) {
|
||||
$stats[$pid]['name'] = $projects[$pid]['name'];
|
||||
$stats[$pid]['customer'] = $projects[$pid]['customer'];
|
||||
$stats[$pid]['customer_id'] = $projects[$pid]['customer_id'];
|
||||
foreach (array_keys($stats[$pid]['activities']) as $aid) {
|
||||
$stats[$pid]['activities'][$aid]['name'] = $activities[$aid]['name'];
|
||||
foreach (array_keys($stats[$pid]['activities'][$aid]['users']) as $uid) {
|
||||
$stats[$pid]['activities'][$aid]['users'][$uid]['name'] = $users[$uid]->getDisplayName();
|
||||
}
|
||||
$stats[$pid]['max_users'] = max($stats[$pid]['max_users'], \count($stats[$pid]['activities'][$aid]['users']));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'stats' => $stats,
|
||||
'projects' => $projects,
|
||||
'activities' => $activities,
|
||||
'users' => $users,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ use App\Entity\User;
|
||||
|
||||
abstract class DateByUser extends AbstractUserList
|
||||
{
|
||||
private $user;
|
||||
private ?User $user = null;
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
public function setUser(?User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\MonthByUser;
|
||||
|
||||
use App\Reporting\DateByUser;
|
||||
|
||||
final class MonthByUser extends DateByUser
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\MonthByUser;
|
||||
|
||||
use App\Form\Type\MonthPickerType;
|
||||
use App\Form\Type\ReportSumType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class MonthByUserForm extends AbstractType
|
||||
final class MonthByUserForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', MonthPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -45,10 +32,7 @@ class MonthByUserForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MonthByUser::class,
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\MonthlyUserList;
|
||||
|
||||
use App\Reporting\AbstractUserList;
|
||||
|
||||
final class MonthlyUserList extends AbstractUserList
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\MonthlyUserList;
|
||||
|
||||
use App\Form\Type\MonthPickerType;
|
||||
use App\Form\Type\ReportSumType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class MonthlyUserListForm extends AbstractType
|
||||
final class MonthlyUserListForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', MonthPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -46,10 +33,7 @@ class MonthlyUserListForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MonthlyUserList::class,
|
||||
@@ -17,26 +17,12 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectDateRangeForm extends AbstractType
|
||||
final class ProjectDateRangeForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
]);
|
||||
|
||||
@@ -48,26 +34,24 @@ class ProjectDateRangeForm extends AbstractType
|
||||
|
||||
$builder->add('includeNoWork', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoWork',
|
||||
'label' => 'includeNoWork',
|
||||
]);
|
||||
|
||||
$builder->add('budgetType', ChoiceType::class, [
|
||||
'required' => true,
|
||||
'placeholder' => null,
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => [
|
||||
'label.budgetIndependent' => null,
|
||||
'label.includeNoBudget' => 'none',
|
||||
'label.includeBudgetType_full' => 'full',
|
||||
'label.includeBudgetType_month' => 'month',
|
||||
'all' => null,
|
||||
'includeNoBudget' => 'none',
|
||||
'includeBudgetType_full' => 'full',
|
||||
'includeBudgetType_month' => 'month',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectDateRangeQuery::class,
|
||||
|
||||
@@ -14,26 +14,11 @@ use App\Entity\User;
|
||||
|
||||
final class ProjectDateRangeQuery
|
||||
{
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $month;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoWork = false;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $budgetType = null;
|
||||
private \DateTime $month;
|
||||
private ?User $user;
|
||||
private ?Customer $customer = null;
|
||||
private bool $includeNoWork = false;
|
||||
private ?string $budgetType = null;
|
||||
|
||||
public function __construct(\DateTime $month, User $user)
|
||||
{
|
||||
|
||||
@@ -12,38 +12,43 @@ namespace App\Reporting\ProjectDetails;
|
||||
use App\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectDetailsForm extends AbstractType
|
||||
final class ProjectDetailsForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('project', ProjectType::class, [
|
||||
$projectOptions = [
|
||||
'ignore_date' => true,
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
'join_customer' => true,
|
||||
]);
|
||||
];
|
||||
|
||||
$builder->add('project', ProjectType::class, $projectOptions);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($projectOptions) {
|
||||
$data = $event->getData();
|
||||
if (isset($data['project']) && !empty($data['project'])) {
|
||||
$projectId = $data['project'];
|
||||
$projects = [];
|
||||
if (\is_int($projectId) || \is_string($projectId)) {
|
||||
$projects = [$projectId];
|
||||
}
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, array_merge($projectOptions, [
|
||||
'projects' => $projects
|
||||
]));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectDetailsQuery::class,
|
||||
|
||||
@@ -19,34 +19,29 @@ use App\Model\UserStatistic;
|
||||
|
||||
final class ProjectDetailsModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var Year[]
|
||||
*/
|
||||
private $years = [];
|
||||
private array $years = [];
|
||||
/**
|
||||
* @var array<string, array<ActivityStatistic>>
|
||||
*/
|
||||
private $yearlyActivities = [];
|
||||
private array $yearlyActivities = [];
|
||||
/**
|
||||
* @var array<string, array<int, UserYear>>
|
||||
*/
|
||||
private $usersMonthly = [];
|
||||
private array $usersMonthly = [];
|
||||
/**
|
||||
* @var ActivityStatistic[]
|
||||
*/
|
||||
private $activities = [];
|
||||
private array $activities = [];
|
||||
/**
|
||||
* @var BudgetStatisticModel
|
||||
*/
|
||||
private $budgetStatisticModel;
|
||||
private ?BudgetStatisticModel $budgetStatisticModel = null;
|
||||
|
||||
public function __construct(Project $project)
|
||||
public function __construct(private Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
@@ -76,7 +71,7 @@ final class ProjectDetailsModel
|
||||
* @param string $year
|
||||
* @return ActivityStatistic[]
|
||||
*/
|
||||
public function getYearActivities(string $year): ?array
|
||||
public function getYearActivities(string $year): array
|
||||
{
|
||||
if (!\array_key_exists($year, $this->yearlyActivities)) {
|
||||
return [];
|
||||
@@ -99,8 +94,8 @@ final class ProjectDetailsModel
|
||||
$userStat = new UserStatistic($userYear->getUser());
|
||||
$users[$id] = $userStat;
|
||||
}
|
||||
$userStat->setRecordDuration($userStat->getRecordDuration() + $userYear->getDuration());
|
||||
$userStat->setRecordRate($userStat->getRecordRate() + $userYear->getRate());
|
||||
$userStat->setDuration($userStat->getDuration() + $userYear->getDuration());
|
||||
$userStat->setRate($userStat->getRate() + $userYear->getRate());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,10 @@ use DateTime;
|
||||
|
||||
final class ProjectDetailsQuery
|
||||
{
|
||||
/**
|
||||
* @var Project|null
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $today;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
private ?Project $project = null;
|
||||
|
||||
public function __construct(DateTime $today, User $user)
|
||||
public function __construct(private DateTime $today, private User $user)
|
||||
{
|
||||
$this->today = $today;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getToday(): DateTime
|
||||
|
||||
@@ -9,39 +9,23 @@
|
||||
|
||||
namespace App\Reporting\ProjectInactive;
|
||||
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectInactiveForm extends AbstractType
|
||||
final class ProjectInactiveForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('lastChange', DateTimePickerType::class, [
|
||||
'label' => 'label.last_record_before',
|
||||
$builder->add('lastChange', DatePickerType::class, [
|
||||
'label' => 'last_record_before',
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectInactiveQuery::class,
|
||||
|
||||
@@ -14,14 +14,8 @@ use DateTime;
|
||||
|
||||
final class ProjectInactiveQuery
|
||||
{
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $lastChange;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
private DateTime $lastChange;
|
||||
private User $user;
|
||||
|
||||
public function __construct(DateTime $lastChange, User $user)
|
||||
{
|
||||
|
||||
@@ -12,45 +12,36 @@ namespace App\Reporting\ProjectView;
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectViewForm extends AbstractType
|
||||
final class ProjectViewForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
]);
|
||||
$builder->add('includeNoBudget', CheckboxType::class, [
|
||||
$builder->add('budgetType', ChoiceType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoBudget',
|
||||
'placeholder' => null,
|
||||
'expanded' => true,
|
||||
'choices' => [
|
||||
'all' => null,
|
||||
'includeWithBudget' => true,
|
||||
'includeNoBudget' => false
|
||||
]
|
||||
]);
|
||||
$builder->add('includeNoWork', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoWork',
|
||||
'label' => 'includeNoWork',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectViewQuery::class,
|
||||
|
||||
@@ -15,31 +15,23 @@ use DateTime;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
private $project;
|
||||
private $timesheetCounter = 0;
|
||||
private $durationDay = 0;
|
||||
private $durationWeek = 0;
|
||||
private $durationMonth = 0;
|
||||
private $durationTotal = 0;
|
||||
private $rateTotal = 0.00;
|
||||
private $notExportedDuration = 0;
|
||||
private $notExportedRate = 0.00;
|
||||
private $notBilledDuration = 0;
|
||||
private $notBilledRate = 0.00;
|
||||
private $billableDuration = 0;
|
||||
private $billableRate = 0.00;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
private $lastRecord;
|
||||
/**
|
||||
* @var BudgetStatisticModelInterface
|
||||
*/
|
||||
private $budgetStatisticModel;
|
||||
private int $timesheetCounter = 0;
|
||||
private int $durationDay = 0;
|
||||
private int $durationWeek = 0;
|
||||
private int $durationMonth = 0;
|
||||
private int $durationTotal = 0;
|
||||
private float $rateTotal = 0.00;
|
||||
private int $notExportedDuration = 0;
|
||||
private float $notExportedRate = 0.00;
|
||||
private int $notBilledDuration = 0;
|
||||
private float $notBilledRate = 0.00;
|
||||
private int $billableDuration = 0;
|
||||
private float $billableRate = 0.00;
|
||||
private ?DateTime $lastRecord = null;
|
||||
private ?BudgetStatisticModelInterface $budgetStatisticModel = null;
|
||||
|
||||
public function __construct(Project $project)
|
||||
public function __construct(private Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
|
||||
@@ -15,31 +15,12 @@ use DateTime;
|
||||
|
||||
final class ProjectViewQuery
|
||||
{
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $today;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoBudget = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoWork = false;
|
||||
private ?Customer $customer = null;
|
||||
private bool $includeNoWork = false;
|
||||
private ?bool $budgetType = true;
|
||||
|
||||
public function __construct(DateTime $today, User $user)
|
||||
public function __construct(private DateTime $today, private User $user)
|
||||
{
|
||||
$this->today = $today;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
@@ -47,14 +28,27 @@ final class ProjectViewQuery
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function isIncludeNoBudget(): bool
|
||||
public function getBudgetType(): ?bool
|
||||
{
|
||||
return $this->includeNoBudget;
|
||||
return $this->budgetType;
|
||||
}
|
||||
|
||||
public function setIncludeNoBudget(bool $includeNoBudget): void
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setBudgetType(?bool $budgetType): void
|
||||
{
|
||||
$this->includeNoBudget = $includeNoBudget;
|
||||
$this->budgetType = $budgetType;
|
||||
}
|
||||
|
||||
public function isIncludeWithoutBudget(): bool
|
||||
{
|
||||
return $this->budgetType === false;
|
||||
}
|
||||
|
||||
public function isIncludeWithBudget(): bool
|
||||
{
|
||||
return $this->budgetType === true;
|
||||
}
|
||||
|
||||
public function isIncludeNoWork(): bool
|
||||
|
||||
@@ -11,19 +11,8 @@ namespace App\Reporting;
|
||||
|
||||
final class Report implements ReportInterface
|
||||
{
|
||||
private $id;
|
||||
private $label;
|
||||
private $route;
|
||||
private $reportIcon = 'reporting';
|
||||
|
||||
public function __construct(string $id, string $route, string $label, ?string $reportIcon = null)
|
||||
public function __construct(private string $id, private string $route, private string $label, private string $reportIcon)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->route = $route;
|
||||
$this->label = $label;
|
||||
if (null !== $reportIcon) {
|
||||
$this->reportIcon = $reportIcon;
|
||||
}
|
||||
}
|
||||
|
||||
public function getRoute(): string
|
||||
|
||||
@@ -16,21 +16,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class ReportingService
|
||||
{
|
||||
public const DEFAULT_VIEW = 'week_by_user';
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
private $security;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $security)
|
||||
public function __construct(private EventDispatcherInterface $dispatcher, private AuthorizationCheckerInterface $security)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,29 +29,33 @@ final class ReportingService
|
||||
$event = new ReportingEvent($user);
|
||||
|
||||
if ($this->security->isGranted('view_reporting')) {
|
||||
$showBudget = $this->security->isGranted('budget_any', 'project');
|
||||
$details = $this->security->isGranted('details', 'project');
|
||||
$viewOther = $this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet');
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user'));
|
||||
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user'));
|
||||
$event->addReport(new Report('year_by_user', 'report_user_year', 'report_user_year', 'user'));
|
||||
if ($viewOther) {
|
||||
if ($this->security->isGranted('report:user')) {
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user'));
|
||||
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user'));
|
||||
$event->addReport(new Report('year_by_user', 'report_user_year', 'report_user_year', 'user'));
|
||||
}
|
||||
|
||||
if ($viewOther = $this->security->isGranted('report:other')) {
|
||||
$event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'users'));
|
||||
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'users'));
|
||||
$event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users'));
|
||||
}
|
||||
if ($showBudget) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
|
||||
if ($this->security->isGranted('report:project')) {
|
||||
if ($this->security->isGranted('details', 'project')) {
|
||||
$event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project'));
|
||||
}
|
||||
if ($this->security->isGranted('budget_any', 'project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
$event->addReport(new Report('daterange_projects', 'report_project_daterange', 'report_project_daterange', 'project'));
|
||||
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
|
||||
}
|
||||
}
|
||||
if ($details) {
|
||||
$event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project'));
|
||||
}
|
||||
if ($showBudget) {
|
||||
$event->addReport(new Report('daterange_projects', 'report_project_daterange', 'report_project_daterange', 'project'));
|
||||
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
|
||||
}
|
||||
if ($viewOther) {
|
||||
$event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer'));
|
||||
|
||||
if ($this->security->isGranted('report:customer')) {
|
||||
if ($viewOther) {
|
||||
$event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\WeekByUser;
|
||||
|
||||
use App\Reporting\DateByUser;
|
||||
|
||||
final class WeekByUser extends DateByUser
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\WeekByUser;
|
||||
|
||||
use App\Form\Type\ReportSumType;
|
||||
use App\Form\Type\UserType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class WeekByUserForm extends AbstractType
|
||||
final class WeekByUserForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', WeekPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -45,10 +32,7 @@ class WeekByUserForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => WeekByUser::class,
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\WeeklyUserList;
|
||||
|
||||
use App\Reporting\AbstractUserList;
|
||||
|
||||
final class WeeklyUserList extends AbstractUserList
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\WeeklyUserList;
|
||||
|
||||
use App\Form\Type\ReportSumType;
|
||||
use App\Form\Type\TeamType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class WeeklyUserListForm extends AbstractType
|
||||
final class WeeklyUserListForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', WeekPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -46,10 +33,7 @@ class WeeklyUserListForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => WeeklyUserList::class,
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\YearByUser;
|
||||
|
||||
use App\Reporting\DateByUser;
|
||||
|
||||
final class YearByUser extends DateByUser
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\YearByUser;
|
||||
|
||||
use App\Form\Type\ReportSumType;
|
||||
use App\Form\Type\UserType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class YearByUserForm extends AbstractType
|
||||
final class YearByUserForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross-linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -45,10 +32,7 @@ class YearByUserForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearByUser::class,
|
||||
@@ -7,7 +7,9 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\YearlyUserList;
|
||||
|
||||
use App\Reporting\AbstractUserList;
|
||||
|
||||
final class YearlyUserList extends AbstractUserList
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting;
|
||||
namespace App\Reporting\YearlyUserList;
|
||||
|
||||
use App\Form\Type\ReportSumType;
|
||||
use App\Form\Type\TeamType;
|
||||
@@ -16,22 +16,9 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class YearlyUserListForm extends AbstractType
|
||||
final class YearlyUserListForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
@@ -47,10 +34,7 @@ class YearlyUserListForm extends AbstractType
|
||||
$builder->add('sumType', ReportSumType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearlyUserList::class,
|
||||
Reference in New Issue
Block a user