link reporting screen for users and month charts (#1842)

This commit is contained in:
Kevin Papst
2020-07-26 20:28:48 +02:00
committed by GitHub
parent 130e6ac057
commit 599b18ac99
9 changed files with 80 additions and 23 deletions

View File

@@ -20,6 +20,7 @@ use App\Form\UserRolesType;
use App\Form\UserTeamsType; use App\Form\UserTeamsType;
use App\Repository\TeamRepository; use App\Repository\TeamRepository;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Utils\LocaleSettings;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -69,7 +70,7 @@ class ProfileController extends AbstractController
* @Route(path="/{username}", name="user_profile", methods={"GET"}) * @Route(path="/{username}", name="user_profile", methods={"GET"})
* @Security("is_granted('view', profile)") * @Security("is_granted('view', profile)")
*/ */
public function indexAction(User $profile, TimesheetRepository $repository) public function indexAction(User $profile, TimesheetRepository $repository, LocaleSettings $localeSettings)
{ {
$userStats = $repository->getUserStatistics($profile); $userStats = $repository->getUserStatistics($profile);
$monthlyStats = $repository->getMonthlyStats($profile); $monthlyStats = $repository->getMonthlyStats($profile);
@@ -79,6 +80,7 @@ class ProfileController extends AbstractController
'user' => $profile, 'user' => $profile,
'stats' => $userStats, 'stats' => $userStats,
'years' => $monthlyStats, 'years' => $monthlyStats,
'stat_date_format' => $localeSettings->getDatePickerFormat(),
]; ];
return $this->render('user/stats.html.twig', $viewVars); return $this->render('user/stats.html.twig', $viewVars);

View File

@@ -64,21 +64,23 @@ final class ReportingController extends AbstractController
$values->setDate($this->dateTimeFactory->getStartOfMonth()); $values->setDate($this->dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthByUserForm::class, $values, [ $form = $this->createForm(MonthByUserForm::class, $values, [
'method' => 'POST',
'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(), 'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(),
]); ]);
$form->handleRequest($request); $form->submit($request->query->all(), false);
if ($form->isSubmitted() && !$form->isValid()) { if ($values->getUser() === null) {
$values->setUser($user); $values->setUser($user);
$values->setDate($this->dateTimeFactory->getStartOfMonth());
} }
if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) { if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) {
throw new AccessDeniedException('User is not allowed to see other users timesheet'); throw new AccessDeniedException('User is not allowed to see other users timesheet');
} }
if ($values->getDate() === null) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
}
$start = $values->getDate(); $start = $values->getDate();
$start->modify('first day of 00:00:00'); $start->modify('first day of 00:00:00');
@@ -124,16 +126,18 @@ final class ReportingController extends AbstractController
$values = new MonthlyUserList(); $values = new MonthlyUserList();
$values->setDate($this->dateTimeFactory->getStartOfMonth()); $values->setDate($this->dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthlyUserListForm::class, $values, [ $form = $this->createForm(MonthlyUserListForm::class, $values, []);
'method' => 'POST',
]);
$form->handleRequest($request); $form->submit($request->query->all(), false);
if ($form->isSubmitted() && !$form->isValid()) { if ($form->isSubmitted() && !$form->isValid()) {
$values->setDate($this->dateTimeFactory->getStartOfMonth()); $values->setDate($this->dateTimeFactory->getStartOfMonth());
} }
if ($values->getDate() === null) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
}
$start = $values->getDate(); $start = $values->getDate();
$start->modify('first day of 00:00:00'); $start->modify('first day of 00:00:00');

View File

@@ -20,6 +20,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/** /**
* Custom form field type to select a month via picker and select previous and next month. * Custom form field type to select a month via picker and select previous and next month.
*
* Always falls back to the current month if none or an invalid date is given.
*/ */
final class MonthPickerType extends AbstractType final class MonthPickerType extends AbstractType
{ {
@@ -67,6 +69,7 @@ final class MonthPickerType extends AbstractType
$date = $this->dateTime->getStartOfMonth(); $date = $this->dateTime->getStartOfMonth();
} }
$view->vars['month'] = $date;
$view->vars['previousMonth'] = (clone $date)->modify('-1 month'); $view->vars['previousMonth'] = (clone $date)->modify('-1 month');
$view->vars['nextMonth'] = (clone $date)->modify('+1 month'); $view->vars['nextMonth'] = (clone $date)->modify('+1 month');
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($this->localeSettings->getDateTypeFormat()); $view->vars['momentFormat'] = (new MomentFormatConverter())->convert($this->localeSettings->getDateTypeFormat());

View File

@@ -17,6 +17,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class MonthByUserForm extends AbstractType class MonthByUserForm extends AbstractType
{ {
/**
* Simplify cross linking between pages by removing the block prefix.
*
* @return null|string
*/
public function getBlockPrefix()
{
return null;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -37,6 +47,8 @@ class MonthByUserForm extends AbstractType
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => MonthByUser::class, 'data_class' => MonthByUser::class,
'include_user' => false, 'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]); ]);
} }
} }

View File

@@ -16,6 +16,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class MonthlyUserListForm extends AbstractType class MonthlyUserListForm extends AbstractType
{ {
/**
* Simplify cross linking between pages by removing the block prefix.
*
* @return null|string
*/
public function getBlockPrefix()
{
return null;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -31,6 +41,8 @@ class MonthlyUserListForm extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => MonthlyUserList::class, 'data_class' => MonthlyUserList::class,
'csrf_protection' => false,
'method' => 'GET',
]); ]);
} }
} }

View File

