financial year setting + new users working time per year report (#2547)

This commit is contained in:
Kevin Papst
2021-05-14 18:40:48 +02:00
committed by GitHub
parent a9da5f8476
commit f7aa3c1e13
59 changed files with 1690 additions and 217 deletions

View File

@@ -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

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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,
]);
}
}

View File

@@ -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']),
]),
];
}

View File

@@ -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())));

View File

@@ -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');

View File

@@ -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);
}

View 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';
}
}

View File

@@ -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

View File

@@ -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'));

View 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
{
}

View 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',
]);
}
}

View File

@@ -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']);

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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',

View File

@@ -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

View 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);
}
}

View 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);
}
}

View 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';
}
}

View 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);
}
}

View File

@@ -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,
];
}
}

View File

@@ -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) {

View 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);
}
}

View 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);
}
}