added avatars, show user teams in list, new team dashboard widgets (#1150)
This commit is contained in:
@@ -73,10 +73,6 @@ class UserController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query
|
||||
->setResultType(UserQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('username')
|
||||
;
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
@@ -90,7 +86,7 @@ class UserController extends BaseApiController
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$data = $this->repository->getUsersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'User']);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -31,65 +31,10 @@ class AboutController extends AbstractController
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/debug", name="about_debug", methods={"GET"})
|
||||
*
|
||||
* @Security("is_granted('system_information')")
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function debugAction()
|
||||
{
|
||||
$phpInfo = $this->getPhpInfo();
|
||||
unset($phpInfo[0]);
|
||||
unset($phpInfo[1]);
|
||||
|
||||
$ini = [
|
||||
'allow_url_fopen',
|
||||
'allow_url_include',
|
||||
'default_charset',
|
||||
'default_mimetype',
|
||||
'display_errors',
|
||||
'error_log',
|
||||
'error_reporting',
|
||||
'log_errors',
|
||||
'max_execution_time',
|
||||
'memory_limit',
|
||||
'open_basedir',
|
||||
'post_max_size',
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
foreach ($ini as $name) {
|
||||
try {
|
||||
$settings[$name] = ini_get($name);
|
||||
} catch (\Exception $ex) {
|
||||
$settings[$name] = "Couldn't load ini setting: " . $ex->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('about/system.html.twig', array_merge(
|
||||
[
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'MAILER_FROM' => getenv('MAILER_FROM'),
|
||||
'CORS_ALLOW_ORIGIN' => getenv('CORS_ALLOW_ORIGIN'),
|
||||
],
|
||||
'info' => $phpInfo,
|
||||
'settings' => $settings,
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="about", methods={"GET"})
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function license()
|
||||
public function license(): Response
|
||||
{
|
||||
$filename = $this->projectDirectory . '/LICENSE';
|
||||
|
||||
@@ -108,41 +53,4 @@ class AboutController extends AbstractController
|
||||
'license' => $license
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author https://php.net/manual/en/function.phpinfo.php#117961
|
||||
* @return array
|
||||
*/
|
||||
private function getPhpInfo()
|
||||
{
|
||||
$plainText = function ($input) {
|
||||
return trim(html_entity_decode(strip_tags($input)));
|
||||
};
|
||||
|
||||
ob_start();
|
||||
phpinfo(1);
|
||||
|
||||
$phpinfo = ['phpinfo' => []];
|
||||
|
||||
if (preg_match_all(
|
||||
'#(?:<h2.*?>(?:<a.*?>)?(.*?)(?:<\/a>)?<\/h2>)|' .
|
||||
'(?:<tr.*?><t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>)?)?</tr>)#s',
|
||||
ob_get_clean(),
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
)) {
|
||||
foreach ($matches as $match) {
|
||||
$fn = $plainText;
|
||||
if (isset($match[3])) {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][$fn($match[2])] = isset($match[4]) ? [$fn($match[3]), $fn($match[4])] : $fn($match[3]);
|
||||
} else {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][] = $fn($match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $phpinfo['phpinfo'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\AuthorizedWidget;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -67,11 +69,23 @@ class DashboardController extends AbstractController
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO this should be dynamic
|
||||
if ($widgetRow['type'] === 'compoundChart') {
|
||||
$row = new CompoundChart();
|
||||
} else {
|
||||
$row = new CompoundRow();
|
||||
if (!isset($widgetRow['type'])) {
|
||||
$widgetRow['type'] = CompoundRow::class;
|
||||
}
|
||||
|
||||
if (!class_exists($widgetRow['type'])) {
|
||||
throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetRow['type']));
|
||||
}
|
||||
|
||||
$row = new $widgetRow['type']();
|
||||
if (!($row instanceof AbstractContainer)) {
|
||||
throw new WidgetException(
|
||||
sprintf(
|
||||
'Expected widget type to be an instanceof "%s", but found "%s"',
|
||||
AbstractContainer::class,
|
||||
$widgetRow['type']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$row->setTitle($widgetRow['title'] ?? '');
|
||||
@@ -82,7 +96,23 @@ class DashboardController extends AbstractController
|
||||
throw new \Exception(sprintf('Unknown widget "%s"', $widgetName));
|
||||
}
|
||||
|
||||
$row->addWidget($this->widgets->getWidget($widgetName));
|
||||
$widget = $this->widgets->getWidget($widgetName);
|
||||
|
||||
$add = true;
|
||||
if ($widget instanceof AuthorizedWidget) {
|
||||
$tmp = false;
|
||||
foreach ($widget->getPermissions() as $perm) {
|
||||
if ($this->isGranted($perm)) {
|
||||
$tmp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$add = $tmp;
|
||||
}
|
||||
|
||||
if ($add) {
|
||||
$row->addWidget($widget);
|
||||
}
|
||||
}
|
||||
|
||||
$event->addSection($row);
|
||||
|
||||
236
src/Controller/DoctorController.php
Normal file
236
src/Controller/DoctorController.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?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;
|
||||
|
||||
use PackageVersions\Versions;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/doctor")
|
||||
* @Security("is_granted('system_information')")
|
||||
*/
|
||||
class DoctorController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* PHP extensions which Kimai needs for runtime.
|
||||
* Some are not a hard requiremenet, but some functions might not work as expected.
|
||||
*/
|
||||
public const REQUIRED_EXTENSIONS = [
|
||||
'intl',
|
||||
'json',
|
||||
'mbstring',
|
||||
'pdo',
|
||||
'zip',
|
||||
'gd',
|
||||
'xml'
|
||||
];
|
||||
|
||||
/**
|
||||
* Directories which need to be writable by the webserver.
|
||||
*/
|
||||
public const DIRECTORIES_WRITABLE = [
|
||||
'var/cache/',
|
||||
'var/data/',
|
||||
'var/log/',
|
||||
'var/sessions/',
|
||||
'public/avatars/',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $projectDirectory;
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="doctor", methods={"GET"})
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
$logLines = 100;
|
||||
|
||||
return $this->render('doctor/index.html.twig', array_merge(
|
||||
[
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => $this->getEnvVars(),
|
||||
'info' => $this->getPhpInfo(),
|
||||
'settings' => $this->getIniSettings(),
|
||||
'extensions' => $this->getLoadedExtensions(),
|
||||
'directories' => $this->getFilePermissions(),
|
||||
'logs' => $this->getLog(),
|
||||
'logLines' => $logLines,
|
||||
'logSize' => $this->getLogSize(),
|
||||
'composer' => Versions::VERSIONS,
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
private function getLoadedExtensions()
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach (self::REQUIRED_EXTENSIONS as $extName) {
|
||||
$results[$extName] = false;
|
||||
if (extension_loaded($extName)) {
|
||||
$results[$extName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function getLogSize()
|
||||
{
|
||||
$logfileName = 'var/log/' . getenv('APP_ENV') . '.log';
|
||||
$logfile = $this->projectDirectory . '/' . $logfileName;
|
||||
|
||||
return filesize($logfile);
|
||||
}
|
||||
|
||||
private function getLog(int $lines = 100)
|
||||
{
|
||||
if (!in_array(getenv('APP_ENV'), ['test', 'dev', 'prod'])) {
|
||||
return [
|
||||
'Unsupported log environment'
|
||||
];
|
||||
}
|
||||
|
||||
$logfileName = 'var/log/' . getenv('APP_ENV') . '.log';
|
||||
$logfile = $this->projectDirectory . '/' . $logfileName;
|
||||
|
||||
if (!file_exists($logfile)) {
|
||||
return [
|
||||
'Could not find logfile: ' . $logfileName
|
||||
];
|
||||
}
|
||||
|
||||
$file = new \SplFileObject($logfile, 'r');
|
||||
$file->seek($file->getSize());
|
||||
$last_line = $file->key();
|
||||
while ($last_line - $lines < 0) {
|
||||
$lines--;
|
||||
}
|
||||
$lines = new \LimitIterator($file, $last_line - $lines, $last_line);
|
||||
|
||||
return iterator_to_array($lines);
|
||||
}
|
||||
|
||||
private function getFilePermissions()
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach (self::DIRECTORIES_WRITABLE as $path) {
|
||||
$results[$path] = false;
|
||||
$fullPath = $this->projectDirectory . '/' . $path;
|
||||
$fullUri = realpath($fullPath);
|
||||
|
||||
if ($fullUri === false && !file_exists($fullPath)) {
|
||||
@mkdir($fullPath);
|
||||
$fullUri = realpath($fullPath);
|
||||
}
|
||||
|
||||
if ($fullUri !== false && is_readable($fullUri) && is_writable($fullUri)) {
|
||||
$results[$path] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function getEnvVars()
|
||||
{
|
||||
return [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'MAILER_FROM' => getenv('MAILER_FROM'),
|
||||
'CORS_ALLOW_ORIGIN' => getenv('CORS_ALLOW_ORIGIN'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getIniSettings()
|
||||
{
|
||||
$ini = [
|
||||
'allow_url_fopen',
|
||||
'allow_url_include',
|
||||
'default_charset',
|
||||
'default_mimetype',
|
||||
'display_errors',
|
||||
'error_log',
|
||||
'error_reporting',
|
||||
'log_errors',
|
||||
'max_execution_time',
|
||||
'memory_limit',
|
||||
'open_basedir',
|
||||
'post_max_size',
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
foreach ($ini as $name) {
|
||||
try {
|
||||
$settings[$name] = ini_get($name);
|
||||
} catch (\Exception $ex) {
|
||||
$settings[$name] = "Couldn't load ini setting: " . $ex->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author https://php.net/manual/en/function.phpinfo.php#117961
|
||||
* @return array
|
||||
*/
|
||||
private function getPhpInfo()
|
||||
{
|
||||
$plainText = function ($input) {
|
||||
return trim(html_entity_decode(strip_tags($input)));
|
||||
};
|
||||
|
||||
ob_start();
|
||||
phpinfo(1);
|
||||
|
||||
$phpinfo = ['phpinfo' => []];
|
||||
|
||||
if (preg_match_all(
|
||||
'#(?:<h2.*?>(?:<a.*?>)?(.*?)(?:<\/a>)?<\/h2>)|' .
|
||||
'(?:<tr.*?><t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>)?)?</tr>)#s',
|
||||
ob_get_clean(),
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
)) {
|
||||
foreach ($matches as $match) {
|
||||
$fn = $plainText;
|
||||
if (isset($match[3])) {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][$fn($match[2])] = isset($match[4]) ? [$fn($match[3]), $fn($match[4])] : $fn($match[3]);
|
||||
} else {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][] = $fn($match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$phpInfo = $phpinfo['phpinfo'];
|
||||
unset($phpInfo[0]);
|
||||
unset($phpInfo[1]);
|
||||
|
||||
return $phpInfo;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::USERS);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
140
src/DataFixtures/TeamFixtures.php
Normal file
140
src/DataFixtures/TeamFixtures.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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\DataFixtures;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class TeamFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
public const AMOUNT_TEAMS = 10;
|
||||
public const MAX_USERS_PER_TEAM = 15;
|
||||
public const MAX_PROJECTS_PER_TEAM = 5;
|
||||
|
||||
// lower batch size, as user preferences are added in the same run
|
||||
public const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDependencies()
|
||||
{
|
||||
return [
|
||||
UserFixtures::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return User[]
|
||||
*/
|
||||
protected function getAllUsers(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var User[] $entries */
|
||||
$entries = $manager->getRepository(User::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Project[]
|
||||
*/
|
||||
protected function getAllProjects(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
|
||||
/* @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$allUsers = $this->getAllUsers($manager);
|
||||
$allProjects = $this->getAllProjects($manager);
|
||||
$faker = Factory::create();
|
||||
|
||||
for ($i = 1; $i <= self::AMOUNT_TEAMS; $i++) {
|
||||
$maxUsers = count($allUsers) - 1;
|
||||
if (self::MAX_USERS_PER_TEAM < $maxUsers) {
|
||||
$maxUsers = self::MAX_USERS_PER_TEAM;
|
||||
}
|
||||
$userCount = mt_rand(0, $maxUsers);
|
||||
|
||||
$maxProjects = count($allProjects) - 1;
|
||||
if (self::MAX_PROJECTS_PER_TEAM < $maxProjects) {
|
||||
$maxProjects = self::MAX_PROJECTS_PER_TEAM;
|
||||
}
|
||||
$projectCount = mt_rand(0, $maxProjects);
|
||||
|
||||
$team = new Team();
|
||||
$team
|
||||
->setName($faker->company . ' ' . $i)
|
||||
->setTeamLead($allUsers[array_rand($allUsers)])
|
||||
;
|
||||
|
||||
if ($userCount > 0) {
|
||||
$userKeys = array_rand($allUsers, $userCount);
|
||||
if (!is_array($userKeys)) {
|
||||
$userKeys = [$userKeys];
|
||||
}
|
||||
foreach ($userKeys as $userKey) {
|
||||
$team->addUser($allUsers[$userKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($projectCount > 0) {
|
||||
$projectKeys = array_rand($allProjects, $projectCount);
|
||||
if (!is_array($projectKeys)) {
|
||||
$projectKeys = [$projectKeys];
|
||||
}
|
||||
foreach ($projectKeys as $projectKey) {
|
||||
$team->addProject($allProjects[$projectKey]);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->persist($team);
|
||||
|
||||
if ($i % self::BATCH_SIZE === 0) {
|
||||
$manager->flush();
|
||||
$manager->clear(Team::class);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear(Team::class);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
|
||||
$manager->persist($entry);
|
||||
|
||||
if ($i % self::BATCH_SIZE == 0) {
|
||||
if ($i % self::BATCH_SIZE === 0) {
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class UserFixtures extends Fixture
|
||||
public const USERNAME_ADMIN = 'anna_admin';
|
||||
public const USERNAME_SUPER_ADMIN = 'susan_super';
|
||||
|
||||
public const AMOUNT_EXTRA_USER = 2;
|
||||
public const AMOUNT_EXTRA_USER = 25;
|
||||
|
||||
public const MIN_RATE = 30;
|
||||
public const MAX_RATE = 120;
|
||||
@@ -158,7 +158,6 @@ class UserFixtures extends Fixture
|
||||
->setUsername($username)
|
||||
->setEmail($email)
|
||||
->setRoles([User::ROLE_USER])
|
||||
->setAvatar(self::DEFAULT_AVATAR)
|
||||
->setEnabled(true)
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
->setPreferences($this->getUserPreferences($user))
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\DependencyInjection;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
@@ -435,7 +436,7 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayPrototype()
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('type')->defaultValue('simple')->end()
|
||||
->scalarNode('type')->defaultValue(CompoundRow::class)->end()
|
||||
->integerNode('order')->defaultValue(0)->end()
|
||||
->scalarNode('title')->end()
|
||||
->scalarNode('permission')->defaultNull()->end()
|
||||
|
||||
@@ -138,6 +138,14 @@ class Activity implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.4
|
||||
*/
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
|
||||
@@ -247,6 +247,14 @@ class Customer implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.4
|
||||
*/
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
|
||||
@@ -166,8 +166,13 @@ class Project implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @deprecated since 1.4
|
||||
*/
|
||||
public function getVisible(): bool
|
||||
{
|
||||
|
||||
@@ -246,10 +246,10 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $duration
|
||||
* @param int|null $duration
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDuration($duration): Timesheet
|
||||
public function setDuration(?int $duration): Timesheet
|
||||
{
|
||||
$this->duration = $duration;
|
||||
|
||||
@@ -259,9 +259,9 @@ class Timesheet implements EntityWithMetaFields, InvoiceItemInterface
|
||||
/**
|
||||
* Do not rely on the results of this method for running records.
|
||||
*
|
||||
* @return int
|
||||
* @return int|null
|
||||
*/
|
||||
public function getDuration(): int
|
||||
public function getDuration(): ?int
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
@@ -159,6 +159,12 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], $this->getIcon('configuration'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_information')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('doctor', 'menu.doctor', 'doctor', [], $this->getIcon('doctor'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getIcon(string $icon)
|
||||
|
||||
@@ -105,6 +105,9 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
if (null === $duration) {
|
||||
$duration = 0;
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,15 @@ trait RendererTrait
|
||||
];
|
||||
}
|
||||
|
||||
$duration = $timesheet->getDuration();
|
||||
if (null === $duration) {
|
||||
$duration = 0;
|
||||
}
|
||||
|
||||
$summary[$id]['rate'] += $timesheet->getRate();
|
||||
$summary[$id]['duration'] += $timesheet->getDuration();
|
||||
$summary[$id]['duration'] += $duration;
|
||||
$summary[$id]['activities'][$activityId]['rate'] += $timesheet->getRate();
|
||||
$summary[$id]['activities'][$activityId]['duration'] += $timesheet->getDuration();
|
||||
$summary[$id]['activities'][$activityId]['duration'] += $duration;
|
||||
}
|
||||
|
||||
asort($summary);
|
||||
|
||||
@@ -54,6 +54,12 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
return;
|
||||
}
|
||||
|
||||
// expanded selects are rendered as checkboxes and using the selectpicker
|
||||
// would display an empty dropdown
|
||||
if (isset($options['expanded']) && true === $options['expanded']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($view->vars['attr'])) {
|
||||
$view->vars['attr'] = [];
|
||||
}
|
||||
|
||||
@@ -219,10 +219,10 @@ class TimesheetEditForm extends AbstractType
|
||||
/** @var Project $project */
|
||||
$project = $repo->find($project);
|
||||
if (null !== $project) {
|
||||
if (!$project->getCustomer()->getVisible()) {
|
||||
if (!$project->getCustomer()->isVisible()) {
|
||||
$customer = null;
|
||||
$project = null;
|
||||
} elseif (!$project->getVisible()) {
|
||||
} elseif (!$project->isVisible()) {
|
||||
$project = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ abstract class AbstractCalculator
|
||||
{
|
||||
$time = 0;
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
if (null === $entry->getFixedRate()) {
|
||||
if (null === $entry->getFixedRate() && null !== $entry->getDuration()) {
|
||||
$time += $entry->getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,15 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
|
||||
|
||||
protected function mergeInvoiceItems(InvoiceItem $invoiceItem, InvoiceItemInterface $entry)
|
||||
{
|
||||
$duration = $invoiceItem->getDuration();
|
||||
if (null !== $entry->getDuration()) {
|
||||
$duration += $entry->getDuration();
|
||||
}
|
||||
|
||||
$invoiceItem->setAmount($invoiceItem->getAmount() + 1);
|
||||
$invoiceItem->setUser($entry->getUser());
|
||||
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
|
||||
$invoiceItem->setDuration($invoiceItem->getDuration() + $entry->getDuration());
|
||||
$invoiceItem->setDuration($duration);
|
||||
|
||||
if (null !== $entry->getFixedRate()) {
|
||||
/*
|
||||
|
||||
@@ -32,7 +32,7 @@ interface InvoiceItemInterface
|
||||
|
||||
public function getEnd(): ?\DateTime;
|
||||
|
||||
public function getDuration(): int;
|
||||
public function getDuration(): ?int;
|
||||
|
||||
public function getDescription(): ?string;
|
||||
|
||||
|
||||
@@ -9,12 +9,28 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Project;
|
||||
|
||||
class ProjectStatistic extends TimesheetCountedStatistic
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $activityAmount = 0;
|
||||
private $activityAmount = 0;
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
|
||||
@@ -13,6 +13,9 @@ use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ActivityIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Repository\Loader;
|
||||
use App\Entity\Customer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CustomerIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Repository\Loader;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ProjectIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Repository\Loader;
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TeamIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,9 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TimesheetIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
|
||||
76
src/Repository/Loader/UserIdLoader.php
Normal file
76
src/Repository/Loader/UserIdLoader.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UserIdLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
*/
|
||||
public function loadResults(array $ids): void
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var User[] $users */
|
||||
$users = $qb->select('PARTIAL u.{id}', 'teams')
|
||||
->from(User::class, 'u')
|
||||
->leftJoin('u.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('u.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$teamIds = [];
|
||||
foreach ($users as $user) {
|
||||
foreach ($user->getTeams() as $team) {
|
||||
$teamIds[] = $team->getId();
|
||||
}
|
||||
}
|
||||
|
||||
if (count($teamIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'teamlead')
|
||||
->from(Team::class, 't')
|
||||
->leftJoin('t.teamlead', 'teamlead')
|
||||
->andWhere($qb->expr()->in('t.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL t.{id}', 'users')
|
||||
->from(Team::class, 't')
|
||||
->leftJoin('t.users', 'users')
|
||||
->andWhere($qb->expr()->in('t.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Repository/Loader/UserLoader.php
Normal file
38
src/Repository/Loader/UserLoader.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Loader;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class UserLoader implements LoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var UserIdLoader
|
||||
*/
|
||||
private $loader;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->loader = new UserIdLoader($entityManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User[] $users
|
||||
*/
|
||||
public function loadResults(array $users): void
|
||||
{
|
||||
$ids = array_map(function (User $user) {
|
||||
return $user->getId();
|
||||
}, $users);
|
||||
|
||||
$this->loader->loadResults($ids);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class ProjectRepository extends EntityRepository
|
||||
|
||||
public function getProjectStatistics(Project $project): ProjectStatistic
|
||||
{
|
||||
$stats = new ProjectStatistic();
|
||||
$stats = new ProjectStatistic($project);
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
|
||||
@@ -25,8 +25,17 @@ class BaseQuery
|
||||
public const DEFAULT_PAGESIZE = 50;
|
||||
public const DEFAULT_PAGE = 1;
|
||||
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
*/
|
||||
public const RESULT_TYPE_OBJECTS = 'Objects';
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
*/
|
||||
public const RESULT_TYPE_PAGER = 'PagerFanta';
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
*/
|
||||
public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
|
||||
|
||||
private $defaults = [
|
||||
@@ -54,6 +63,7 @@ class BaseQuery
|
||||
private $order = self::ORDER_ASC;
|
||||
/**
|
||||
* @var string
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
*/
|
||||
private $resultType = self::RESULT_TYPE_PAGER;
|
||||
/**
|
||||
@@ -179,28 +189,11 @@ class BaseQuery
|
||||
*/
|
||||
public function getResultType()
|
||||
{
|
||||
@trigger_error('BaseQuery::getResultType() is deprecated and will be removed with 1.6', E_USER_DEPRECATED);
|
||||
|
||||
return $this->resultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.0
|
||||
* @param string $resultType
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setResultType(string $resultType)
|
||||
{
|
||||
$allowed = [self::RESULT_TYPE_PAGER, self::RESULT_TYPE_QUERYBUILDER, self::RESULT_TYPE_OBJECTS];
|
||||
|
||||
if (!in_array($resultType, $allowed)) {
|
||||
throw new \InvalidArgumentException('Unsupported query result type');
|
||||
}
|
||||
|
||||
$this->resultType = $resultType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSearchTerm(): bool
|
||||
{
|
||||
return null !== $this->searchTerm;
|
||||
|
||||
@@ -1,54 +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\Repository;
|
||||
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* @deprecated since 1.0
|
||||
*/
|
||||
trait RepositoryTrait
|
||||
{
|
||||
/**
|
||||
* @deprecated since 1.0
|
||||
* @param QueryBuilder $qb
|
||||
* @param BaseQuery $query
|
||||
* @return QueryBuilder|Pagerfanta|array
|
||||
*/
|
||||
protected function getBaseQueryResult(QueryBuilder $qb, BaseQuery $query)
|
||||
{
|
||||
if (BaseQuery::RESULT_TYPE_PAGER === $query->getResultType()) {
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
} elseif (BaseQuery::RESULT_TYPE_OBJECTS === $query->getResultType()) {
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query $query
|
||||
* @param int $page
|
||||
* @param int $maxPerPage
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
private function getPager(Query $query, $page = 1, $maxPerPage = 25)
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($query, false));
|
||||
$paginator->setMaxPerPage($maxPerPage);
|
||||
$paginator->setCurrentPage($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
@@ -341,7 +341,10 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
$duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp();
|
||||
$durationPercent = $duration / $result->getDuration();
|
||||
$durationPercent = 0;
|
||||
if ($result->getDuration() !== null && $result->getDuration() > 0) {
|
||||
$durationPercent = $duration / $result->getDuration();
|
||||
}
|
||||
$rate = $result->getRate() * $durationPercent;
|
||||
|
||||
$results[$dateKey]['rate'] += $rate;
|
||||
@@ -605,7 +608,9 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
if (!empty($query->getTeams())) {
|
||||
foreach ($query->getTeams() as $team) {
|
||||
$user = array_merge($user, $team->getUsers()->toArray());
|
||||
foreach ($team->getUsers() as $teamUser) {
|
||||
$user[] = $teamUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,20 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Loader\UserLoader;
|
||||
use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
|
||||
class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
{
|
||||
use RepositoryTrait;
|
||||
|
||||
public function getById($id): ?User
|
||||
{
|
||||
@trigger_error('UserRepository::getById is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
@@ -81,8 +85,84 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
/**
|
||||
* @param UserQuery $query
|
||||
* @return array|\Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
* @deprecated since 1.4, use getUsersForQuery() instead
|
||||
*/
|
||||
public function findByQuery(UserQuery $query)
|
||||
{
|
||||
@trigger_error('UserRepository::findByQuery() is deprecated and will be removed with 1.6', E_USER_DEPRECATED);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
if (BaseQuery::RESULT_TYPE_PAGER === $query->getResultType()) {
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($qb->getQuery(), false));
|
||||
$paginator->setMaxPerPage($query->getPageSize());
|
||||
$paginator->setCurrentPage($query->getPage());
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
if (BaseQuery::RESULT_TYPE_OBJECTS === $query->getResultType()) {
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @return mixed|null|\Symfony\Component\Security\Core\User\UserInterface
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p', 't', 'tu', 'tl')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->leftJoin('u.teams', 't')
|
||||
->leftJoin('t.users', 'tu')
|
||||
->leftJoin('t.teamlead', 'tl')
|
||||
->where('u.username = :username')
|
||||
->orWhere('u.email = :username')
|
||||
->setParameter('username', $username)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
}
|
||||
|
||||
public function getQueryBuilderForFormType(UserFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('u.enabled', ':enabled'));
|
||||
$qb->setParameter('enabled', true, \PDO::PARAM_BOOL);
|
||||
|
||||
$qb->orderBy('u.username', 'ASC');
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that admins see all user
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->leftJoin('u.teams', 'teams')
|
||||
->leftJoin('teams.users', 'users')
|
||||
->andWhere('teams.teamlead = :id')
|
||||
->setParameter('id', $user);
|
||||
}
|
||||
}
|
||||
|
||||
private function getQueryBuilderForQuery(UserQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
@@ -144,61 +224,55 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @return mixed|null|\Symfony\Component\Security\Core\User\UserInterface
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p', 't', 'tu', 'tl')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->leftJoin('u.teams', 't')
|
||||
->leftJoin('t.users', 'tu')
|
||||
->leftJoin('t.teamlead', 'tl')
|
||||
->where('u.username = :username')
|
||||
->orWhere('u.email = :username')
|
||||
->setParameter('username', $username)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
}
|
||||
|
||||
public function getQueryBuilderForFormType(UserFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('u.enabled', ':enabled'));
|
||||
$qb->setParameter('enabled', true, \PDO::PARAM_BOOL);
|
||||
|
||||
$qb->orderBy('u.username', 'ASC');
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
public function getPagerfantaForQuery(UserQuery $query): Pagerfanta
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
if (null === $user && empty($teams)) {
|
||||
return;
|
||||
}
|
||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||
$paginator->setMaxPerPage($query->getPageSize());
|
||||
$paginator->setCurrentPage($query->getPage());
|
||||
|
||||
// make sure that admins see all user
|
||||
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||
return;
|
||||
}
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->leftJoin('u.teams', 'teams')
|
||||
->leftJoin('teams.users', 'users')
|
||||
->andWhere('teams.teamlead = :id')
|
||||
->setParameter('id', $user);
|
||||
}
|
||||
protected function getPaginatorForQuery(UserQuery $query): PaginatorInterface
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->countDistinct('u.id'))
|
||||
;
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return new LoaderPaginator(new UserLoader($qb->getEntityManager()), $qb, $counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserQuery $query
|
||||
* @return User[]
|
||||
*/
|
||||
public function getUsersForQuery(UserQuery $query): iterable
|
||||
{
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
return $this->getHydratedResultsByQuery($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @return User[]
|
||||
*/
|
||||
protected function getHydratedResultsByQuery(QueryBuilder $qb): iterable
|
||||
{
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$loader = new UserLoader($qb->getEntityManager());
|
||||
$loader->loadResults($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace App\Repository;
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\YearChart;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetInterface;
|
||||
|
||||
@@ -79,6 +81,12 @@ class WidgetRepository
|
||||
return $this->widgets[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $widget
|
||||
* @return WidgetInterface
|
||||
* @throws WidgetException
|
||||
*/
|
||||
protected function create(string $name, array $widget): WidgetInterface
|
||||
{
|
||||
$user = $this->user;
|
||||
@@ -88,23 +96,33 @@ class WidgetRepository
|
||||
|
||||
if (!isset($widget['type'])) {
|
||||
@trigger_error('Using a widget definition without a "type" is deprecated', E_USER_DEPRECATED);
|
||||
$widget['type'] = 'counter';
|
||||
$widget['type'] = Counter::class;
|
||||
}
|
||||
|
||||
$widgetClassName = '\\App\\Widget\\Type\\' . ucfirst($widget['type']);
|
||||
$widgetClassName = ucfirst($widget['type']);
|
||||
if (!class_exists($widgetClassName)) {
|
||||
throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetClassName));
|
||||
}
|
||||
|
||||
$model = new \ReflectionClass($widgetClassName);
|
||||
if (!$model->isSubclassOf(AbstractWidgetType::class)) {
|
||||
throw new WidgetException(sprintf('Invalid widget type "%s" does not extend AbstractWidgetType', $widgetClassName));
|
||||
}
|
||||
|
||||
$data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser);
|
||||
|
||||
/** @var AbstractWidgetType $model */
|
||||
$model = new $widgetClassName();
|
||||
if (!($model instanceof AbstractWidgetType)) {
|
||||
throw new WidgetException(
|
||||
sprintf(
|
||||
'Widget type "%s" is not an instance of "%s"',
|
||||
$widgetClassName,
|
||||
AbstractWidgetType::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser);
|
||||
} catch (\Exception $ex) {
|
||||
throw new WidgetException(
|
||||
'Failed loading widget data: ' . $ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$model
|
||||
->setId($name)
|
||||
->setTitle($widget['title'])
|
||||
@@ -140,7 +158,7 @@ class WidgetRepository
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'green',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationWeek' => [
|
||||
'title' => 'stats.durationWeek',
|
||||
@@ -150,7 +168,7 @@ class WidgetRepository
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'blue',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationMonth' => [
|
||||
'title' => 'stats.durationMonth',
|
||||
@@ -160,7 +178,7 @@ class WidgetRepository
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'purple',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
@@ -170,7 +188,7 @@ class WidgetRepository
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
@@ -178,7 +196,7 @@ class WidgetRepository
|
||||
'user' => true,
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountToday' => [
|
||||
'title' => 'stats.amountToday',
|
||||
@@ -188,7 +206,7 @@ class WidgetRepository
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'green',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountWeek' => [
|
||||
'title' => 'stats.amountWeek',
|
||||
@@ -198,7 +216,7 @@ class WidgetRepository
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'blue',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountMonth' => [
|
||||
'title' => 'stats.amountMonth',
|
||||
@@ -208,7 +226,7 @@ class WidgetRepository
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'purple',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
@@ -218,7 +236,7 @@ class WidgetRepository
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
@@ -226,7 +244,7 @@ class WidgetRepository
|
||||
'user' => true,
|
||||
'icon' => 'money',
|
||||
'color' => 'red',
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationToday' => [
|
||||
'title' => 'stats.durationToday',
|
||||
@@ -236,7 +254,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationWeek' => [
|
||||
'title' => 'stats.durationWeek',
|
||||
@@ -246,7 +264,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationMonth' => [
|
||||
'title' => 'stats.durationMonth',
|
||||
@@ -256,7 +274,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
@@ -266,7 +284,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
@@ -274,7 +292,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountToday' => [
|
||||
'title' => 'stats.amountToday',
|
||||
@@ -284,7 +302,7 @@ class WidgetRepository
|
||||
'icon' => 'money',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountWeek' => [
|
||||
'title' => 'stats.amountWeek',
|
||||
@@ -294,7 +312,7 @@ class WidgetRepository
|
||||
'icon' => 'money',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountMonth' => [
|
||||
'title' => 'stats.amountMonth',
|
||||
@@ -304,7 +322,7 @@ class WidgetRepository
|
||||
'icon' => 'money',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
@@ -314,7 +332,7 @@ class WidgetRepository
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
@@ -322,7 +340,7 @@ class WidgetRepository
|
||||
'icon' => 'money',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersToday' => [
|
||||
'title' => 'stats.userActiveToday',
|
||||
@@ -332,7 +350,7 @@ class WidgetRepository
|
||||
'icon' => 'user',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersWeek' => [
|
||||
'title' => 'stats.userActiveWeek',
|
||||
@@ -342,7 +360,7 @@ class WidgetRepository
|
||||
'icon' => 'user',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersMonth' => [
|
||||
'title' => 'stats.userActiveMonth',
|
||||
@@ -352,7 +370,7 @@ class WidgetRepository
|
||||
'icon' => 'user',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersYear' => [
|
||||
'title' => 'stats.userActiveYear',
|
||||
@@ -362,7 +380,7 @@ class WidgetRepository
|
||||
'icon' => 'user',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersTotal' => [
|
||||
'title' => 'stats.userActiveTotal',
|
||||
@@ -370,7 +388,7 @@ class WidgetRepository
|
||||
'icon' => 'user',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeRecordings' => [
|
||||
'title' => 'stats.activeRecordings',
|
||||
@@ -378,7 +396,7 @@ class WidgetRepository
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userRecapThisYear' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
@@ -388,7 +406,7 @@ class WidgetRepository
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'color' => '',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
'type' => YearChart::class,
|
||||
],
|
||||
'userRecapLastYear' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
@@ -398,7 +416,7 @@ class WidgetRepository
|
||||
'end' => '31 december last year 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.7)|#3b8bba',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
'type' => YearChart::class,
|
||||
],
|
||||
'userRecapTwoYears' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
@@ -408,7 +426,7 @@ class WidgetRepository
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.6)|#3b8bba;rgba(233,233,233,0.8)|#ccc',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
'type' => YearChart::class,
|
||||
],
|
||||
'userRecapThreeYears' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
@@ -418,7 +436,7 @@ class WidgetRepository
|
||||
'end' => 'this year last day of december 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.4)|#3b8bba;rgba(233,233,233,0.7)|#ccc;rgba(210,214,222,0.9)|#c1c7d1',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
'type' => YearChart::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ class RateCalculator implements CalculatorInterface
|
||||
$factor = $this->getRateFactor($record);
|
||||
|
||||
$hourlyRate = (float) ($hourlyRate * $factor);
|
||||
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
$rate = 0;
|
||||
if (null !== $record->getDuration()) {
|
||||
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
}
|
||||
|
||||
$record->setHourlyRate($hourlyRate);
|
||||
$record->setRate($rate);
|
||||
|
||||
59
src/Twig/AvatarExtension.php
Normal file
59
src/Twig/AvatarExtension.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -135,12 +135,11 @@ class Extensions extends AbstractExtension
|
||||
}
|
||||
|
||||
if ($duration instanceof Timesheet) {
|
||||
$seconds = $duration->getDuration();
|
||||
if (null === $duration->getEnd()) {
|
||||
$seconds = time() - $duration->getBegin()->getTimestamp();
|
||||
$duration = time() - $duration->getBegin()->getTimestamp();
|
||||
} else {
|
||||
$duration = $duration->getDuration();
|
||||
}
|
||||
|
||||
$duration = $seconds;
|
||||
}
|
||||
|
||||
return (int) $duration;
|
||||
|
||||
@@ -75,6 +75,8 @@ final class IconExtension extends AbstractExtension
|
||||
'configuration' => 'fas fa-cogs',
|
||||
'mail-sent' => 'fas fa-paper-plane',
|
||||
'mail' => 'fas fa-envelope-open',
|
||||
'doctor' => 'fas fa-medkit',
|
||||
'success' => 'fas fa-check',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
155
src/Utils/AvatarService.php
Normal file
155
src/Utils/AvatarService.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?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 $projectDirectory;
|
||||
|
||||
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->projectDirectory = $projectDirectory;
|
||||
}
|
||||
|
||||
private function getAvatarUrl(User $profile): string
|
||||
{
|
||||
return '/avatars/' . md5($profile->getId() . '_' . $profile->getDisplayName()) . '.png';
|
||||
}
|
||||
|
||||
private function getImagePath(User $profile): string
|
||||
{
|
||||
$avatarPath = realpath($this->projectDirectory . '/public/');
|
||||
|
||||
return $avatarPath . $this->getAvatarUrl($profile);
|
||||
}
|
||||
|
||||
public function generateAvatar(User $profile, bool $regenerate = false): bool
|
||||
{
|
||||
if (!extension_loaded('gd')) {
|
||||
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);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ class TimesheetValidator extends ConstraintValidator
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd() && $activity->getVisible() === false) {
|
||||
if (null === $timesheet->getEnd() && !$activity->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
@@ -170,7 +170,7 @@ class TimesheetValidator extends ConstraintValidator
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd() && $project->getVisible() === false) {
|
||||
if (null === $timesheet->getEnd() && !$project->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
@@ -178,7 +178,7 @@ class TimesheetValidator extends ConstraintValidator
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd() && $project->getCustomer()->getVisible() === false) {
|
||||
if (null === $timesheet->getEnd() && !$project->getCustomer()->isVisible()) {
|
||||
$context->buildViolation('Cannot start a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
|
||||
@@ -140,11 +140,11 @@ class TimesheetVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$timesheet->getActivity()->getVisible() || !$timesheet->getProject()->getVisible()) {
|
||||
if (!$timesheet->getActivity()->isVisible() || !$timesheet->getProject()->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$timesheet->getProject()->getCustomer()->getVisible()) {
|
||||
if (!$timesheet->getProject()->getCustomer()->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
21
src/Widget/Type/AuthorizedWidget.php
Normal file
21
src/Widget/Type/AuthorizedWidget.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
interface AuthorizedWidget
|
||||
{
|
||||
/**
|
||||
* Return a list of granted syntax string.
|
||||
* If ANY of the given permission strings matches, access is granted.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array;
|
||||
}
|
||||
83
src/Widget/Type/UserTeamProjects.php
Normal file
83
src/Widget/Type/UserTeamProjects.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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\Widget\Type;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserTeamProjects extends SimpleWidget implements AuthorizedWidget
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(CurrentUser $user, ProjectRepository $repository)
|
||||
{
|
||||
$this->setId('UserTeamProjects');
|
||||
$this->setTitle('label.my_team_projects');
|
||||
$this->setOptions([
|
||||
'user' => $user->getUser(),
|
||||
'id' => '',
|
||||
]);
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
$options = parent::getOptions($options);
|
||||
|
||||
if (empty($options['id'])) {
|
||||
$options['id'] = uniqid('UserTeamProjects_');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
/** @var User $user */
|
||||
$user = $options['user'];
|
||||
$projects = [];
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($user->getTeams() as $team) {
|
||||
/** @var Project $project */
|
||||
foreach ($team->getProjects() as $project) {
|
||||
if (!$project->isVisible() || !$project->getCustomer()->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
$projects[$project->getId()] = $project;
|
||||
}
|
||||
}
|
||||
|
||||
$stats = [];
|
||||
|
||||
foreach ($projects as $id => $project) {
|
||||
if ($project->getBudget() > 0 || $project->getTimeBudget() > 0) {
|
||||
$stats[] = $this->repository->getProjectStatistics($project);
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['budget_team_project', 'budget_teamlead_project', 'budget_project'];
|
||||
}
|
||||
}
|
||||
54
src/Widget/Type/UserTeams.php
Normal file
54
src/Widget/Type/UserTeams.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserTeams extends SimpleWidget implements AuthorizedWidget
|
||||
{
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
$this->setId('UserTeams');
|
||||
$this->setTitle('label.teams');
|
||||
$this->setOptions([
|
||||
'user' => $user->getUser(),
|
||||
'id' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
$options = parent::getOptions($options);
|
||||
|
||||
if (empty($options['id'])) {
|
||||
$options['id'] = uniqid('UserTeams_');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
/** @var User $user */
|
||||
$user = $options['user'];
|
||||
|
||||
return $user->getTeams();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return ['view_team_member', 'view_team'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user