financial year setting + new users working time per year report (#2547)
This commit is contained in:
@@ -241,6 +241,19 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $this->getIncrement('timesheet.time_increment', $this->getTimesheetDefaultRoundingEnd(), 0);
|
||||
}
|
||||
|
||||
// ========== Company configurations ==========
|
||||
|
||||
public function getFinancialYearStart(): ?string
|
||||
{
|
||||
$start = $this->find('company.financial_year');
|
||||
|
||||
if (empty($start)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $start;
|
||||
}
|
||||
|
||||
// ========== Theme configurations ==========
|
||||
|
||||
public function isThemeColorsLimited(): bool
|
||||
|
||||
@@ -70,7 +70,11 @@ final class ProfileController extends AbstractController
|
||||
public function indexAction(User $profile, TimesheetRepository $repository, LocaleSettings $localeSettings)
|
||||
{
|
||||
$userStats = $repository->getUserStatistics($profile);
|
||||
$monthlyStats = $repository->getMonthlyStats($profile);
|
||||
|
||||
$begin = $userStats->getFirstEntry() ?? $this->getDateTimeFactory()->getStartOfMonth();
|
||||
$end = $this->getDateTimeFactory()->getEndOfMonth();
|
||||
$monthlyStats = $repository->getMonthlyStats($begin, $end, $profile);
|
||||
arsort($monthlyStats);
|
||||
|
||||
$viewVars = [
|
||||
'tab' => 'charts',
|
||||
|
||||
@@ -184,6 +184,10 @@ final class ReportByUserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Day[] $data
|
||||
* @return array
|
||||
*/
|
||||
private function prepareReportData(array $data): array
|
||||
{
|
||||
$days = [];
|
||||
@@ -194,7 +198,6 @@ final class ReportByUserController extends AbstractController
|
||||
|
||||
$rows = [];
|
||||
|
||||
/** @var Day $day */
|
||||
foreach ($data as $day) {
|
||||
$dayId = $day->getDay()->format('Ymd');
|
||||
foreach ($day->getDetails() as $id => $detail) {
|
||||
|
||||
@@ -9,12 +9,16 @@
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Controller\AbstractController;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Model\Statistic\Year;
|
||||
use App\Reporting\MonthlyUserList;
|
||||
use App\Reporting\MonthlyUserListForm;
|
||||
use App\Reporting\WeeklyUserList;
|
||||
use App\Reporting\WeeklyUserListForm;
|
||||
use App\Reporting\YearlyUserList;
|
||||
use App\Reporting\YearlyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
@@ -26,7 +30,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/reporting")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_timesheet')")
|
||||
* @Security("is_granted('view_reporting') and is_granted('view_other_reporting') and is_granted('view_other_timesheet')")
|
||||
*/
|
||||
final class ReportUsersListController extends AbstractController
|
||||
{
|
||||
@@ -45,6 +49,141 @@ final class ReportUsersListController extends AbstractController
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/yearly_users_list", name="report_yearly_users", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function yearlyUsersList(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory();
|
||||
$localeFormats = $this->getLocaleFormats($request->getLocale());
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($currentUser);
|
||||
$allUsers = $this->userRepository->getUsersForQuery($query);
|
||||
$defaultDate = $dateTimeFactory->createDateTime('01 january this year 00:00:00');
|
||||
|
||||
if (null !== ($financialYear = $systemConfiguration->getFinancialYearStart())) {
|
||||
$defaultDate = $this->getDateTimeFactory()->createStartOfFinancialYear($financialYear);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
|
||||
$values = new YearlyUserList();
|
||||
$values->setDate(clone $defaultDate);
|
||||
|
||||
$form = $this->createForm(YearlyUserListForm::class, $values, [
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
'format' => $localeFormats->getDateTypeFormat(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
$values->setDate(clone $defaultDate);
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate(clone $defaultDate);
|
||||
}
|
||||
|
||||
$start = $values->getDate();
|
||||
$end = $this->getDateTimeFactory()->createEndOfFinancialYear($start);
|
||||
|
||||
$months = [];
|
||||
$totals = [];
|
||||
foreach ($allUsers as $user) {
|
||||
$rows[] = [
|
||||
'years' => $this->timesheetRepository->getMonthlyStats($start, $end, $user),
|
||||
'user' => $user
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($rows[0])) {
|
||||
/** @var Year $year */
|
||||
foreach ($rows[0]['years'] as $year) {
|
||||
foreach ($year->getMonths() as $month) {
|
||||
$date = new \DateTime();
|
||||
$date->setDate((int) $year->getYear(), $month->getMonthNumber(), 1);
|
||||
$date->setTime(0, 0, 0);
|
||||
$months[$date->format('Ym')] = $date;
|
||||
}
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
foreach ($row['years'] as $year) {
|
||||
foreach ($year->getMonths() as $month) {
|
||||
$date = new \DateTime();
|
||||
$date->setDate((int) $year->getYear(), $month->getMonthNumber(), 1);
|
||||
$totalsId = $date->format('Ym');
|
||||
if (!isset($totals[$totalsId])) {
|
||||
$totals[$totalsId] = 0;
|
||||
}
|
||||
$totals[$totalsId] += $month->getTotalDuration();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
foreach ($allUsers as $user) {
|
||||
$rows[] = [
|
||||
'days' => $this->timesheetRepository->getDailyStats($user, $start, $end),
|
||||
'user' => $user
|
||||
];
|
||||
}
|
||||
|
||||
$userYears = [];
|
||||
|
||||
if (isset($rows[0])) {
|
||||
foreach ($rows[0]['days'] as $day) {
|
||||
$months[$day->getDay()->format('Ym')] = $day->getDay();
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$userYear = ['user' => $row['user']];
|
||||
foreach ($row['days'] as $day) {
|
||||
$yearId = $day->getDay()->format('Y');
|
||||
$monthId = $day->getDay()->format('m');
|
||||
$totalsId = $yearId.$monthId;
|
||||
|
||||
if (!array_key_exists('years', $userYear)) {
|
||||
$userYear['years'] = [];
|
||||
}
|
||||
if (!array_key_exists($yearId, $userYear['years'])) {
|
||||
$userYear['years'][$yearId] = ['year' => $yearId];
|
||||
}
|
||||
if (!array_key_exists('months', $userYear['years'][$yearId])) {
|
||||
$userYear['years'][$yearId]['months'] = [];
|
||||
}
|
||||
if (!array_key_exists($monthId, $userYear['years'][$yearId]['months'])) {
|
||||
$userYear['years'][$yearId]['months'][$monthId] = ['month' => $monthId, 'totalDuration' => 0];
|
||||
}
|
||||
if (!array_key_exists($totalsId, $totals)) {
|
||||
$totals[$totalsId] = 0;
|
||||
}
|
||||
|
||||
$totals[$totalsId] += $day->getTotalDuration();
|
||||
$userYear['years'][$yearId]['months'][$monthId]['totalDuration'] += $day->getTotalDuration();;
|
||||
}
|
||||
$userYears[] = $userYear;
|
||||
}
|
||||
}
|
||||
$rows = $userYears;
|
||||
*/
|
||||
|
||||
return $this->render('reporting/report_user_list_monthly.html.twig', [
|
||||
'report_title' => 'report_yearly_users',
|
||||
'box_id' => 'yearly-user-list-reporting-box',
|
||||
'form' => $form->createView(),
|
||||
'rows' => $rows,
|
||||
'months' => $months,
|
||||
'totals' => $totals,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
|
||||
*
|
||||
@@ -89,11 +228,11 @@ final class ReportUsersListController extends AbstractController
|
||||
$end = clone $start;
|
||||
$end->modify('last day of 23:59:59');
|
||||
|
||||
$previousMonth = clone $start;
|
||||
$previousMonth->modify('-1 month');
|
||||
$previous = clone $start;
|
||||
$previous->modify('-1 month');
|
||||
|
||||
$nextMonth = clone $start;
|
||||
$nextMonth->modify('+1 month');
|
||||
$next = clone $start;
|
||||
$next->modify('+1 month');
|
||||
|
||||
foreach ($allUsers as $user) {
|
||||
$rows[] = [
|
||||
@@ -103,12 +242,22 @@ final class ReportUsersListController extends AbstractController
|
||||
}
|
||||
|
||||
$days = [];
|
||||
$totals = [];
|
||||
|
||||
if (isset($rows[0])) {
|
||||
/** @var Day $day */
|
||||
foreach ($rows[0]['days'] as $day) {
|
||||
$days[$day->getDay()->format('Ymd')] = $day->getDay();
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
foreach ($row['days'] as $day) {
|
||||
$totalsId = $day->getDay()->format('Ymd');
|
||||
if (!isset($totals[$totalsId])) {
|
||||
$totals[$totalsId] = 0;
|
||||
}
|
||||
$totals[$totalsId] += $day->getTotalDuration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('reporting/report_user_list.html.twig', [
|
||||
@@ -117,9 +266,10 @@ final class ReportUsersListController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'rows' => $rows,
|
||||
'days' => $days,
|
||||
'totals' => $totals,
|
||||
'current' => $start,
|
||||
'next' => $nextMonth,
|
||||
'previous' => $previousMonth,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -164,11 +314,11 @@ final class ReportUsersListController extends AbstractController
|
||||
$start = $dateTimeFactory->getStartOfWeek($values->getDate());
|
||||
$end = $dateTimeFactory->getEndOfWeek($values->getDate());
|
||||
|
||||
$previousWeek = clone $start;
|
||||
$previousWeek->modify('-1 week');
|
||||
$previous = clone $start;
|
||||
$previous->modify('-1 week');
|
||||
|
||||
$nextWeek = clone $start;
|
||||
$nextWeek->modify('+1 week');
|
||||
$next = clone $start;
|
||||
$next->modify('+1 week');
|
||||
|
||||
foreach ($allUsers as $user) {
|
||||
$rows[] = [
|
||||
@@ -178,12 +328,22 @@ final class ReportUsersListController extends AbstractController
|
||||
}
|
||||
|
||||
$days = [];
|
||||
$totals = [];
|
||||
|
||||
if (isset($rows[0])) {
|
||||
/** @var Day $day */
|
||||
foreach ($rows[0]['days'] as $day) {
|
||||
$days[$day->getDay()->format('Ymd')] = $day->getDay();
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
foreach ($row['days'] as $day) {
|
||||
$totalsId = $day->getDay()->format('Ymd');
|
||||
if (!isset($totals[$totalsId])) {
|
||||
$totals[$totalsId] = 0;
|
||||
}
|
||||
$totals[$totalsId] += $day->getTotalDuration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('reporting/report_user_list.html.twig', [
|
||||
@@ -192,9 +352,10 @@ final class ReportUsersListController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'rows' => $rows,
|
||||
'days' => $days,
|
||||
'totals' => $totals,
|
||||
'current' => $start,
|
||||
'next' => $nextWeek,
|
||||
'previous' => $previousWeek,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Form\Model\Configuration;
|
||||
use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
|
||||
use App\Form\SystemConfigurationForm;
|
||||
use App\Form\Type\ArrayToCommaStringType;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use App\Form\Type\DateTimeTextType;
|
||||
use App\Form\Type\DayTimeType;
|
||||
use App\Form\Type\LanguageType;
|
||||
@@ -507,6 +508,12 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(TextType::class),
|
||||
(new Configuration())
|
||||
->setName('company.financial_year')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setRequired(false)
|
||||
->setType(DatePickerType::class)
|
||||
->setOptions(['input' => 'string']),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
@@ -30,19 +31,20 @@ use Faker\Factory;
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
class TimesheetFixtures extends Fixture implements DependentFixtureInterface, FixtureGroupInterface
|
||||
{
|
||||
public const MIN_TIMESHEETS_PER_USER = 50;
|
||||
public const MAX_TIMESHEETS_PER_USER = 500;
|
||||
public const MAX_TIMESHEETS_TOTAL = 5000;
|
||||
public const MAX_RUNNING_TIMESHEETS_PER_USER = 1;
|
||||
public const TIMERANGE_DAYS = 1095; // 3 years
|
||||
public const TIMERANGE_BEGIN_DAYS = 1; // yesterday
|
||||
public const TIMERANGE_END_DAYS = 1095; // 3 years
|
||||
public const TIMERANGE_RUNNING = 1047; // in minutes = 17:45 hours
|
||||
public const MIN_MINUTES_PER_ENTRY = 15;
|
||||
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
|
||||
public const MAX_DESCRIPTION_LENGTH = 200;
|
||||
|
||||
public const ADD_TAGS_MAX_ENTRIES = 10000;
|
||||
public const ADD_TAGS_MAX_ENTRIES = 1000;
|
||||
public const MAX_TAG_PER_ENTRY = 3;
|
||||
|
||||
public const BATCH_SIZE = 100;
|
||||
@@ -59,6 +61,11 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['timesheet'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -103,7 +110,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
|
||||
$manager->persist($entry);
|
||||
|
||||
if ($i % self::BATCH_SIZE === 0) {
|
||||
if ($all % self::BATCH_SIZE === 0) {
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
@@ -129,21 +136,18 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
}
|
||||
$manager->flush();
|
||||
|
||||
// TODO this breaks if too many records need to be loaded: find a better way of adding tags
|
||||
if ($all < self::ADD_TAGS_MAX_ENTRIES) {
|
||||
$entries = $manager->getRepository(Timesheet::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$tagAmount = rand(0, self::MAX_TAG_PER_ENTRY);
|
||||
for ($iTag = 0; $iTag < $tagAmount; $iTag++) {
|
||||
$tagId = rand(1, TagFixtures::MAX_TAGS);
|
||||
if (isset($allTags[$tagId])) {
|
||||
$temp->addTag($allTags[$tagId]);
|
||||
}
|
||||
$entries = $manager->getRepository(Timesheet::class)->findBy([], [], min($all, self::ADD_TAGS_MAX_ENTRIES));
|
||||
foreach ($entries as $temp) {
|
||||
$tagAmount = rand(0, self::MAX_TAG_PER_ENTRY);
|
||||
for ($iTag = 0; $iTag < $tagAmount; $iTag++) {
|
||||
$tagId = rand(1, TagFixtures::MAX_TAGS);
|
||||
if (isset($allTags[$tagId])) {
|
||||
$temp->addTag($allTags[$tagId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
|
||||
$manager->clear(Timesheet::class);
|
||||
$manager->clear(Tag::class);
|
||||
}
|
||||
@@ -212,10 +216,10 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, $setEndDate = true)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate)
|
||||
{
|
||||
$start = new \DateTime();
|
||||
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
|
||||
$start = $start->modify('- ' . (rand(self::TIMERANGE_BEGIN_DAYS, self::TIMERANGE_END_DAYS)) . ' days');
|
||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||
$start->setTimezone(new \DateTimeZone($user->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get())));
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getLanguagesNode())
|
||||
->append($this->getCalendarNode())
|
||||
->append($this->getThemeNode())
|
||||
->append($this->getCompanyNode())
|
||||
->append($this->getIndustryNode())
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
@@ -483,6 +484,22 @@ class Configuration implements ConfigurationInterface
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getCompanyNode()
|
||||
{
|
||||
$builder = new TreeBuilder('company');
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('financial_year')->defaultNull()->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getUserNode()
|
||||
{
|
||||
$builder = new TreeBuilder('user');
|
||||
|
||||
@@ -129,7 +129,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$auth = $this->security;
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$users = new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], $this->getIcon('user'));
|
||||
$users = new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], $this->getIcon('users'));
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences']);
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
69
src/Form/Type/YearPickerType.php
Normal file
69
src/Form/Type/YearPickerType.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select a year via picker and select previous and next year.
|
||||
*
|
||||
* Always falls back to the current year if none or an invalid date is given.
|
||||
*/
|
||||
final class YearPickerType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'start_date' => new \DateTime(),
|
||||
'show_range' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
/** @var \DateTime|null $date */
|
||||
$date = $form->getData();
|
||||
|
||||
if (null === $date) {
|
||||
$date = $options['start_date'];
|
||||
}
|
||||
|
||||
$view->vars['year'] = $date;
|
||||
$view->vars['show_range'] = $options['show_range'];
|
||||
$view->vars['previousYear'] = (clone $date)->modify('-1 year');
|
||||
$view->vars['nextYear'] = (clone $date)->modify('+1 year');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'yearpicker';
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ final class Month
|
||||
sprintf('Invalid month given. Expected 1-12, received "%s".', $monthNumber)
|
||||
);
|
||||
}
|
||||
$this->month = $month;
|
||||
$this->month = str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function getMonth(): string
|
||||
|
||||
@@ -44,9 +44,10 @@ final class ReportingService
|
||||
if ($this->security->isGranted('view_reporting')) {
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user'));
|
||||
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user'));
|
||||
if ($this->security->isGranted('view_other_timesheet')) {
|
||||
$event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'user'));
|
||||
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'user'));
|
||||
if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) {
|
||||
$event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'users'));
|
||||
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'users'));
|
||||
$event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users'));
|
||||
}
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
|
||||
14
src/Reporting/YearlyUserList.php
Normal file
14
src/Reporting/YearlyUserList.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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;
|
||||
|
||||
final class YearlyUserList extends AbstractUserList
|
||||
{
|
||||
}
|
||||
58
src/Reporting/YearlyUserListForm.php
Normal file
58
src/Reporting/YearlyUserListForm.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
|
||||
use App\Form\Type\YearPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class YearlyUserListForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
'format' => $options['format'],
|
||||
'show_range' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearlyUserList::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class TimesheetRepository extends EntityRepository
|
||||
return \count($this->getActiveEntries($user));
|
||||
|
||||
case self::STATS_QUERY_MONTHLY:
|
||||
return $this->getMonthlyStats($user, $begin, $end);
|
||||
return $this->getMonthlyStats($begin, $end, $user);
|
||||
|
||||
case 'daily':
|
||||
return $this->getDailyStats($user, $begin, $end);
|
||||
@@ -337,7 +337,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$stats->setDurationTotal($durationTotal);
|
||||
$stats->setAmountThisMonth($amountMonth);
|
||||
$stats->setDurationThisMonth($durationMonth);
|
||||
$stats->setFirstEntry(new DateTime($firstEntry));
|
||||
$stats->setFirstEntry(new DateTime($firstEntry, new \DateTimeZone($user->getTimezone())));
|
||||
$stats->setRecordsTotal($recordsTotal);
|
||||
|
||||
return $stats;
|
||||
@@ -346,50 +346,57 @@ class TimesheetRepository extends EntityRepository
|
||||
/**
|
||||
* Returns an array of Year statistics.
|
||||
*
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User|null $user
|
||||
* @param DateTime|null $begin
|
||||
* @param DateTime|null $end
|
||||
* @return Year[]
|
||||
*/
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null): array
|
||||
public function getMonthlyStats(DateTime $begin, DateTime $end, ?User $user = null): array
|
||||
{
|
||||
/** @var Year[] $years */
|
||||
$years = [];
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, null);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
$tmp = clone $begin;
|
||||
while ($tmp < $end) {
|
||||
$curYear = $tmp->format('Y');
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $i < 10 ? '0' . $i : (string) $i;
|
||||
$year->setMonth(new Month($month));
|
||||
$date = clone $begin;
|
||||
$date->setDate((int) $curYear, $i, (int) $begin->format('d'));
|
||||
$date->setTime(0, 0, 0);
|
||||
if ($date < $begin || $date > $end) {
|
||||
continue;
|
||||
}
|
||||
$year->setMonth(new Month((string) $i));
|
||||
}
|
||||
$years[$curYear] = $year;
|
||||
}
|
||||
$tmp->modify('+1 month');
|
||||
}
|
||||
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, null);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
if (!isset($years[$statRow['year']])) {
|
||||
continue;
|
||||
}
|
||||
$month = $years[$statRow['year']]->getMonth((int) $statRow['month']);
|
||||
if (null === $month) {
|
||||
continue;
|
||||
}
|
||||
$month->setTotalDuration((int) $statRow['duration']);
|
||||
$month->setTotalRate((float) $statRow['rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, true);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $i < 10 ? '0' . $i : (string) $i;
|
||||
$year->setMonth(new Month($month));
|
||||
}
|
||||
$years[$curYear] = $year;
|
||||
if (!isset($years[$statRow['year']])) {
|
||||
continue;
|
||||
}
|
||||
$month = $years[$statRow['year']]->getMonth((int) $statRow['month']);
|
||||
if (null === $month) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$month->setBillableDuration((int) $statRow['duration']);
|
||||
$month->setBillableRate((float) $statRow['rate']);
|
||||
}
|
||||
@@ -597,7 +604,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$results = $this->getDailyData($begin, $end, $user);
|
||||
|
||||
foreach ($results as $statRow) {
|
||||
$dateTime = new DateTime();
|
||||
$dateTime = clone $begin;
|
||||
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
|
||||
$dateTime->setTime(0, 0, 0);
|
||||
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
|
||||
|
||||
@@ -174,16 +174,6 @@ class WidgetRepository
|
||||
'color' => 'purple',
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
'query' => TimesheetRepository::STATS_QUERY_DURATION,
|
||||
'user' => true,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userDurationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
'query' => TimesheetRepository::STATS_QUERY_DURATION,
|
||||
@@ -222,16 +212,6 @@ class WidgetRepository
|
||||
'color' => 'purple',
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
'query' => TimesheetRepository::STATS_QUERY_RATE,
|
||||
'user' => true,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'userAmountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
'query' => TimesheetRepository::STATS_QUERY_RATE,
|
||||
@@ -270,16 +250,6 @@ class WidgetRepository
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
'query' => TimesheetRepository::STATS_QUERY_DURATION,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'durationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
'query' => TimesheetRepository::STATS_QUERY_DURATION,
|
||||
@@ -318,16 +288,6 @@ class WidgetRepository
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
'query' => TimesheetRepository::STATS_QUERY_RATE,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'amountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
'query' => TimesheetRepository::STATS_QUERY_RATE,
|
||||
@@ -366,16 +326,6 @@ class WidgetRepository
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersYear' => [
|
||||
'title' => 'stats.userActiveYear',
|
||||
'query' => TimesheetRepository::STATS_QUERY_USER,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'user',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => Counter::class,
|
||||
],
|
||||
'activeUsersTotal' => [
|
||||
'title' => 'stats.userActiveTotal',
|
||||
'query' => TimesheetRepository::STATS_QUERY_USER,
|
||||
|
||||
@@ -124,4 +124,32 @@ class DateTimeFactory
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function createStartOfFinancialYear(?string $financialYear = null): DateTime
|
||||
{
|
||||
$defaultDate = $this->createDateTime('01 january this year 00:00:00');
|
||||
|
||||
if (null === $financialYear) {
|
||||
return $defaultDate;
|
||||
}
|
||||
|
||||
$financialYear = $this->createDateTime($financialYear);
|
||||
$financialYear->setDate((int) $defaultDate->format('Y'), (int) $financialYear->format('m'), (int) $financialYear->format('d'));
|
||||
|
||||
$now = $this->createDateTime('00:00:00');
|
||||
|
||||
if ($financialYear >= $now) {
|
||||
$financialYear->modify('-1 year');
|
||||
}
|
||||
|
||||
return $financialYear;
|
||||
}
|
||||
|
||||
public function createEndOfFinancialYear(DateTime $financialYear): DateTime
|
||||
{
|
||||
$yearEnd = clone $financialYear;
|
||||
$yearEnd->modify('+1 year')->modify('-1 day')->setTime(23, 59, 59);
|
||||
|
||||
return $yearEnd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,8 @@ final class IconExtension extends AbstractExtension
|
||||
'trash' => 'far fa-trash-alt',
|
||||
'unlocked' => 'fas fa-unlock-alt',
|
||||
'upload' => 'fas fa-upload',
|
||||
'user' => 'fas fa-user-friends',
|
||||
'user' => 'fas fa-user',
|
||||
'users' => 'fas fa-user-friends',
|
||||
'visibility' => 'far fa-eye',
|
||||
'warning' => 'fas fa-exclamation-triangle',
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
|
||||
@@ -16,7 +16,7 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id = '';
|
||||
protected $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@@ -39,7 +39,11 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
if (!empty($this->id)) {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
return (new \ReflectionClass($this))->getShortName();
|
||||
}
|
||||
|
||||
public function setData($data): self
|
||||
|
||||
34
src/Widget/Type/ActiveUsersYear.php
Normal file
34
src/Widget/Type/ActiveUsersYear.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class ActiveUsersYear extends CounterYear
|
||||
{
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository, $systemConfiguration);
|
||||
$this->setId('activeUsersYear');
|
||||
$this->setOption('icon', 'user');
|
||||
$this->setOption('color', 'yellow');
|
||||
$this->setTitle('stats.userActiveYear');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->titleYear = 'stats.userActiveFinancialYear';
|
||||
$this->setQuery(TimesheetRepository::STATS_QUERY_USER);
|
||||
$this->setQueryWithUser(false);
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
}
|
||||
35
src/Widget/Type/AmountYear.php
Normal file
35
src/Widget/Type/AmountYear.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class AmountYear extends CounterYear
|
||||
{
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository, $systemConfiguration);
|
||||
$this->setId('amountYear');
|
||||
$this->setOption('dataType', 'money');
|
||||
$this->setOption('icon', 'money');
|
||||
$this->setOption('color', 'yellow');
|
||||
$this->setTitle('stats.amountYear');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->titleYear = 'stats.amountFinancialYear';
|
||||
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
|
||||
$this->setQueryWithUser(false);
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
}
|
||||
52
src/Widget/Type/CounterYear.php
Normal file
52
src/Widget/Type/CounterYear.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
|
||||
class CounterYear extends SimpleStatisticChart
|
||||
{
|
||||
private $systemConfiguration;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $titleYear;
|
||||
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository);
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
$this->setOption('dataType', 'int');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->begin = '01 january this year 00:00:00';
|
||||
$this->end = '31 december this year 23:59:59';
|
||||
|
||||
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
|
||||
$factory = new DateTimeFactory($this->getTimezone());
|
||||
$this->begin = $factory->createStartOfFinancialYear($financialYear);
|
||||
$this->end = $factory->createEndOfFinancialYear($this->begin);
|
||||
if (!empty($this->titleYear)) {
|
||||
$this->setTitle($this->titleYear);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'widget/widget-counter.html.twig';
|
||||
}
|
||||
}
|
||||
35
src/Widget/Type/DurationYear.php
Normal file
35
src/Widget/Type/DurationYear.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class DurationYear extends CounterYear
|
||||
{
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository, $systemConfiguration);
|
||||
$this->setId('durationYear');
|
||||
$this->setOption('dataType', 'duration');
|
||||
$this->setOption('icon', 'duration');
|
||||
$this->setOption('color', 'yellow');
|
||||
$this->setTitle('stats.durationYear');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->titleYear = 'stats.durationFinancialYear';
|
||||
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
|
||||
$this->setQueryWithUser(false);
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
@@ -16,22 +17,15 @@ use DateTime;
|
||||
|
||||
final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
private $systemConfiguration;
|
||||
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
$this->setId('PaginatedWorkingTimeChart');
|
||||
$this->setTitle('stats.yourWorkingHours');
|
||||
|
||||
$this->setOptions([
|
||||
'year' => (new DateTime('now'))->format('o'),
|
||||
'week' => (new DateTime('now'))->format('W'),
|
||||
'type' => 'bar',
|
||||
]);
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
@@ -48,10 +42,15 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
{
|
||||
$options = parent::getOptions($options);
|
||||
|
||||
if (!\in_array($options['type'], ['bar', 'line'])) {
|
||||
if (!\array_key_exists('type', $options) || !\in_array($options['type'], ['bar', 'line'])) {
|
||||
$options['type'] = 'bar';
|
||||
}
|
||||
|
||||
if (!\array_key_exists('year', $options)) {
|
||||
$options['year'] = (new DateTime('now'))->format('o');
|
||||
$options['week'] = (new DateTime('now'))->format('W');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
@@ -91,6 +90,25 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
$thisMonth = ($dateTimeFactory->createDateTime())->setISODate($year, $week, 1)->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
$dayBegin = $dateTimeFactory->createDateTime('00:00:00');
|
||||
$dayEnd = $dateTimeFactory->createDateTime('23:59:59');
|
||||
|
||||
$monthBegin = (clone $weekBegin)->setDate((int) $weekBegin->format('Y'), (int) $weekBegin->format('n'), 1)->setTime(0, 0, 0);
|
||||
$monthEnd = (clone $weekBegin)->setDate((int) $weekBegin->format('Y'), (int) $weekBegin->format('n'), (int) $weekBegin->format('t'))->setTime(23, 59, 59);
|
||||
|
||||
$yearBegin = $dateTimeFactory->createDateTime(sprintf('01 january %s 00:00:00', $year));
|
||||
$yearEnd = $dateTimeFactory->createDateTime(sprintf('31 december %s 23:59:59', $year));
|
||||
$yearData = $this->repository->getStatistic('duration', $yearBegin, $yearEnd, $user);
|
||||
|
||||
$financialYearData = null;
|
||||
$financialYearBegin = null;
|
||||
|
||||
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
|
||||
$financialYearBegin = $dateTimeFactory->createStartOfFinancialYear($financialYear);
|
||||
$financialYearEnd = $dateTimeFactory->createEndOfFinancialYear($financialYearBegin);
|
||||
$financialYearData = $this->repository->getStatistic('duration', $financialYearBegin, $financialYearEnd, $user);
|
||||
}
|
||||
|
||||
return [
|
||||
'begin' => clone $weekBegin,
|
||||
'end' => clone $weekEnd,
|
||||
@@ -98,30 +116,12 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
'thisMonth' => $thisMonth,
|
||||
'lastWeekInYear' => $lastWeekInYear,
|
||||
'lastWeekInLastYear' => $lastWeekInLastYear,
|
||||
'day' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
$dateTimeFactory->createDateTime('00:00:00'),
|
||||
$dateTimeFactory->createDateTime('23:59:59'),
|
||||
$user
|
||||
),
|
||||
'week' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
$weekBegin,
|
||||
$weekEnd,
|
||||
$user
|
||||
),
|
||||
'month' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
(clone $weekBegin)->setDate((int) $weekBegin->format('Y'), (int) $weekBegin->format('n'), 1)->setTime(0, 0, 0),
|
||||
(clone $weekBegin)->setDate((int) $weekBegin->format('Y'), (int) $weekBegin->format('n'), (int) $weekBegin->format('t'))->setTime(23, 59, 59),
|
||||
$user
|
||||
),
|
||||
'year' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
$dateTimeFactory->createDateTime(sprintf('01 january %s 00:00:00', $year)),
|
||||
$dateTimeFactory->createDateTime(sprintf('31 december %s 23:59:59', $year)),
|
||||
$user
|
||||
),
|
||||
'day' => $this->repository->getStatistic('duration', $dayBegin, $dayEnd, $user),
|
||||
'week' => $this->repository->getStatistic('duration', $weekBegin, $weekEnd, $user),
|
||||
'month' => $this->repository->getStatistic('duration', $monthBegin, $monthEnd, $user),
|
||||
'year' => $yearData,
|
||||
'financial' => $financialYearData,
|
||||
'financialBegin' => $financialYearBegin,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ class SimpleStatisticChart extends SimpleWidget implements UserWidget
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|\DateTime
|
||||
*/
|
||||
private $begin;
|
||||
protected $begin;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|\DateTime
|
||||
*/
|
||||
private $end;
|
||||
protected $end;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
@@ -83,6 +83,16 @@ class SimpleStatisticChart extends SimpleWidget implements UserWidget
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimezone(): \DateTimeZone
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->user) {
|
||||
$timezone = $this->user->getTimezone();
|
||||
}
|
||||
|
||||
return new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return mixed|null
|
||||
@@ -90,14 +100,18 @@ class SimpleStatisticChart extends SimpleWidget implements UserWidget
|
||||
*/
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->user) {
|
||||
$timezone = $this->user->getTimezone();
|
||||
}
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
$timezone = $this->getTimezone();
|
||||
|
||||
$begin = !empty($this->begin) ? new \DateTime($this->begin, $timezone) : null;
|
||||
$end = !empty($this->end) ? new \DateTime($this->end, $timezone) : null;
|
||||
$begin = $this->begin;
|
||||
$end = $this->end;
|
||||
|
||||
if (!empty($begin) && \is_string($begin)) {
|
||||
$begin = new \DateTime($begin, $timezone);
|
||||
}
|
||||
|
||||
if (!empty($end) && \is_string($end)) {
|
||||
$end = new \DateTime($end, $timezone);
|
||||
}
|
||||
|
||||
try {
|
||||
if (true === $this->queryWithUser) {
|
||||
|
||||
35
src/Widget/Type/UserAmountYear.php
Normal file
35
src/Widget/Type/UserAmountYear.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class UserAmountYear extends CounterYear
|
||||
{
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository, $systemConfiguration);
|
||||
$this->setId('userAmountYear');
|
||||
$this->setOption('dataType', 'money');
|
||||
$this->setOption('icon', 'money');
|
||||
$this->setOption('color', 'yellow');
|
||||
$this->setTitle('stats.amountYear');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->titleYear = 'stats.amountFinancialYear';
|
||||
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
|
||||
$this->setQueryWithUser(true);
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
}
|
||||
35
src/Widget/Type/UserDurationYear.php
Normal file
35
src/Widget/Type/UserDurationYear.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class UserDurationYear extends CounterYear
|
||||
{
|
||||
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
parent::__construct($repository, $systemConfiguration);
|
||||
$this->setId('userDurationYear');
|
||||
$this->setOption('dataType', 'duration');
|
||||
$this->setOption('icon', 'duration');
|
||||
$this->setOption('color', 'yellow');
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$this->setTitle('stats.durationYear');
|
||||
$this->titleYear = 'stats.durationFinancialYear';
|
||||
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
|
||||
$this->setQueryWithUser(true);
|
||||
|
||||
return parent::getData($options);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user