@@ -137,13 +137,18 @@ final class LocaleHelper
if (null === $this->moneyFormatterNoCurrency) { if (null === $this->moneyFormatterNoCurrency) {
// if anyone knows a better way of achieving this, please let me know! // if anyone knows a better way of achieving this, please let me know!
$this->moneyFormatterNoCurrency = new NumberFormatter($this->locale, NumberFormatter::CURRENCY); $this->moneyFormatterNoCurrency = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
$pattern = $this->moneyFormatterNoCurrency->getPattern();
$pattern = str_replace('¤ ', '¤', $pattern); $this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::CURRENCY_CODE, '');
$pattern = str_replace(' ¤', '¤', $pattern);
$this->moneyFormatterNoCurrency->setPattern($pattern);
$this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::CURRENCY_SYMBOL, '');
$this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::CURRENCY_CODE, '');
$this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL, ''); $this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL, '');
$this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::CURRENCY_SYMBOL, '');
// don't understand why this is needed, I'd say this shouldn't be necessary after the above calls
// even worse: the logic changes either between PHP/ICU versions
$pattern = $this->moneyFormatterNoCurrency->getPattern();
$pattern = str_replace(['¤ ', ' ¤', '-¤', ' XXX', 'XXX '], '¤', $pattern);
$pattern = str_replace('XXX', '¤', $pattern);
$pattern = str_replace('¤', '', $pattern);
$this->moneyFormatterNoCurrency->setPattern($pattern);
} }
return $this->moneyFormatterNoCurrency; return $this->moneyFormatterNoCurrency;

View File

@@ -52,7 +52,7 @@
<i class="{{ 'left'|icon }}"></i> <i class="{{ 'left'|icon }}"></i>
</a> </a>
<a class="btn btn-default" href="#" onclick="return false;"> <a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">{{ data|month_name|trans ~ ' ' ~ data|date_format('Y') }}</span> <span id="{{ form.vars.id }}_month_name">{{ month|month_name|trans ~ ' ' ~ month|date_format('Y') }}</span>
</a> </a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{nextMonth|date_short}}').change()"> <a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{nextMonth|date_short}}').change()">
<i class="{{ 'right'|icon }}"></i> <i class="{{ 'right'|icon }}"></i>

View File

@@ -58,6 +58,7 @@
{% set actions = {} %} {% set actions = {} %}
{% if user.id is not empty %} {% if user.id is not empty %}
{% set view_other = is_granted('view_other_timesheet') %}
{% if is_granted('view', user) %} {% if is_granted('view', user) %}
{% set actions = actions|merge({'profile-stats': {'url': path('user_profile', {'username' : user.username})}}) %} {% set actions = actions|merge({'profile-stats': {'url': path('user_profile', {'username' : user.username})}}) %}
{% endif %} {% endif %}
@@ -70,7 +71,12 @@
{% if actions|length > 0 %} {% if actions|length > 0 %}
{% set actions = actions|merge({'divider': null}) %} {% set actions = actions|merge({'divider': null}) %}
{% endif %} {% endif %}
{% if is_granted('view_other_timesheet') and user.enabled %} {% if is_granted('view_reporting') %}
{% if view_other or app.user.id == user.id %}
{% set actions = actions|merge({'report': path('report_user_month', {'user': user.id})}) %}
{% endif %}
{% endif %}
{% if view_other and user.enabled %}
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'users': [user.id]})}) %} {% set actions = actions|merge({'timesheet': path('admin_timesheet', {'users': [user.id]})}) %}
{% endif %} {% endif %}
{% if view == 'index' and is_granted('delete', user) %} {% if view == 'index' and is_granted('delete', user) %}

View File

@@ -49,6 +49,16 @@
} }
} }
} }
{%- if is_granted('view_reporting') and (app.user.id == user.id or is_granted('view_other_timesheet')) -%}
,
onClick: function(event, elements) {
var element = elements[0];
var month = this.data.datasets[0].monthData[element._index];
var formattedMonth = moment(month).format('{{ stat_date_format }}');
var reportUrl = '{{ path('report_user_month', {'user': user.id, 'date': 'XXXXX'})|raw }}'.replace('XXXXX', formattedMonth);
document.location = reportUrl;
}
{% endif %}
}; };
} }
</script> </script>
@@ -58,7 +68,6 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %} {% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ year }}{% endblock %} {% block box_title %}{{ year }}{% endblock %}
{% block box_body %} {% block box_body %}
<div class="chart"> <div class="chart">
<canvas id="userProfileChart{{ year }}" style="height: {{ kimai_context.chart.height }}px;"></canvas> <canvas id="userProfileChart{{ year }}" style="height: {{ kimai_context.chart.height }}px;"></canvas>
</div> </div>
@@ -77,20 +86,24 @@
{% if not loop.last %},{% endif %} {% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
], ],
realData : [ realData: [
{% for month in yearStat.months %} {% for month in yearStat.months %}
'{{ month.totalDuration|duration }}' '{{ month.totalDuration|duration }}'
{% if not loop.last %},{% endif %} {% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
] ],
monthData: [
{% for month in yearStat.months %}
'{{ yearStat.year }}-{{ month.month|length == 1 ? '0'~ month.month : month.month }}-01'
{% if not loop.last %},{% endif %}
{% endfor %}
],
} }
] ]
}; };
var userProfileChartCanvas{{ year }} = $("#userProfileChart{{ year }}").get(0).getContext("2d");
var userProfileChart{{ year }} = new Chart( var userProfileChart{{ year }} = new Chart(
userProfileChartCanvas{{ year }}, { $("#userProfileChart{{ year }}").get(0).getContext("2d"), {
type: 'bar', type: 'bar',
data: userProfileChartData{{ year }}, data: userProfileChartData{{ year }},
options: userProfileChartOptions() options: userProfileChartOptions()