financial year setting + new users working time per year report (#2547)
This commit is contained in:
@@ -8,7 +8,7 @@ you can upgrade your Kimai installation to the latest stable release.
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [1.14](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
|
||||
## [1.14](https://github.com/kevinpapst/kimai2/releases/tag/1.14)
|
||||
|
||||
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ kimai:
|
||||
EXPORT: ['create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
||||
REPORTING: ['view_reporting']
|
||||
REPORTING: ['view_reporting','view_other_reporting']
|
||||
# some single default definitions for roles
|
||||
SINGLE_USER: ['view_team_member','budget_team_project']
|
||||
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@
|
||||
<li>
|
||||
<a href="{{ path('user_profile', {'username' : app.user.username}) }}">
|
||||
<h4 class="control-sidebar-subheading">
|
||||
<i class="{{ 'avatar'|icon }}"></i>
|
||||
<i class="{{ 'user'|icon }}"></i>
|
||||
{{ 'my.profile'|trans }}
|
||||
</h4>
|
||||
</a>
|
||||
|
||||
@@ -71,6 +71,27 @@
|
||||
{% endif %}
|
||||
{%- endblock text_widget %}
|
||||
|
||||
{% block yearpicker_widget -%}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousYear|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}">
|
||||
<i class="{{ 'left'|icon }}"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" href="#" onclick="return false;">
|
||||
<span id="{{ form.vars.id }}_month_name">
|
||||
{% if show_range %}
|
||||
{{ year|date_short }} – {{ nextYear|date_short }}
|
||||
{% else %}
|
||||
{{ year|date_format('Y') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextYear|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextYear|date_short }}">
|
||||
<i class="{{ 'right'|icon }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{ block('hidden_widget') }}
|
||||
{%- endblock yearpicker_widget %}
|
||||
|
||||
{% block monthpicker_widget -%}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}">
|
||||
|
||||
@@ -82,9 +82,7 @@
|
||||
<th class="text-nowrap text-center total">{{ total|duration }}</th>
|
||||
{% for day in days %}
|
||||
<th class="text-nowrap text-center day-total{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% if day.totalDuration > 0 %}
|
||||
{{ day.totalDuration|duration }}
|
||||
{% endif %}
|
||||
{{ day.totalDuration|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% endblock %}
|
||||
{% block box_body_class %}{{ box_id }} table-responsive no-padding{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set absoluteTotals = 0 %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
@@ -37,15 +38,14 @@
|
||||
{% for day in userDay.days %}
|
||||
{% if day.totalDuration > 0 %}
|
||||
{% set usersTotalDuration = usersTotalDuration + day.totalDuration %}
|
||||
{% set absoluteTotals = absoluteTotals + day.totalDuration %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th class="text-nowrap text-center total">
|
||||
{% if usersTotalDuration > 0 %}
|
||||
{{ usersTotalDuration|duration }}
|
||||
{% endif %}
|
||||
{{ usersTotalDuration|duration }}
|
||||
</th>
|
||||
{% for day in userDay.days %}
|
||||
<td class="text-nowrap day-total{% if day.day is weekend %} weekend{% endif %}">
|
||||
<td class="text-nowrap text-center day-total{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% if day.totalDuration > 0 %}
|
||||
{{ day.totalDuration|duration }}
|
||||
{% endif %}
|
||||
@@ -53,6 +53,17 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th class="text-center text-nowrap">
|
||||
{{ absoluteTotals|duration }}
|
||||
</th>
|
||||
{% for id, duration in totals %}
|
||||
<th class="text-center text-nowrap">
|
||||
{{ duration|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
86
templates/reporting/report_user_list_monthly.html.twig
Normal file
86
templates/reporting/report_user_list_monthly.html.twig
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends 'reporting/layout.html.twig' %}
|
||||
|
||||
{% block report_title %}{{ report_title|trans({}, 'reporting') }}{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_before %}
|
||||
{{ form_start(form, {'attr': {'class': 'form-inline'}}) }}
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ form_widget(form.date) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}{{ box_id }} table-responsive no-padding{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for id, month in months %}
|
||||
<th class="text-center text-nowrap">
|
||||
<a href="{{ path('report_monthly_users', {'date': month|date_short}) }}">
|
||||
{{ month|month_name }}<br>
|
||||
{{ month|date_format('Y') }}
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for userYear in rows %}
|
||||
{% set usersTotalDuration = 0 %}
|
||||
<tr class="user">
|
||||
<td class="text-nowrap">
|
||||
<strong>{{ widgets.username(userYear.user) }}</strong>
|
||||
</td>
|
||||
{% for yid, year in userYear.years %}
|
||||
{% for mid, month in year.months %}
|
||||
{% if month.totalDuration > 0 %}
|
||||
{% set usersTotalDuration = usersTotalDuration + month.totalDuration %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<th class="text-nowrap text-center total">
|
||||
{{ usersTotalDuration|duration }}
|
||||
</th>
|
||||
{% for yid, year in userYear.years %}
|
||||
{% for mid, month in year.months %}
|
||||
<td class="text-nowrap text-center day-total">
|
||||
{% if month.totalDuration > 0 %}
|
||||
<a href="{{ path('report_user_month', {'date': (year.year ~'-'~month.month~'-01')|date_short, 'user': userYear.user.id}) }}" data-toggle="tooltip" title="{{ 'label.billable'|trans }}: {{ month.billableDuration|duration }}">
|
||||
{{ month.totalDuration|duration }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
{% for id, duration in totals %}
|
||||
<th class="text-center text-nowrap">
|
||||
{{ duration|duration }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
$('#{{ form.date.vars.id }}').on('change', function(ev) {
|
||||
$(this).closest('form').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -28,7 +28,6 @@
|
||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||
{% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.users('index') }}{% endblock %}
|
||||
{% block page_icon %}{{ 'user'|icon }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns, 'kimai.userUpdate') }}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.user(user, tab) }}{% endblock %}
|
||||
{% block page_icon %}{{ 'user'|icon }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{% for year,yearStat in years %}
|
||||
{% for year, yearStat in years %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_title %}{{ year }}{% endblock %}
|
||||
@@ -100,21 +100,33 @@
|
||||
backgroundColor: '{{ kimai_context.chart.background_color }}',
|
||||
borderColor: '{{ kimai_context.chart.border_color }}',
|
||||
data: [
|
||||
{% for month in yearStat.months %}
|
||||
{{ (month.totalDuration / 3600)|number_format(2, '.', '') }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% for monthNumber in 1..12 %}
|
||||
{% set duration = 0 %}
|
||||
{% set month = yearStat.month(monthNumber) %}
|
||||
{% if month is not null %}
|
||||
{% set duration = month.totalDuration %}
|
||||
{% endif %}
|
||||
{{ (duration / 3600)|number_format(2, '.', '') }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
realData: [
|
||||
{% for month in yearStat.months %}
|
||||
{% set realDayData = {duration: month.totalDuration|duration, billable: month.billableDuration|duration} %}
|
||||
{% for monthNumber in 1..12 %}
|
||||
{% set duration = 0 %}
|
||||
{% set billable = 0 %}
|
||||
{% set month = yearStat.month(monthNumber) %}
|
||||
{% if month is not null %}
|
||||
{% set duration = month.totalDuration %}
|
||||
{% set billable = month.billableDuration %}
|
||||
{% endif %}
|
||||
{% set realDayData = {'duration': duration|duration, 'billable': billable|duration} %}
|
||||
{{ realDayData|json_encode|raw }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
monthData: [
|
||||
{% for month in yearStat.months %}
|
||||
'{{ yearStat.year }}-{{ month.month|length == 1 ? '0'~ month.month : month.month }}-01'
|
||||
{% for monthNumber in 1..12 %}
|
||||
'{{ yearStat.year }}-{{ monthNumber < 10 ? '0' ~ monthNumber : monthNumber }}-01'
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
|
||||
@@ -56,12 +56,21 @@
|
||||
<span class="description-text">{{ 'stats.workingTimeMonth'|trans({'%month%': data.thisMonth|month_name, '%year%': data.thisMonth|date_format('Y')}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if data.financial is not null %}
|
||||
<div class="col-sm-3 col-xs-6">
|
||||
<div class="description-block border-right">
|
||||
<h5 class="description-header">{{ data.financial|duration }}</h5>
|
||||
<span class="description-text">{{ 'stats.workingTimeFinancialYear'|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-sm-3 col-xs-6">
|
||||
<div class="description-block border-right">
|
||||
<h5 class="description-header">{{ data.year|duration }}</h5>
|
||||
<span class="description-text">{{ 'stats.workingTimeYear'|trans({'%year%': data.thisMonth|date_format('Y')}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
@@ -157,10 +157,12 @@ class SystemConfigurationTest extends TestCase
|
||||
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue(''),
|
||||
(new Configuration())->setName('saml.activate')->setValue(true),
|
||||
(new Configuration())->setName('theme.color_choices')->setValue(''),
|
||||
(new Configuration())->setName('company.financial_year')->setValue('2020-03-27'),
|
||||
]);
|
||||
$this->assertFalse($sut->find('timesheet.rules.allow_future_times'));
|
||||
$this->assertTrue($sut->isSamlActive());
|
||||
$this->assertNull($sut->getThemeColorChoices());
|
||||
$this->assertEquals('2020-03-27', $sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
public function testUnknownConfigs()
|
||||
@@ -209,6 +211,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('blue', $sut->getUserDefaultTheme());
|
||||
$this->assertEquals('IT', $sut->getUserDefaultLanguage());
|
||||
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
|
||||
$this->assertNull($sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
public function testFormDefaultWithLoader()
|
||||
|
||||
@@ -35,7 +35,7 @@ class PermissionControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 118);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 119);
|
||||
$this->assertPageActions($client, [
|
||||
//'back' => $this->createUrl('/admin/user/'),
|
||||
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
|
||||
@@ -34,9 +34,13 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
$this->assertHasProfileBox($client, 'John Doe');
|
||||
$this->assertHasAboutMeBox($client, UserFixtures::USERNAME_USER);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$year = (new \DateTime())->format('Y');
|
||||
$this->assertStringContainsString('<h3 class="box-title">' . $year . '</h3>', $content);
|
||||
$this->assertStringContainsString('var userProfileChart' . $year . ' = new Chart(', $content);
|
||||
}
|
||||
|
||||
public function testIndexAction()
|
||||
|
||||
@@ -28,6 +28,11 @@ class ReportUsersListControllerTest extends ControllerBaseTest
|
||||
$this->importFixture($fixture);
|
||||
}
|
||||
|
||||
public function testYearlyListIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/yearly_users_list');
|
||||
}
|
||||
|
||||
public function testWeeklyListIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/weekly_users_list');
|
||||
@@ -38,6 +43,11 @@ class ReportUsersListControllerTest extends ControllerBaseTest
|
||||
$this->assertUrlIsSecured('/reporting/monthly_users_list');
|
||||
}
|
||||
|
||||
public function testYearlyUsersListIsSecureForUserRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/yearly_users_list');
|
||||
}
|
||||
|
||||
public function testWeeklyUsersListIsSecureForUserRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/weekly_users_list');
|
||||
@@ -48,6 +58,16 @@ class ReportUsersListControllerTest extends ControllerBaseTest
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/monthly_users_list');
|
||||
}
|
||||
|
||||
public function testYearlyUsersReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importReportingFixture(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/reporting/yearly_users_list');
|
||||
self::assertStringContainsString('<div class="box-body yearly-user-list-reporting-box', $client->getResponse()->getContent());
|
||||
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
|
||||
self::assertEquals(0, $select->count());
|
||||
}
|
||||
|
||||
public function testWeeklyUsersReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
@@ -428,6 +428,9 @@ class ConfigurationTest extends TestCase
|
||||
'connection' => [
|
||||
'organization' => []
|
||||
],
|
||||
],
|
||||
'company' => [
|
||||
'financial_year' => null,
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Tests\Model\Statistic;
|
||||
|
||||
use App\Model\Statistic\Month;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -29,32 +28,61 @@ class MonthTest extends TestCase
|
||||
self::assertSame(0.0, $sut->getBillableRate());
|
||||
}
|
||||
|
||||
public function testAllowedMonths()
|
||||
public function getTestData()
|
||||
{
|
||||
for ($i = 1; $i < 10; $i++) {
|
||||
new Month('0' . $i);
|
||||
}
|
||||
for ($i = 10; $i < 13; $i++) {
|
||||
new Month((string) $i);
|
||||
}
|
||||
self::assertTrue(true);
|
||||
yield ['01', '01', 1];
|
||||
yield ['02', '02', 2];
|
||||
yield ['03', '03', 3];
|
||||
yield ['04', '04', 4];
|
||||
yield ['05', '05', 5];
|
||||
yield ['06', '06', 6];
|
||||
yield ['07', '07', 7];
|
||||
yield ['08', '08', 8];
|
||||
yield ['09', '09', 9];
|
||||
yield ['10', '10', 10];
|
||||
yield ['11', '11', 11];
|
||||
yield ['12', '12', 12];
|
||||
yield [1, '01', 1];
|
||||
yield [2, '02', 2];
|
||||
yield [3, '03', 3];
|
||||
yield [4, '04', 4];
|
||||
yield [5, '05', 5];
|
||||
yield [6, '06', 6];
|
||||
yield [7, '07', 7];
|
||||
yield [8, '08', 8];
|
||||
yield [9, '09', 9];
|
||||
yield [10, '10', 10];
|
||||
yield [11, '11', 11];
|
||||
yield [12, '12', 12];
|
||||
}
|
||||
|
||||
public function testInvalidMonths()
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testAllowedMonths($init, $month, $number)
|
||||
{
|
||||
foreach (['00', '13', '99', '0.9'] as $month) {
|
||||
$ex = null;
|
||||
try {
|
||||
new Month($month);
|
||||
} catch (Exception $e) {
|
||||
$ex = $e;
|
||||
}
|
||||
self::assertInstanceOf(InvalidArgumentException::class, $ex);
|
||||
self::assertEquals(
|
||||
'Invalid month given. Expected 1-12, received "' . ((int) $month) . '".',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
$sut = new Month($init);
|
||||
self::assertEquals($month, $sut->getMonth());
|
||||
self::assertEquals($number, $sut->getMonthNumber());
|
||||
}
|
||||
|
||||
public function getInvalidTestData()
|
||||
{
|
||||
yield ['00'];
|
||||
yield ['13'];
|
||||
yield ['99'];
|
||||
yield ['0.9'];
|
||||
yield [19];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidTestData
|
||||
*/
|
||||
public function testInvalidMonths($month)
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid month given. Expected 1-12, received "' . ((int) $month) . '".');
|
||||
new Month($month);
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
|
||||
@@ -47,6 +47,6 @@ class ReportingServiceTest extends TestCase
|
||||
$sut = $this->getSut(true);
|
||||
$reports = $sut->getAvailableReports(new User());
|
||||
self::assertIsArray($reports);
|
||||
self::assertCount(6, $reports);
|
||||
self::assertCount(7, $reports);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +169,44 @@ class DateTimeFactoryTest extends TestCase
|
||||
// poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects
|
||||
$this->assertTrue(2 >= $difference);
|
||||
}
|
||||
|
||||
public function testCreateStartOfFinancialYearWithoutConfig()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->createStartOfFinancialYear();
|
||||
$expected = $sut->createDateTime('01 january this year 00:00:00');
|
||||
self::assertInstanceOf(DateTime::class, $dateTime);
|
||||
self::assertEquals($expected, $dateTime);
|
||||
}
|
||||
|
||||
public function testCreateStartOfFinancialYearWithConfig()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
|
||||
$future = $sut->createDateTime('+10 days');
|
||||
$past = $sut->createDateTime('-10 days');
|
||||
|
||||
$financial = $sut->createStartOfFinancialYear($future->format('Y-m-d'));
|
||||
|
||||
$future->modify('-1 year');
|
||||
$future->setTime(0, 0, 0);
|
||||
|
||||
self::assertEquals($future, $financial);
|
||||
|
||||
$financial = $sut->createStartOfFinancialYear($past->format('Y-m-d'));
|
||||
|
||||
$past->setTime(0, 0, 0);
|
||||
self::assertEquals($past, $financial);
|
||||
}
|
||||
|
||||
public function testCreateEndOfFinancialYearWithConfig()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
|
||||
$expected = $sut->createDateTime('2021-07-22 23:59:59 ');
|
||||
$financial = $sut->createStartOfFinancialYear('2020-07-23 15:30:00');
|
||||
$end = $sut->createEndOfFinancialYear($financial);
|
||||
|
||||
self::assertEquals($expected, $end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ abstract class AbstractWidgetTypeTest extends TestCase
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut);
|
||||
self::assertEquals('', $sut->getId());
|
||||
self::assertEquals('', $sut->getTitle());
|
||||
self::assertEquals($this->getDefaultOptions(), $sut->getOptions());
|
||||
self::assertNull($sut->getData());
|
||||
self::assertEquals('bar', $sut->getOption('foo', 'bar'));
|
||||
@@ -51,7 +49,10 @@ abstract class AbstractWidgetTypeTest extends TestCase
|
||||
self::assertEquals(array_merge($this->getDefaultOptions(), ['föööö' => 'trääääää']), $sut->getOptions());
|
||||
|
||||
$sut->setOptions(['blub' => 'blab', 'dataType' => 'money']);
|
||||
self::assertEquals(['blub' => 'blab', 'dataType' => 'money', 'föööö' => 'trääääää'], $sut->getOptions());
|
||||
$options = $sut->getOptions();
|
||||
self::assertEquals('blab', $options['blub']);
|
||||
self::assertEquals('money', $options['dataType']);
|
||||
self::assertEquals('trääääää', $options['föööö']);
|
||||
|
||||
// id
|
||||
$sut->setId('cvbnmyx');
|
||||
|
||||
62
tests/Widget/Type/ActiveUsersYearTest.php
Normal file
62
tests/Widget/Type/ActiveUsersYearTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\ActiveUsersYear;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\ActiveUsersYear
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
*/
|
||||
class ActiveUsersYearTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/**
|
||||
* @return CounterYear
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new ActiveUsersYear($repository, $configuration);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'dataType' => 'int',
|
||||
'icon' => 'user',
|
||||
'color' => 'yellow',
|
||||
];
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleStatisticChart::class, $sut);
|
||||
$sut->setData(10);
|
||||
}
|
||||
|
||||
public function testSettings()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
self::assertEquals('activeUsersYear', $sut->getId());
|
||||
}
|
||||
}
|
||||
62
tests/Widget/Type/AmountYearTest.php
Normal file
62
tests/Widget/Type/AmountYearTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\AmountYear;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\AmountYear
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
*/
|
||||
class AmountYearTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/**
|
||||
* @return CounterYear
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new AmountYear($repository, $configuration);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'dataType' => 'money',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
];
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleStatisticChart::class, $sut);
|
||||
$sut->setData(10);
|
||||
}
|
||||
|
||||
public function testSettings()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
self::assertEquals('amountYear', $sut->getId());
|
||||
}
|
||||
}
|
||||
101
tests/Widget/Type/CounterYearTest.php
Normal file
101
tests/Widget/Type/CounterYearTest.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
* @covers \App\Widget\Type\SimpleStatisticChart
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class CounterYearTest extends AbstractSimpleStatisticsWidgetTypeTest
|
||||
{
|
||||
public function createSut(?string $financialYear = null): AbstractWidgetType
|
||||
{
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
if (null !== $financialYear) {
|
||||
$configuration->method('getFinancialYearStart')->willReturn($financialYear);
|
||||
}
|
||||
|
||||
$sut = new CounterYear($this->createMock(TimesheetRepository::class), $configuration);
|
||||
$sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
public function testQueryWithUser()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$repository->expects($this->once())->method('getStatistic')->willReturnCallback(function (string $type, ?DateTime $begin, ?DateTime $end, ?User $user) {
|
||||
self::assertEquals($type, 'active');
|
||||
self::assertNull($begin);
|
||||
self::assertNull($end);
|
||||
self::assertNull($user);
|
||||
});
|
||||
$sut = new Counter($repository);
|
||||
$sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
|
||||
$sut->setUser($user);
|
||||
$sut->getData([]);
|
||||
|
||||
$user = new User();
|
||||
$user->setAlias('bar');
|
||||
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$repository->expects($this->once())->method('getStatistic')->willReturnCallback(function (string $type, ?DateTime $begin, ?DateTime $end, ?User $user) {
|
||||
self::assertEquals($type, 'active');
|
||||
self::assertNull($begin);
|
||||
self::assertNull($end);
|
||||
self::assertNotNull($user);
|
||||
self::assertEquals('bar', $user->getAlias());
|
||||
});
|
||||
$sut = new Counter($repository);
|
||||
$sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
|
||||
$sut->setUser($user);
|
||||
$sut->setQueryWithUser(true);
|
||||
$sut->getData([]);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return ['dataType' => 'int'];
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleWidget::class, $sut);
|
||||
}
|
||||
|
||||
public function testTemplateName()
|
||||
{
|
||||
/** @var Counter $sut */
|
||||
$sut = $this->createSut();
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
}
|
||||
|
||||
public function testTemplateNameWithFinancialYear()
|
||||
{
|
||||
/** @var Counter $sut */
|
||||
$sut = $this->createSut('2020-01-01');
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
}
|
||||
}
|
||||
62
tests/Widget/Type/DurationYearTest.php
Normal file
62
tests/Widget/Type/DurationYearTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\DurationYear;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\DurationYear
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
*/
|
||||
class DurationYearTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/**
|
||||
* @return CounterYear
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new DurationYear($repository, $configuration);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'dataType' => 'duration',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
];
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleStatisticChart::class, $sut);
|
||||
$sut->setData(10);
|
||||
}
|
||||
|
||||
public function testSettings()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
self::assertEquals('durationYear', $sut->getId());
|
||||
}
|
||||
}
|
||||
199
tests/Widget/Type/PaginatedWorkingTimeChartTest.php
Normal file
199
tests/Widget/Type/PaginatedWorkingTimeChartTest.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\PaginatedWorkingTimeChart;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\PaginatedWorkingTimeChart
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
* @covers \App\Widget\Type\AbstractWidgetType
|
||||
* @covers \App\Repository\TimesheetRepository
|
||||
*/
|
||||
class PaginatedWorkingTimeChartTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return PaginatedWorkingTimeChart
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
$sut = new PaginatedWorkingTimeChart($repository, $configuration);
|
||||
$sut->setUser(new User());
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleWidget::class, $sut);
|
||||
}
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut);
|
||||
self::assertEquals('PaginatedWorkingTimeChart', $sut->getId());
|
||||
self::assertEquals('stats.yourWorkingHours', $sut->getTitle());
|
||||
//self::assertNull($sut->getOption('begin', 'xxx'));
|
||||
// self::assertNull($sut->getOption('end', 'xxx'));
|
||||
// self::assertEquals('', $sut->getOption('color', 'xxx'));
|
||||
self::assertInstanceOf(User::class, $sut->getOption('user', 'xxx'));
|
||||
// self::assertEquals('bar', $sut->getOption('type', 'xxx'));
|
||||
}
|
||||
|
||||
public function testFluentInterface()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setOptions([]));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setId(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setTitle(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setData(''));
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
// options
|
||||
$sut->setOption('föööö', 'trääääää');
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
|
||||
// check default values
|
||||
self::assertEquals('xxxxx', $sut->getOption('blub', 'xxxxx'));
|
||||
self::assertEquals('xxxxx', $sut->getOption('dataType', 'xxxxx'));
|
||||
|
||||
$sut->setOptions(['blub' => 'blab', 'dataType' => 'money']);
|
||||
// check option still exists
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
// check options are now existing
|
||||
self::assertEquals('blab', $sut->getOption('blub', 'xxxxx'));
|
||||
self::assertEquals('money', $sut->getOption('dataType', 'xxxxx'));
|
||||
|
||||
// id
|
||||
$sut->setId('cvbnmyx');
|
||||
self::assertEquals('cvbnmyx', $sut->getId());
|
||||
}
|
||||
|
||||
public function testGetOptions()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
$options = $sut->getOptions(['type' => 'xxx']);
|
||||
self::assertEquals('bar', $options['type']);
|
||||
}
|
||||
|
||||
public function testGetData()
|
||||
{
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn(42);
|
||||
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(4711);
|
||||
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$repository->expects($this->once())->method('getDailyStats')->willReturnCallback(function ($user, $begin, $end) use ($activity, $project) {
|
||||
return [
|
||||
[
|
||||
'year' => $begin->format('Y'),
|
||||
'month' => $begin->format('n'),
|
||||
'day' => $begin->format('j'),
|
||||
'rate' => 13.75,
|
||||
'duration' => 1234,
|
||||
'billable' => 1234,
|
||||
'details' => [
|
||||
[
|
||||
'activity' => $activity,
|
||||
'project' => $project,
|
||||
'billable' => 1234,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
$expectedKeys = [
|
||||
'begin', 'end', 'stats', 'thisMonth', 'lastWeekInYear', 'lastWeekInLastYear', 'day', 'week', 'month', 'year', 'financial', 'financialBegin'
|
||||
];
|
||||
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
$configuration->expects($this->once())->method('getFinancialYearStart')->willReturn(null);
|
||||
|
||||
$sut = new PaginatedWorkingTimeChart($repository, $configuration);
|
||||
|
||||
$sut->setUser(new User());
|
||||
$data = $sut->getData([]);
|
||||
|
||||
self::assertCount(\count($expectedKeys), $data);
|
||||
foreach ($expectedKeys as $key) {
|
||||
self::assertArrayHasKey($key, $data);
|
||||
}
|
||||
self::assertNull($data['financialBegin']);
|
||||
}
|
||||
|
||||
public function testGetDataWithFinancialYear()
|
||||
{
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn(42);
|
||||
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(4711);
|
||||
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$repository->expects($this->once())->method('getDailyStats')->willReturnCallback(function ($user, $begin, $end) use ($activity, $project) {
|
||||
return [
|
||||
[
|
||||
'year' => $begin->format('Y'),
|
||||
'month' => $begin->format('n'),
|
||||
'day' => $begin->format('j'),
|
||||
'rate' => 13.75,
|
||||
'duration' => 1234,
|
||||
'billable' => 1234,
|
||||
'details' => [
|
||||
[
|
||||
'activity' => $activity,
|
||||
'project' => $project,
|
||||
'billable' => 1234,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
$expectedKeys = [
|
||||
'begin', 'end', 'stats', 'thisMonth', 'lastWeekInYear', 'lastWeekInLastYear', 'day', 'week', 'month', 'year', 'financial', 'financialBegin'
|
||||
];
|
||||
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
$configuration->expects($this->once())->method('getFinancialYearStart')->willReturn('2020-01-01');
|
||||
|
||||
$sut = new PaginatedWorkingTimeChart($repository, $configuration);
|
||||
|
||||
$sut->setUser(new User());
|
||||
$data = $sut->getData([]);
|
||||
|
||||
self::assertCount(\count($expectedKeys), $data);
|
||||
foreach ($expectedKeys as $key) {
|
||||
self::assertArrayHasKey($key, $data);
|
||||
}
|
||||
self::assertInstanceOf(\DateTime::class, $data['financialBegin']);
|
||||
}
|
||||
}
|
||||
62
tests/Widget/Type/UserAmountYearTest.php
Normal file
62
tests/Widget/Type/UserAmountYearTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
use App\Widget\Type\UserAmountYear;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\UserAmountYear
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
*/
|
||||
class UserAmountYearTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/**
|
||||
* @return CounterYear
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new UserAmountYear($repository, $configuration);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'dataType' => 'money',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
];
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleStatisticChart::class, $sut);
|
||||
$sut->setData(10);
|
||||
}
|
||||
|
||||
public function testSettings()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
self::assertEquals('userAmountYear', $sut->getId());
|
||||
}
|
||||
}
|
||||
62
tests/Widget/Type/UserDurationYearTest.php
Normal file
62
tests/Widget/Type/UserDurationYearTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\CounterYear;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
use App\Widget\Type\UserDurationYear;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\UserDurationYear
|
||||
* @covers \App\Widget\Type\CounterYear
|
||||
*/
|
||||
class UserDurationYearTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
/**
|
||||
* @return CounterYear
|
||||
*/
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new UserDurationYear($repository, $configuration);
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [
|
||||
'dataType' => 'duration',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
];
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleStatisticChart::class, $sut);
|
||||
$sut->setData(10);
|
||||
}
|
||||
|
||||
public function testSettings()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
self::assertEquals('userDurationYear', $sut->getId());
|
||||
}
|
||||
}
|
||||
@@ -806,6 +806,10 @@
|
||||
<source>stats.workingTimeYear</source>
|
||||
<target>Gesamtes Jahr %year%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.workingTimeFinancialYear">
|
||||
<source>stats.workingTimeFinancialYear</source>
|
||||
<target>Geschäftsjahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationToday">
|
||||
<source>stats.durationToday</source>
|
||||
<target>Arbeitszeit heute</target>
|
||||
@@ -822,6 +826,10 @@
|
||||
<source>stats.durationYear</source>
|
||||
<target>Arbeitszeit dieses Jahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationFinancialYear">
|
||||
<source>stats.durationFinancialYear</source>
|
||||
<target>Arbeitszeit dieses Geschäftsjahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationTotal">
|
||||
<source>stats.durationTotal</source>
|
||||
<target>Arbeitszeit total</target>
|
||||
@@ -846,6 +854,10 @@
|
||||
<source>stats.amountYear</source>
|
||||
<target>Umsatz dieses Jahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.amountFinancialYear">
|
||||
<source>stats.amountFinancialYear</source>
|
||||
<target>Umsatz dieses Geschäftsjahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.amountTotal">
|
||||
<source>stats.amountTotal</source>
|
||||
<target>Umsatz total</target>
|
||||
@@ -866,6 +878,10 @@
|
||||
<source>stats.userActiveYear</source>
|
||||
<target>Aktive Benutzer dieses Jahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.userActiveFinancialYear">
|
||||
<source>stats.userActiveFinancialYear</source>
|
||||
<target>Aktive Benutzer dieses Geschäftsjahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.userActiveTotal">
|
||||
<source>stats.userActiveTotal</source>
|
||||
<target>Aktive Benutzer jemals</target>
|
||||
|
||||
@@ -820,6 +820,10 @@
|
||||
<source>stats.workingTimeYear</source>
|
||||
<target>Full year %year%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.workingTimeFinancialYear">
|
||||
<source>stats.workingTimeFinancialYear</source>
|
||||
<target>Financial year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationToday">
|
||||
<source>stats.durationToday</source>
|
||||
<target>Working hours today</target>
|
||||
@@ -836,6 +840,10 @@
|
||||
<source>stats.durationYear</source>
|
||||
<target>Working hours this year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationFinancialYear">
|
||||
<source>stats.durationFinancialYear</source>
|
||||
<target>Working hours this financial year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationTotal">
|
||||
<source>stats.durationTotal</source>
|
||||
<target>Working hours total</target>
|
||||
@@ -860,6 +868,10 @@
|
||||
<source>stats.amountYear</source>
|
||||
<target>Revenue this year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.amountFinancialYear">
|
||||
<source>stats.amountFinancialYear</source>
|
||||
<target>Revenue this financial year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.amountTotal">
|
||||
<source>stats.amountTotal</source>
|
||||
<target>Total revenue</target>
|
||||
@@ -880,6 +892,10 @@
|
||||
<source>stats.userActiveYear</source>
|
||||
<target>Active users this year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.userActiveFinancialYear">
|
||||
<source>stats.userActiveFinancialYear</source>
|
||||
<target>Active users this financial year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.userActiveTotal">
|
||||
<source>stats.userActiveTotal</source>
|
||||
<target>Active users ever</target>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<source>report_monthly_users</source>
|
||||
<target>Monatsansicht für alle Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_yearly_users">
|
||||
<source>report_yearly_users</source>
|
||||
<target>Jahresansicht für alle Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_project_view">
|
||||
<source>report_project_view</source>
|
||||
<target>Projektübersicht</target>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<source>report_monthly_users</source>
|
||||
<target>Monthly view for all users</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_yearly_users">
|
||||
<source>report_yearly_users</source>
|
||||
<target>Yearly view for all users</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_project_view">
|
||||
<source>report_project_view</source>
|
||||
<target>Project overview</target>
|
||||
|
||||
@@ -128,15 +128,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="branding">
|
||||
<source>branding</source>
|
||||
<target>Markendarstellung</target>
|
||||
<target>Mein Unternehmen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.logo">
|
||||
<source>label.theme.branding.logo</source>
|
||||
<target>Logo (Bild URL, ersetzt das Unternehmen im Anmeldebildschirm)</target>
|
||||
<target>Logo URL (ersetzt den Unternehmensnamen im Anmeldebildschirm)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.company">
|
||||
<source>label.theme.branding.company</source>
|
||||
<target>Unternehmen</target>
|
||||
<target>Unternehmensname</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.mini">
|
||||
<source>label.theme.branding.mini</source>
|
||||
@@ -146,6 +146,10 @@
|
||||
<source>label.theme.branding.title</source>
|
||||
<target>Browser Titel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company.financial_year">
|
||||
<source>label.company.financial_year</source>
|
||||
<target>Geschäftsjahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rounding">
|
||||
<source>rounding</source>
|
||||
<target>Zeitenrundung</target>
|
||||
|
||||
@@ -128,15 +128,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="branding">
|
||||
<source>branding</source>
|
||||
<target>Branding</target>
|
||||
<target>My company</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.logo">
|
||||
<source>label.theme.branding.logo</source>
|
||||
<target>Logo (Image URL, replaces the company in login screen)</target>
|
||||
<target>Logo URL (replaces the company name in login screen)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.company">
|
||||
<source>label.theme.branding.company</source>
|
||||
<target>Company</target>
|
||||
<target>Company name</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.branding.mini">
|
||||
<source>label.theme.branding.mini</source>
|
||||
@@ -146,6 +146,10 @@
|
||||
<source>label.theme.branding.title</source>
|
||||
<target>Browser Title</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company.financial_year">
|
||||
<source>label.company.financial_year</source>
|
||||
<target>Financial year</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="rounding">
|
||||
<source>rounding</source>
|
||||
<target>Time rounding</target>
|
||||
|
||||
Reference in New Issue
Block a user