added project detail report (#2651)
- avatars via css only - random colors for entities - improves print view - added colors for teams - added color to tag
This commit is contained in:
@@ -362,6 +362,16 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (bool) $this->find('theme.colors_limited');
|
||||
}
|
||||
|
||||
public function isThemeRandomColors(): bool
|
||||
{
|
||||
return (bool) $this->find('theme.random_colors');
|
||||
}
|
||||
|
||||
public function isThemeAllowAvatarUrls(): bool
|
||||
{
|
||||
return (bool) $this->find('theme.avatar_url');
|
||||
}
|
||||
|
||||
public function getThemeAutocompleteCharacters(): int
|
||||
{
|
||||
return (int) $this->find('theme.autocomplete_chars');
|
||||
@@ -384,11 +394,6 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $this->find('theme.branding.title');
|
||||
}
|
||||
|
||||
public function getBrandingCompany(): ?string
|
||||
{
|
||||
return $this->find('theme.branding.company');
|
||||
}
|
||||
|
||||
public function isAllowTagCreation(): bool
|
||||
{
|
||||
return (bool) $this->find('theme.tags_create');
|
||||
|
||||
@@ -43,7 +43,6 @@ class DoctorController extends AbstractController
|
||||
'var/data/',
|
||||
'var/log/',
|
||||
'var/sessions/',
|
||||
'public/avatars/',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,6 @@ final class ProfileController extends AbstractController
|
||||
'user' => $profile,
|
||||
'stats' => $userStats,
|
||||
'years' => $monthlyStats,
|
||||
'stat_date_format' => $localeSettings->getDatePickerFormat(),
|
||||
];
|
||||
|
||||
return $this->render('user/stats.html.twig', $viewVars);
|
||||
@@ -111,7 +110,7 @@ final class ProfileController extends AbstractController
|
||||
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->render('user/profile.html.twig', [
|
||||
return $this->render('user/form.html.twig', [
|
||||
'tab' => 'password',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
@@ -169,7 +168,7 @@ final class ProfileController extends AbstractController
|
||||
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->render('user/profile.html.twig', [
|
||||
return $this->render('user/form.html.twig', [
|
||||
'tab' => 'roles',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
@@ -195,7 +194,7 @@ final class ProfileController extends AbstractController
|
||||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->render('user/profile.html.twig', [
|
||||
return $this->render('user/form.html.twig', [
|
||||
'tab' => 'teams',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
@@ -276,7 +275,7 @@ final class ProfileController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('user/form.html.twig', [
|
||||
return $this->render('user/preferences.html.twig', [
|
||||
'tab' => 'preferences',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
|
||||
53
src/Controller/Reporting/ProjectDetailsController.php
Normal file
53
src/Controller/Reporting/ProjectDetailsController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Project;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsForm;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectDetailsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_details", name="report_project_details", methods={"GET"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('details_project')")
|
||||
*/
|
||||
public function __invoke(Request $request, ProjectStatisticService $service, LocaleSettings $localeSettings)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectDetailsQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createForm(ProjectDetailsForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$projectView = null;
|
||||
$projectDetails = null;
|
||||
|
||||
if ($query->getProject() !== null && $this->isGranted('details', $query->getProject())) {
|
||||
$projectViews = $service->getProjectView($user, [$query->getProject()], $query->getToday());
|
||||
$projectView = $projectViews[0];
|
||||
$projectDetails = $service->getProjectsDetails($query);
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_details.html.twig', [
|
||||
'project_view' => $projectView,
|
||||
'project_details' => $projectDetails,
|
||||
'form' => $form->createView(),
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -531,6 +531,20 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setOptions(['help' => 'help.theme.color_choices'])
|
||||
->setConstraints([new ColorChoices()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// allow avatar URLs
|
||||
(new Configuration())
|
||||
->setName('theme.avatar_url')
|
||||
->setRequired(false)
|
||||
->setLabel('theme.avatar_url')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// random colors as fallback
|
||||
(new Configuration())
|
||||
->setName('theme.random_colors')
|
||||
->setRequired(false)
|
||||
->setLabel('theme.random_colors')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_CALENDAR)
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Configuration\ThemeConfiguration;
|
||||
use App\Twig\Configuration;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
@@ -27,10 +27,11 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
$twig = $container->getDefinition('twig');
|
||||
|
||||
// @deprecated since 1.15
|
||||
$theme = $container->getDefinition(ThemeConfiguration::class);
|
||||
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
||||
|
||||
$config = $container->getDefinition(SystemConfiguration::class);
|
||||
$config = $container->getDefinition(Configuration::class);
|
||||
$twig->addMethodCall('addGlobal', ['kimai_config', $config]);
|
||||
|
||||
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
||||
|
||||
@@ -475,6 +475,12 @@ class Configuration implements ConfigurationInterface
|
||||
->integerNode('autocomplete_chars')
|
||||
->defaultValue(3)
|
||||
->end()
|
||||
->booleanNode('random_colors')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->booleanNode('avatar_url')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -51,12 +51,9 @@ trait ColorTrait
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return self
|
||||
*/
|
||||
public function setColor(?string $color = null)
|
||||
public function setColor(?string $color = null): void
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ class Team
|
||||
*/
|
||||
private $activities;
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->users = new ArrayCollection();
|
||||
|
||||
@@ -280,6 +280,8 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
*/
|
||||
private $roles = [];
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->registeredAt = new DateTime();
|
||||
@@ -896,4 +898,40 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
{
|
||||
return $this->getDisplayName();
|
||||
}
|
||||
|
||||
public function getInitials(): string
|
||||
{
|
||||
$length = 2;
|
||||
|
||||
$name = $this->getDisplayName();
|
||||
$initial = '';
|
||||
|
||||
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
|
||||
// turn my.email@gmail.com into "My Email"
|
||||
$result = mb_strstr($name, '@', true);
|
||||
$name = $result === false ? $name : $result;
|
||||
$name = str_replace('.', ' ', $name);
|
||||
}
|
||||
|
||||
$words = explode(' ', $name);
|
||||
|
||||
// if name contains single word, use first N character
|
||||
if (\count($words) === 1) {
|
||||
$initial = $words[0];
|
||||
|
||||
if (mb_strlen($name) >= $length) {
|
||||
$initial = mb_substr($name, 0, $length, 'UTF-8');
|
||||
}
|
||||
} else {
|
||||
// otherwise, use initial char from each word
|
||||
foreach ($words as $word) {
|
||||
$initial .= mb_substr($word, 0, 1, 'UTF-8');
|
||||
}
|
||||
$initial = mb_substr($initial, 0, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
$initial = mb_strtoupper($initial);
|
||||
|
||||
return $initial;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,5 +71,9 @@ class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
if (($event->isIndexView() || $event->isView('customer_details')) && $this->isGranted('delete', $project)) {
|
||||
$event->addDelete($this->path('admin_project_delete', ['id' => $project->getId()]));
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_reporting') && $this->isGranted('details_project')) {
|
||||
$event->addAction('report_project_details', ['url' => $this->path('report_project_details', ['project' => $project->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ActivityTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
51
src/Form/ColorTrait.php
Normal file
51
src/Form/ColorTrait.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Form\Type\ColorChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
trait ColorTrait
|
||||
{
|
||||
protected function addColor(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('color', ColorChoiceType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
|
||||
// this code exists only for backward compatibility
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) {
|
||||
if (!$event->getForm()->getConfig()->hasOption('choices')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $event->getData();
|
||||
$choices = $event->getForm()->getConfig()->getOption('choices');
|
||||
if (\is_object($data) && method_exists($data, 'getColor')) {
|
||||
$color = $data->getColor();
|
||||
if (!empty($color) && array_search($color, $choices) === false) {
|
||||
$choices[$color] = $color;
|
||||
}
|
||||
}
|
||||
|
||||
$event->getForm()->add('color', ColorChoiceType::class, [
|
||||
'required' => false,
|
||||
'choices' => $choices,
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -9,18 +9,17 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Type\ColorChoiceType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
trait EntityFormTrait
|
||||
{
|
||||
use ColorTrait;
|
||||
|
||||
public function addCommonFields(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addColor($builder);
|
||||
@@ -50,39 +49,10 @@ trait EntityFormTrait
|
||||
]);
|
||||
}
|
||||
|
||||
public function addColor(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('color', ColorChoiceType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
|
||||
// this code exists only for backward compatibility
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) use ($builder) {
|
||||
if (!$builder->get('color')->hasOption('choices')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $event->getData();
|
||||
$choices = $builder->get('color')->getOption('choices');
|
||||
if (\is_object($data) && method_exists($data, 'getColor')) {
|
||||
$color = $data->getColor();
|
||||
if (!empty($color) && array_search($color, $choices) === false) {
|
||||
$choices[$color] = $color;
|
||||
}
|
||||
}
|
||||
|
||||
$event->getForm()->add('color', ColorChoiceType::class, [
|
||||
'required' => false,
|
||||
'choices' => $choices,
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.15
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
public function addCreateMore(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('create_more', CheckboxType::class, [
|
||||
|
||||
@@ -17,8 +17,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\Type\ColorPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -18,6 +17,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TagEditForm extends AbstractType
|
||||
{
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -34,7 +35,8 @@ class TagEditForm extends AbstractType
|
||||
'description' => 'The tag name (forbidden character: comma)',
|
||||
],
|
||||
])
|
||||
->add('color', ColorPickerType::class);
|
||||
;
|
||||
$this->addColor($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamEditForm extends AbstractType
|
||||
{
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -37,7 +39,9 @@ class TeamEditForm extends AbstractType
|
||||
'type' => 'string',
|
||||
'description' => 'Name of the team',
|
||||
],
|
||||
])
|
||||
]);
|
||||
$this->addColor($builder);
|
||||
$builder
|
||||
->add('teamlead', UserType::class, [
|
||||
'label' => 'label.teamlead',
|
||||
'multiple' => false,
|
||||
|
||||
@@ -67,6 +67,7 @@ class ProjectType extends AbstractType
|
||||
'activity_select' => 'activity',
|
||||
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
||||
'ignore_date' => false,
|
||||
'join_customer' => false,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
@@ -78,6 +79,9 @@ class ProjectType extends AbstractType
|
||||
if (true === $options['ignore_date']) {
|
||||
$query->setIgnoreDate(true);
|
||||
}
|
||||
if (true === $options['join_customer']) {
|
||||
$query->setWithCustomer(true);
|
||||
}
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\AvatarType;
|
||||
use App\Form\Type\LanguageType;
|
||||
@@ -25,6 +26,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class UserEditType extends AbstractType
|
||||
{
|
||||
use ColorTrait;
|
||||
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -39,9 +49,17 @@ class UserEditType extends AbstractType
|
||||
'label' => 'label.title',
|
||||
'required' => false,
|
||||
])
|
||||
->add('avatar', AvatarType::class, [
|
||||
;
|
||||
|
||||
if ($this->configuration->isThemeAllowAvatarUrls()) {
|
||||
$builder->add('avatar', AvatarType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
$this->addColor($builder);
|
||||
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
])
|
||||
|
||||
44
src/Migrations/Version20210704111542.php
Normal file
44
src/Migrations/Version20210704111542.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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 Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20210704111542 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Creates the color columns on: user, team';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$users = $schema->getTable('kimai2_users');
|
||||
$users->addColumn('color', 'string', ['length' => 7, 'notnull' => false, 'default' => null]);
|
||||
|
||||
$teams = $schema->getTable('kimai2_teams');
|
||||
$teams->addColumn('color', 'string', ['length' => 7, 'notnull' => false, 'default' => null]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$users = $schema->getTable('kimai2_users');
|
||||
$users->dropColumn('color');
|
||||
|
||||
$teams = $schema->getTable('kimai2_teams');
|
||||
$teams->dropColumn('color');
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,58 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class ActivityStatistic extends TimesheetCountedStatistic
|
||||
use App\Entity\Activity;
|
||||
|
||||
class ActivityStatistic extends TimesheetCountedStatistic implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var Activity
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function setActivity(Activity $activity): void
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Added for simpler re-use in frontend (charts).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
if ($this->activity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->activity->getColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Added for simpler re-use in frontend (charts).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
if ($this->activity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->activity->getName();
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'name' => $this->getName(),
|
||||
'color' => $this->getColor(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,20 +11,12 @@ namespace App\Model\Statistic;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Day
|
||||
class Day extends Timesheet
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $totalDuration = 0;
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $totalDurationBillable = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $totalRate = 0.00;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
@@ -37,8 +29,8 @@ class Day
|
||||
public function __construct(DateTime $day, int $duration, float $rate)
|
||||
{
|
||||
$this->day = $day;
|
||||
$this->totalDuration = $duration;
|
||||
$this->totalRate = $rate;
|
||||
$this->setTotalDuration($duration);
|
||||
$this->setTotalRate($rate);
|
||||
}
|
||||
|
||||
public function getDay(): DateTime
|
||||
@@ -46,18 +38,6 @@ class Day
|
||||
return $this->day;
|
||||
}
|
||||
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
public function setTotalDuration(int $seconds): Day
|
||||
{
|
||||
$this->totalDuration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalDurationBillable(): int
|
||||
{
|
||||
return $this->totalDurationBillable;
|
||||
@@ -68,18 +48,6 @@ class Day
|
||||
$this->totalDurationBillable = $seconds;
|
||||
}
|
||||
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
public function setTotalRate(float $totalRate): Day
|
||||
{
|
||||
$this->totalRate = $totalRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDetails(array $details): Day
|
||||
{
|
||||
$this->details = $details;
|
||||
|
||||
@@ -11,11 +11,9 @@ namespace App\Model\Statistic;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class Month
|
||||
final class Month extends Timesheet
|
||||
{
|
||||
private $month;
|
||||
private $totalDuration = 0;
|
||||
private $totalRate = 0.00;
|
||||
private $billableDuration = 0;
|
||||
private $billableRate = 0.00;
|
||||
|
||||
@@ -40,30 +38,6 @@ final class Month
|
||||
return (int) $this->month;
|
||||
}
|
||||
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
public function setTotalDuration(int $seconds): Month
|
||||
{
|
||||
$this->totalDuration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
public function setTotalRate(float $totalRate): Month
|
||||
{
|
||||
$this->totalRate = $totalRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
|
||||
87
src/Model/Statistic/Timesheet.php
Normal file
87
src/Model/Statistic/Timesheet.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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\Model\Statistic;
|
||||
|
||||
class Timesheet
|
||||
{
|
||||
private $totalDuration = 0;
|
||||
private $totalRate = 0.00;
|
||||
private $totalInternalRate = 0.00;
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getValue(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
public function setTotalDuration(int $totalDuration): void
|
||||
{
|
||||
$this->totalDuration = $totalDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
public function setTotalRate(float $totalRate): void
|
||||
{
|
||||
$this->totalRate = $totalRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getInternalRate(): float
|
||||
{
|
||||
return $this->totalInternalRate;
|
||||
}
|
||||
|
||||
public function getTotalInternalRate(): float
|
||||
{
|
||||
return $this->totalInternalRate;
|
||||
}
|
||||
|
||||
public function setTotalInternalRate(float $totalInternalRate): void
|
||||
{
|
||||
$this->totalInternalRate = $totalInternalRate;
|
||||
}
|
||||
}
|
||||
60
src/Model/Statistic/UserYear.php
Normal file
60
src/Model/Statistic/UserYear.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Model\Statistic;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class UserYear
|
||||
{
|
||||
/**
|
||||
* @var Year
|
||||
*/
|
||||
private $year;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user, Year $year)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->year = $year;
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getDuration(): int
|
||||
{
|
||||
$duration = 0;
|
||||
foreach ($this->year->getMonths() as $month) {
|
||||
$duration += $month->getDuration();
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
public function getRate(): float
|
||||
{
|
||||
$rate = 0;
|
||||
foreach ($this->year->getMonths() as $month) {
|
||||
$rate += $month->getRate();
|
||||
}
|
||||
|
||||
return $rate;
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,13 @@
|
||||
|
||||
namespace App\Model\Statistic;
|
||||
|
||||
/**
|
||||
* Yearly statistics
|
||||
*/
|
||||
class Year
|
||||
final class Year extends Timesheet
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $year;
|
||||
private $year;
|
||||
/**
|
||||
* @var Month[]
|
||||
*/
|
||||
protected $months = [];
|
||||
private $months = [];
|
||||
|
||||
public function __construct(string $year)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class TimesheetCountedStatistic
|
||||
class TimesheetCountedStatistic implements \JsonSerializable
|
||||
{
|
||||
private $recordAmount = 0;
|
||||
private $recordDuration = 0;
|
||||
@@ -40,6 +40,26 @@ class TimesheetCountedStatistic
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getValue(): int
|
||||
{
|
||||
return $this->recordDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDuration(): int
|
||||
{
|
||||
return $this->recordDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total duration of all included timesheet records.
|
||||
*
|
||||
@@ -61,6 +81,16 @@ class TimesheetCountedStatistic
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getRate(): float
|
||||
{
|
||||
return $this->recordRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total rate of all included timesheet records.
|
||||
*
|
||||
@@ -132,4 +162,17 @@ class TimesheetCountedStatistic
|
||||
{
|
||||
$this->recordRateBillable = $recordRate;
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'duration' => $this->recordDuration,
|
||||
'duration_billable' => $this->recordDurationBillable,
|
||||
'rate' => $this->recordRate,
|
||||
'rate_billable' => $this->recordRateBillable,
|
||||
'rate_internal' => $this->recordInternalRate,
|
||||
'amount' => $this->recordAmount,
|
||||
'amount_billable' => $this->recordAmountBillable,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Model/UserStatistic.php
Normal file
40
src/Model/UserStatistic.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Month;
|
||||
|
||||
class UserStatistic extends TimesheetCountedStatistic
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function addValuesFromMonth(Month $month): void
|
||||
{
|
||||
$this->setRecordDuration($this->getRecordDuration() + $month->getTotalDuration());
|
||||
$this->setDurationBillable($this->getDurationBillable() + $month->getBillableDuration());
|
||||
$this->setRecordRate($this->getRate() + $month->getTotalRate());
|
||||
$this->setRateBillable($this->getRateBillable() + $month->getBillableRate());
|
||||
$this->setRecordInternalRate($this->getRecordInternalRate() + $month->getTotalInternalRate());
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,28 @@
|
||||
|
||||
namespace App\Project;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Event\ProjectStatisticEvent;
|
||||
use App\Model\ActivityStatistic;
|
||||
use App\Model\ProjectStatistic;
|
||||
use App\Model\Statistic\Month;
|
||||
use App\Model\Statistic\Year;
|
||||
use App\Model\UserStatistic;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsModel;
|
||||
use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewModel;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
@@ -32,12 +41,14 @@ class ProjectStatisticService
|
||||
private $repository;
|
||||
private $timesheetRepository;
|
||||
private $dispatcher;
|
||||
private $userRepository;
|
||||
|
||||
public function __construct(ProjectRepository $projectRepository, TimesheetRepository $timesheetRepository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ProjectRepository $projectRepository, TimesheetRepository $timesheetRepository, EventDispatcherInterface $dispatcher, UserRepository $userRepository)
|
||||
{
|
||||
$this->repository = $projectRepository;
|
||||
$this->timesheetRepository = $timesheetRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function getProjectStatistics(Project $project, ?DateTime $end = null): ProjectStatistic
|
||||
@@ -89,6 +100,137 @@ class ProjectStatisticService
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectDetailsQuery $query
|
||||
* @return ProjectDetailsModel
|
||||
*/
|
||||
public function getProjectsDetails(ProjectDetailsQuery $query): ProjectDetailsModel
|
||||
{
|
||||
$model = new ProjectDetailsModel($query->getProject());
|
||||
|
||||
$years = [];
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internalRate')
|
||||
->addSelect('COUNT(t.id) as count')
|
||||
->andWhere('t.project = :project')
|
||||
->setParameter('project', $query->getProject())
|
||||
;
|
||||
|
||||
// fetch stats grouped by ACTIVITY for all time
|
||||
$qb1 = clone $qb;
|
||||
$qb1
|
||||
->leftJoin(Activity::class, 'a', Join::WITH, 'a.id = t.activity')
|
||||
->addSelect('a as activity')
|
||||
->addGroupBy('a')
|
||||
;
|
||||
foreach ($qb1->getQuery()->getResult() as $tmp) {
|
||||
$activity = new ActivityStatistic();
|
||||
$activity->setActivity($tmp['activity']);
|
||||
$activity->setRecordRate($tmp['rate']);
|
||||
$activity->setRecordDuration($tmp['duration']);
|
||||
$activity->setRecordInternalRate($tmp['internalRate']);
|
||||
$activity->setRecordAmount($tmp['count']);
|
||||
$model->addActivity($activity);
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
|
||||
// fetch stats grouped by YEAR, MONTH and USER
|
||||
$qb1 = clone $qb;
|
||||
$qb1
|
||||
->addSelect('YEAR(t.begin) as year')
|
||||
->addSelect('MONTH(t.begin) as month')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->addGroupBy('year')
|
||||
->addGroupBy('month')
|
||||
->addGroupBy('user')
|
||||
;
|
||||
|
||||
$userMonths = $qb1->getQuery()->getResult();
|
||||
$userIds = array_unique(array_column($userMonths, 'user'));
|
||||
|
||||
$qb2 = $this->userRepository->createQueryBuilder('u');
|
||||
$qb2->select('u')->where($qb2->expr()->in('u.id', $userIds));
|
||||
$users = [];
|
||||
foreach ($qb2->getQuery()->getResult() as $user) {
|
||||
$users[$user->getId()] = new UserStatistic($user);
|
||||
}
|
||||
|
||||
foreach ($userMonths as $tmp) {
|
||||
$user = $users[$tmp['user']]->getUser();
|
||||
$year = $model->getUserYear($tmp['year'], $user);
|
||||
if ($year === null) {
|
||||
$year = new Year($tmp['year']);
|
||||
$model->setUserYear($year, $user);
|
||||
}
|
||||
$month = new Month($tmp['month']);
|
||||
$month->setTotalRate($tmp['rate']);
|
||||
$month->setTotalDuration($tmp['duration']);
|
||||
$month->setTotalInternalRate($tmp['internalRate']);
|
||||
$year->setMonth($month);
|
||||
$users[$tmp['user']]->addValuesFromMonth($month);
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
|
||||
// fetch stats grouped by YEARS
|
||||
$qb1 = clone $qb;
|
||||
$qb1
|
||||
->addSelect('YEAR(t.begin) as year')
|
||||
->addGroupBy('year')
|
||||
;
|
||||
foreach ($qb1->getQuery()->getResult() as $year) {
|
||||
$tmp = new Year($year['year']);
|
||||
$tmp->setTotalRate($year['rate']);
|
||||
$tmp->setTotalInternalRate($year['internalRate']);
|
||||
$tmp->setTotalDuration($year['duration']);
|
||||
$years[$year['year']] = $tmp;
|
||||
|
||||
// fetch yearly stats grouped by ACTIVITY and YEAR
|
||||
$qb2 = clone $qb;
|
||||
$qb2
|
||||
->leftJoin(Activity::class, 'a', Join::WITH, 'a.id = t.activity')
|
||||
->addSelect('a as activity')
|
||||
->addSelect('YEAR(t.begin) as year')
|
||||
->andWhere('YEAR(t.begin) = :year')
|
||||
->setParameter('year', $year['year'])
|
||||
->addGroupBy('year')
|
||||
->addGroupBy('a')
|
||||
;
|
||||
foreach ($qb2->getQuery()->getResult() as $tmp) {
|
||||
$activity = new ActivityStatistic();
|
||||
$activity->setActivity($tmp['activity']);
|
||||
$activity->setRecordRate($tmp['rate']);
|
||||
$activity->setRecordDuration($tmp['duration']);
|
||||
$activity->setRecordInternalRate($tmp['internalRate']);
|
||||
$activity->setRecordAmount($tmp['count']);
|
||||
$model->addYearActivity($tmp['year'], $activity);
|
||||
}
|
||||
}
|
||||
$model->setYears(array_values($years));
|
||||
// ---------------------------------------------------
|
||||
|
||||
// fetch stats grouped by MONTH and YEAR
|
||||
$qb1 = clone $qb;
|
||||
$qb1
|
||||
->addSelect('YEAR(t.begin) as year')
|
||||
->addSelect('MONTH(t.begin) as month')
|
||||
->addGroupBy('year')
|
||||
->addGroupBy('month')
|
||||
;
|
||||
foreach ($qb1->getQuery()->getResult() as $month) {
|
||||
$tmp = new Month($month['month']);
|
||||
$tmp->setTotalRate($month['rate']);
|
||||
$tmp->setTotalInternalRate($month['internalRate']);
|
||||
$tmp->setTotalDuration($month['duration']);
|
||||
$model->getYear($month['year'])->setMonth($tmp);
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return Project[]
|
||||
|
||||
53
src/Reporting/ProjectDetails/ProjectDetailsForm.php
Normal file
53
src/Reporting/ProjectDetails/ProjectDetailsForm.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\ProjectDetails;
|
||||
|
||||
use App\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectDetailsForm 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)
|
||||
{
|
||||
$builder->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
'join_customer' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectDetailsQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
158
src/Reporting/ProjectDetails/ProjectDetailsModel.php
Normal file
158
src/Reporting/ProjectDetails/ProjectDetailsModel.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?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\ProjectDetails;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Model\ActivityStatistic;
|
||||
use App\Model\Statistic\UserYear;
|
||||
use App\Model\Statistic\Year;
|
||||
use App\Model\UserStatistic;
|
||||
|
||||
final class ProjectDetailsModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var Year[]
|
||||
*/
|
||||
private $years = [];
|
||||
/**
|
||||
* @var array<string, array<ActivityStatistic>>
|
||||
*/
|
||||
private $yearlyActivities = [];
|
||||
/**
|
||||
* @var array<string, array<int, UserYear>>
|
||||
*/
|
||||
private $usersMonthly = [];
|
||||
/**
|
||||
* @var ActivityStatistic[]
|
||||
*/
|
||||
private $activities = [];
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function addActivity(ActivityStatistic $activityStatistic): void
|
||||
{
|
||||
$this->activities[$activityStatistic->getActivity()->getId()] = $activityStatistic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActivityStatistic[]
|
||||
*/
|
||||
public function getActivities(): array
|
||||
{
|
||||
return array_values($this->activities);
|
||||
}
|
||||
|
||||
public function addYearActivity(string $year, ActivityStatistic $activityStatistic): void
|
||||
{
|
||||
$this->yearlyActivities[$year][] = $activityStatistic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @return ActivityStatistic[]
|
||||
*/
|
||||
public function getYearActivities(string $year): ?array
|
||||
{
|
||||
if (!\array_key_exists($year, $this->yearlyActivities)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->yearlyActivities[$year];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserStatistic[]
|
||||
*/
|
||||
public function getUserStats(): array
|
||||
{
|
||||
$users = [];
|
||||
foreach ($this->usersMonthly as $year) {
|
||||
foreach ($year as $id => $userYear) {
|
||||
if (\array_key_exists($id, $users)) {
|
||||
$userStat = $users[$id];
|
||||
} else {
|
||||
$userStat = new UserStatistic($userYear->getUser());
|
||||
$users[$id] = $userStat;
|
||||
}
|
||||
$userStat->setRecordDuration($userStat->getRecordDuration() + $userYear->getDuration());
|
||||
$userStat->setRecordRate($userStat->getRecordRate() + $userYear->getRate());
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function setUserYear(Year $year, User $user): void
|
||||
{
|
||||
$this->usersMonthly[$year->getYear()][$user->getId()] = new UserYear($user, $year);
|
||||
}
|
||||
|
||||
public function getUserYear(string $year, User $user): ?Year
|
||||
{
|
||||
if (!\array_key_exists($year, $this->usersMonthly) || !\array_key_exists($user->getId(), $this->usersMonthly[$year])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->usersMonthly[$year][$user->getId()]->getYear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @return UserYear[]
|
||||
*/
|
||||
public function getUserYears(string $year): array
|
||||
{
|
||||
if (!\array_key_exists($year, $this->usersMonthly)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->usersMonthly[$year];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Year[]
|
||||
*/
|
||||
public function getYears(): array
|
||||
{
|
||||
return $this->years;
|
||||
}
|
||||
|
||||
public function getYear(string $year): ?Year
|
||||
{
|
||||
foreach ($this->years as $tmp) {
|
||||
if ($tmp->getYear() === $year) {
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Year[] $years
|
||||
*/
|
||||
public function setYears(array $years): void
|
||||
{
|
||||
$this->years = $years;
|
||||
}
|
||||
}
|
||||
56
src/Reporting/ProjectDetails/ProjectDetailsQuery.php
Normal file
56
src/Reporting/ProjectDetails/ProjectDetailsQuery.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\ProjectDetails;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectDetailsQuery
|
||||
{
|
||||
/**
|
||||
* @var Project|null
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $today;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(DateTime $today, User $user)
|
||||
{
|
||||
$this->today = $today;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getToday(): DateTime
|
||||
{
|
||||
return $this->today;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@ final class ReportingService
|
||||
}
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
if ($this->security->isGranted('details_project')) {
|
||||
$event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project'));
|
||||
}
|
||||
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,10 @@ class ProjectRepository extends EntityRepository
|
||||
->addOrderBy('p.name', 'ASC')
|
||||
;
|
||||
|
||||
if ($query->withCustomer()) {
|
||||
$qb->addSelect('c');
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
* @var Project|null
|
||||
*/
|
||||
private $projectToIgnore;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $ignoreDate = false;
|
||||
private $withCustomer = false;
|
||||
|
||||
/**
|
||||
* @param Project|int|null $project
|
||||
@@ -44,6 +42,16 @@ final class ProjectFormTypeQuery extends BaseFormTypeQuery
|
||||
}
|
||||
}
|
||||
|
||||
public function withCustomer(): bool
|
||||
{
|
||||
return $this->withCustomer;
|
||||
}
|
||||
|
||||
public function setWithCustomer(bool $withCustomer): void
|
||||
{
|
||||
$this->withCustomer = $withCustomer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project|null
|
||||
*/
|
||||
|
||||
@@ -1,59 +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\Twig;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Utils\AvatarService;
|
||||
use Symfony\Component\Asset\Packages;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class AvatarExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var AvatarService
|
||||
*/
|
||||
private $avatar;
|
||||
/**
|
||||
* @var Packages
|
||||
*/
|
||||
private $packages;
|
||||
|
||||
public function __construct(AvatarService $avatar, Packages $packages)
|
||||
{
|
||||
$this->avatar = $avatar;
|
||||
$this->packages = $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('avatar', [$this, 'getAvatarUrl']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getAvatarUrl(?User $profile, string $default): string
|
||||
{
|
||||
if (null === $profile) {
|
||||
return $this->packages->getUrl($default);
|
||||
}
|
||||
|
||||
$url = $this->avatar->getAvatar($profile);
|
||||
|
||||
if (null === $url) {
|
||||
return $this->packages->getUrl($default);
|
||||
}
|
||||
|
||||
return $this->packages->getUrl($url, 'avatars');
|
||||
}
|
||||
}
|
||||
67
src/Twig/Configuration.php
Normal file
67
src/Twig/Configuration.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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\Twig;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
final class Configuration extends AbstractExtension
|
||||
{
|
||||
private $configuration;
|
||||
private $cache = [];
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('config', [$this, 'get']),
|
||||
];
|
||||
}
|
||||
|
||||
public function get(string $name)
|
||||
{
|
||||
if (\array_key_exists($name, $this->cache)) {
|
||||
return $this->cache[$name];
|
||||
}
|
||||
|
||||
$value = $this->configuration->find($name);
|
||||
$this->cache[$name] = $value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
if (\array_key_exists($name, $this->cache)) {
|
||||
return $this->cache[$name];
|
||||
}
|
||||
|
||||
$checks = ['is' . $name, 'get' . $name, 'has' . $name, $name];
|
||||
|
||||
foreach ($checks as $methodName) {
|
||||
if (method_exists($this->configuration, $methodName)) {
|
||||
$value = \call_user_func([$this->configuration, $methodName], $arguments);
|
||||
$this->cache[$name] = $value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->configuration->find($name);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class Extensions extends AbstractExtension
|
||||
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
|
||||
new TwigFilter('color', [$this, 'color']),
|
||||
new TwigFilter('font_contrast', [$this, 'calculateFontContrastColor']),
|
||||
new TwigFilter('default_color', [$this, 'defaultColor']),
|
||||
new TwigFilter('nl2str', [$this, 'replaceNewline'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
@@ -43,6 +44,7 @@ class Extensions extends AbstractExtension
|
||||
return [
|
||||
new TwigFunction('class_name', [$this, 'getClassName']),
|
||||
new TwigFunction('iso_day_by_name', [$this, 'getIsoDayByName']),
|
||||
new TwigFunction('random_color', [$this, 'randomColor']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -71,11 +73,21 @@ class Extensions extends AbstractExtension
|
||||
return (new Color())->getColor($entity, $defaultColor);
|
||||
}
|
||||
|
||||
public function randomColor(?string $input = null): string
|
||||
{
|
||||
return (new Color())->getRandom($input);
|
||||
}
|
||||
|
||||
public function calculateFontContrastColor(string $color): string
|
||||
{
|
||||
return (new Color())->getFontContrastColor($color);
|
||||
}
|
||||
|
||||
public function defaultColor(?string $color = null): string
|
||||
{
|
||||
return $color ?? Constants::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
* @return null|string
|
||||
|
||||
@@ -59,6 +59,7 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
new TwigFilter('time', [$this, 'time']),
|
||||
new TwigFilter('hour24', [$this, 'hour24']),
|
||||
new TwigFilter('duration', [$this, 'duration']),
|
||||
new TwigFilter('chart_duration', [$this, 'durationChart']),
|
||||
new TwigFilter('duration_decimal', [$this, 'durationDecimal']),
|
||||
new TwigFilter('money', [$this, 'money']),
|
||||
new TwigFilter('currency', [$this, 'currency']),
|
||||
@@ -91,6 +92,7 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
new TwigFunction('get_format_duration', [$this, 'getDurationFormat']),
|
||||
new TwigFunction('create_date', [$this, 'createDate']),
|
||||
new TwigFunction('locales', [$this, 'getLocales']),
|
||||
new TwigFunction('month_names', [$this, 'getMonthNames']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -187,6 +189,25 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return $this->getFormatter()->time($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $year
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMonthNames(?string $year = null): array
|
||||
{
|
||||
$withYear = true;
|
||||
if ($year === null) {
|
||||
$year = date('Y');
|
||||
$withYear = false;
|
||||
}
|
||||
$months = [];
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$months[] = $this->getFormatter()->monthName(new DateTime(sprintf('%s-%s-10', $year, ($i < 10 ? '0' . $i : (string) $i))), $withYear);
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
public function monthName(\DateTime $dateTime, bool $withYear = false): string
|
||||
{
|
||||
return $this->getFormatter()->monthName($dateTime, $withYear);
|
||||
@@ -235,6 +256,11 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return $this->getFormatter()->durationDecimal($duration);
|
||||
}
|
||||
|
||||
public function durationChart($duration): string
|
||||
{
|
||||
return number_format(($duration / 3600), 2, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|float $amount
|
||||
* @return bool|false|string
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Entity\User;
|
||||
use App\Event\PageActionsEvent;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||
use App\Utils\Color;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -26,6 +27,10 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
private $eventDispatcher;
|
||||
private $translator;
|
||||
private $configuration;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $randomColors;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TranslatorInterface $translator, SystemConfiguration $configuration)
|
||||
{
|
||||
@@ -135,4 +140,25 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
return $this->configuration->find($name);
|
||||
}
|
||||
|
||||
public function colorize(?string $color, ?string $identifier = null, ?string $fallback = null): string
|
||||
{
|
||||
if ($color !== null) {
|
||||
return $color;
|
||||
}
|
||||
|
||||
if ($this->randomColors === null) {
|
||||
$this->randomColors = $this->configuration->isThemeRandomColors();
|
||||
}
|
||||
|
||||
if ($this->randomColors) {
|
||||
return (new Color())->getRandom($identifier);
|
||||
}
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return Constants::DEFAULT_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class RuntimeExtensions extends AbstractExtension
|
||||
new TwigFilter('desc2html', [MarkdownExtension::class, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('comment2html', [MarkdownExtension::class, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('comment1line', [MarkdownExtension::class, 'commentOneLiner'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('colorize', [ThemeExtension::class, 'colorize']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +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\Utils;
|
||||
|
||||
use App\Entity\User;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
use Laravolt\Avatar\Generator\DefaultGenerator;
|
||||
|
||||
class AvatarService
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $directory;
|
||||
|
||||
public const AVATAR_CONFIG = [
|
||||
'driver' => 'gd',
|
||||
'generator' => DefaultGenerator::class,
|
||||
'ascii' => true,
|
||||
'shape' => 'circle',
|
||||
'width' => 100,
|
||||
'height' => 100,
|
||||
'chars' => 2,
|
||||
'fontSize' => 44,
|
||||
'fontFamily' => null,
|
||||
'uppercase' => true,
|
||||
//'fonts' => ['path/to/OpenSans-Bold.ttf', 'path/to/rockwell.ttf'],
|
||||
'foregrounds' => [
|
||||
'#FFFFFF'
|
||||
],
|
||||
'backgrounds' => [
|
||||
'#f44336',
|
||||
'#E91E63',
|
||||
'#9C27B0',
|
||||
'#673AB7',
|
||||
'#3F51B5',
|
||||
'#2196F3',
|
||||
'#03A9F4',
|
||||
'#00BCD4',
|
||||
'#009688',
|
||||
'#4CAF50',
|
||||
'#8BC34A',
|
||||
'#CDDC39',
|
||||
'#FFC107',
|
||||
'#FF9800',
|
||||
'#FF5722',
|
||||
],
|
||||
'border' => [
|
||||
'size' => 1,
|
||||
'color' => 'background'
|
||||
],
|
||||
'theme' => '*',
|
||||
'themes' => [
|
||||
/*
|
||||
'grayscale-light' => [
|
||||
'backgrounds' => ['#edf2f7', '#e2e8f0', '#cbd5e0'],
|
||||
'foregrounds' => ['#a0aec0'],
|
||||
],
|
||||
'grayscale-dark' => [
|
||||
'backgrounds' => ['#2d3748', '#4a5568', '#718096'],
|
||||
'foregrounds' => ['#e2e8f0'],
|
||||
],
|
||||
*/
|
||||
'colorful' => [
|
||||
'backgrounds' => [
|
||||
'#a972c9',
|
||||
'#9C27B0',
|
||||
'#673AB7',
|
||||
'#5319e7',
|
||||
'#041fd1',
|
||||
'#3F51B5',
|
||||
'#2196F3',
|
||||
'#03A9F4',
|
||||
'#00BCD4',
|
||||
'#006b75',
|
||||
'#009688',
|
||||
'#00bb32',
|
||||
'#4CAF50',
|
||||
'#8BC34A',
|
||||
'#CDDC39',
|
||||
'#FFC107',
|
||||
'#FF9800',
|
||||
'#FF5722',
|
||||
'#f41a00',
|
||||
'#E91E63',
|
||||
'#b60205',
|
||||
'#cc317c',
|
||||
'#d82d80',
|
||||
'#e135f4',
|
||||
'#2d3748',
|
||||
'#4a5568',
|
||||
'#718096',
|
||||
],
|
||||
'foregrounds' => ['#FFFFFF'],
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->setStorageDirectory($projectDirectory . '/public/avatars/');
|
||||
}
|
||||
|
||||
public function getStorageDirectory(): string
|
||||
{
|
||||
return $this->directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setStorageDirectory(string $directory)
|
||||
{
|
||||
$this->directory = realpath($directory);
|
||||
}
|
||||
|
||||
private function getAvatarUrl(User $profile): string
|
||||
{
|
||||
return md5($profile->getId() . '_' . $profile->getDisplayName()) . '.png';
|
||||
}
|
||||
|
||||
private function getImagePath(User $profile): string
|
||||
{
|
||||
return $this->getStorageDirectory() . '/' . $this->getAvatarUrl($profile);
|
||||
}
|
||||
|
||||
public function generateAvatar(User $profile, bool $regenerate = false): bool
|
||||
{
|
||||
if (!$this->hasDependencies()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filePath = $this->getImagePath($profile);
|
||||
|
||||
if ($regenerate || !file_exists($filePath)) {
|
||||
if (!is_writable(\dirname($filePath))) {
|
||||
return false;
|
||||
}
|
||||
$avatar = new Avatar(self::AVATAR_CONFIG);
|
||||
$avatar->create($profile->getDisplayName())->save($filePath, 90);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAvatar(User $profile): ?string
|
||||
{
|
||||
if (!empty(trim($profile->getAvatar()))) {
|
||||
return $profile->getAvatar();
|
||||
}
|
||||
|
||||
if (!$this->generateAvatar($profile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getAvatarUrl($profile);
|
||||
}
|
||||
|
||||
public function hasDependencies(): bool
|
||||
{
|
||||
return \extension_loaded('gd') && \function_exists('imagettfbbox');
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,13 @@ use App\Entity\Timesheet;
|
||||
|
||||
final class Color
|
||||
{
|
||||
// @see https://clrs.cc
|
||||
public const PALETTE_1 = ['#0074D9', '#7FDBFF', '#39CCCC', '#B10DC9', '#F012BE', '#85144b', '#FF4136', '#FF851B', '#FFDC00', '#3D9970', '#2ECC40', '#01FF70', '#AAAAAA', '#DDDDDD'];
|
||||
// old avatar color set
|
||||
public const PALETTE_2 = ['#a972c9', '#9C27B0', '#673AB7', '#5319e7', '#041fd1', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#006b75', '#009688', '#00bb32', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#f41a00', '#E91E63', '#b60205', '#cc317c', '#d82d80', '#e135f4', '#2d3748', '#4a5568', '#718096'];
|
||||
// all mixed together
|
||||
public const PALETTE_3 = ['#AAAAAA', '#DDDDDD', '#a972c9', '#9C27B0', '#673AB7', '#041fd1', '#5319e7', '#3F51B5', '#0074D9', '#2196F3', '#03A9F4', '#7FDBFF', '#39CCCC', '#00BCD4', '#006b75', '#009688', '#00bb32', '#4CAF50', '#3D9970', '#2ECC40', '#01FF70', '#8BC34A', '#CDDC39', '#FFDC00', '#FFC107', '#FF851B', '#FF9800', '#FF5722', '#f41a00', '#E91E63', '#85144b', '#b60205', '#FF4136', '#cc317c', '#F012BE', '#d82d80', '#B10DC9', '#e135f4', '#2d3748', '#4a5568', '#718096'];
|
||||
|
||||
public function getTimesheetColor(Timesheet $timesheet): string
|
||||
{
|
||||
$activity = $timesheet->getActivity();
|
||||
@@ -76,6 +83,33 @@ final class Color
|
||||
return $defaultColor ? Constants::DEFAULT_COLOR : null;
|
||||
}
|
||||
|
||||
public function getRandom(?string $input = null): string
|
||||
{
|
||||
if ($input === null) {
|
||||
return $this->getRandomColor();
|
||||
}
|
||||
|
||||
return $this->getRandomFromPalette($input);
|
||||
}
|
||||
|
||||
public function getRandomColor(): string
|
||||
{
|
||||
return sprintf('#%06x', rand(0, 16777215));
|
||||
}
|
||||
|
||||
public function getRandomFromPalette(string $input): string
|
||||
{
|
||||
$id = 0;
|
||||
for ($pos = 0; $pos < \strlen($input); $pos++) {
|
||||
$id += mb_ord($input[$pos], 'UTF-8');
|
||||
}
|
||||
|
||||
$colors = self::PALETTE_3;
|
||||
$key = $id % \count($colors);
|
||||
|
||||
return $colors[$key];
|
||||
}
|
||||
|
||||
public function getFontContrastColor(string $color): string
|
||||
{
|
||||
if (empty($color) || $color[0] !== '#') {
|
||||
|
||||
Reference in New Issue
Block a user