Release 2.19 (#4922)

This commit is contained in:
Kevin Papst
2024-07-22 17:51:03 +02:00
committed by GitHub
parent ce22520d7f
commit 8788311faf
32 changed files with 585 additions and 372 deletions

View File

@@ -482,7 +482,7 @@ final class SystemConfiguration
}
/**
* @internal will be made private soon after 2.18.0 - do ot access this method directly, but through getThemeColors()
* @internal will be made private soon after 2.18.0 - do not access this method directly, but through getThemeColors()
*/
public function getThemeColorChoices(): string
{

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.18.0';
public const VERSION = '2.19.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 21800;
public const VERSION_ID = 21900;
/**
* The software name
*/

View File

@@ -90,7 +90,6 @@ final class ContractController extends AbstractController
'boxes' => $controllerEvent->getController(),
'year' => $year,
'user' => $profile,
'form' => $form->createView(),
]);
}
}

View File

@@ -11,7 +11,9 @@ namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Form\QuickEntryForm;
use App\Form\WeekByUserForm;
use App\Model\QuickEntryWeek;
use App\Reporting\WeekByUser\WeekByUser;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository;
use App\Timesheet\FavoriteRecordService;
@@ -25,7 +27,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Controller used to enter times in weekly form.
*/
#[Route(path: '/quick_entry')]
#[IsGranted('quick-entry')]
final class QuickEntryController extends AbstractController
{
@@ -38,18 +39,30 @@ final class QuickEntryController extends AbstractController
{
}
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
public function quickEntry(Request $request, ?string $begin = null): Response
#[Route(path: '/quick_entry/', name: 'quick_entry', methods: ['GET', 'POST'])]
public function quickEntry(Request $request): Response
{
$factory = $this->getDateTimeFactory();
$user = $this->getUser();
$factory = $this->getDateTimeFactory($user);
$defaultDate = $factory->createDateTime();
if ($begin !== null) {
try {
$begin = $factory->createDateTime($begin);
} catch (\Exception $ex) {
$begin = null;
}
}
$values = new WeekByUser();
$values->setUser($user);
$values->setDate($defaultDate);
$weeklyForm = $this->createFormForGetRequest(WeekByUserForm::class, $values, [
'include_user' => $this->isGranted('view_other_timesheet'),
'timezone' => $factory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'attr' => ['name' => 'quick_entry_weekrange_form']
]);
$weeklyForm->submit($request->query->all(), false);
$user = $values->getUser() ?? $user;
$factory = $this->getDateTimeFactory($user);
$begin = $values->getDate();
if ($begin === null) {
$begin = $factory->createDateTime();
@@ -57,7 +70,6 @@ final class QuickEntryController extends AbstractController
$startWeek = $factory->getStartOfWeek($begin);
$endWeek = $factory->getEndOfWeek($begin);
$user = $this->getUser();
$tmpDay = clone $startWeek;
$week = [];
@@ -235,7 +247,7 @@ final class QuickEntryController extends AbstractController
if ($saved) {
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('quick_entry', ['begin' => $begin->format('Y-m-d')]);
return $this->redirectToRoute('quick_entry', ['date' => $begin->format('Y-m-d'), 'user' => $user->getId()]);
}
} catch (\Exception $ex) {
$this->flashUpdateException($ex);
@@ -244,6 +256,8 @@ final class QuickEntryController extends AbstractController
$page = new PageSetup('quick_entry.title');
$page->setHelp('weekly-times.html');
$page->setPaginationForm($weeklyForm);
$page->setActionName('weekly-times');
return $this->render('quick-entry/index.html.twig', [
'page_setup' => $page,

View File

@@ -61,9 +61,11 @@ final class ReportUsersYearController extends AbstractController
$dateTimeFactory = $this->getDateTimeFactory();
$defaultDate = $dateTimeFactory->createStartOfYear();
$isFinancialYear = false;
if (null !== ($financialYear = $systemConfiguration->getFinancialYearStart())) {
$defaultDate = $this->getDateTimeFactory()->createStartOfFinancialYear($financialYear);
$isFinancialYear = true;
}
$values = new YearlyUserList();
@@ -72,6 +74,7 @@ final class ReportUsersYearController extends AbstractController
$form = $this->createFormForGetRequest(YearlyUserListForm::class, $values, [
'timezone' => $dateTimeFactory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'show_range' => $isFinancialYear,
]);
$form->submit($request->query->all(), false);

View File

@@ -11,7 +11,6 @@ namespace App\Form;
use App\Configuration\SystemConfiguration;
use App\Form\Type\QuickEntryWeekType;
use App\Form\Type\WeekPickerType;
use App\Model\QuickEntryWeek;
use App\Validator\Constraints\QuickEntryModel;
use Symfony\Component\Form\AbstractType;
@@ -57,13 +56,6 @@ final class QuickEntryForm extends AbstractType
}
));
$builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'label' => false,
]);
$builder->add('rows', CollectionType::class, [
'label' => false,
'entry_type' => QuickEntryWeekType::class,

View File

@@ -0,0 +1,48 @@
<?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;
use App\Form\Type\UserType;
use App\Form\Type\WeekPickerType;
use App\Reporting\WeekByUser\WeekByUser;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class WeekByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, [
'width' => false,
'include_current_user_if_system_account' => true
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => WeekByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]);
}
}

View File

@@ -25,7 +25,7 @@ class QuickEntryModel
*/
private array $timesheets = [];
public function __construct(private ?User $user = null, private ?Project $project = null, private ?Activity $activity = null)
public function __construct(private User $user, private ?Project $project = null, private ?Activity $activity = null)
{
}
@@ -39,7 +39,7 @@ class QuickEntryModel
return $this->prototype;
}
public function getUser(): ?User
public function getUser(): User
{
return $this->user;
}

View File

@@ -27,7 +27,7 @@ class QuickEntryWeek
{
}
public function addRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
public function addRow(User $user, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
{
$model = $this->createRow($user, $project, $activity);
@@ -36,7 +36,7 @@ class QuickEntryWeek
return $model;
}
public function createRow(?User $user = null, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
public function createRow(User $user, ?Project $project = null, ?Activity $activity = null): QuickEntryModel
{
return new QuickEntryModel($user, $project, $activity);
}

View File

@@ -28,7 +28,7 @@ final class YearlyUserListForm extends AbstractType
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'show_range' => true,
'show_range' => $options['show_range'],
]);
$builder->add('team', TeamType::class, [
'multiple' => false,
@@ -51,6 +51,7 @@ final class YearlyUserListForm extends AbstractType
'start_date' => new \DateTime(),
'csrf_protection' => false,
'method' => 'GET',
'show_range' => false,
]);
}
}

View File

@@ -37,6 +37,7 @@ final class Extensions extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('report_date', [$this, 'buildReportDate']),
new TwigFunction('class_name', [$this, 'getClassName']),
new TwigFunction('iso_day_by_name', [$this, 'getIsoDayByName']),
new TwigFunction('random_color', [$this, 'randomColor']),
@@ -52,6 +53,38 @@ final class Extensions extends AbstractExtension
];
}
public function buildReportDate(string|int $year, string|int $month = 1, string|int $day = 1): \DateTimeImmutable
{
if (\is_string($month)) {
$month = (int) $month;
}
if ($month > 12 || $month < 1) {
throw new \InvalidArgumentException('Unknown month: ' . $month);
}
if ($month < 10) {
$month = '0' . $month;
}
if (\is_string($day)) {
$day = (int) $day;
}
if ($day > 31 || $day < 1) {
throw new \InvalidArgumentException('Unknown day: ' . $day);
}
if ($day < 10) {
$day = '0' . $day;
}
if (\is_string($year)) {
$year = (int) $year;
}
if ($year < 1980 || $year > 2100) {
throw new \InvalidArgumentException('Unknown year: ' . $year);
}
return \DateTimeImmutable::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day); // @phpstan-ignore-line
}
public function formatReportDate(\DateTimeInterface $dateTime): string
{
return $dateTime->format(self::REPORT_DATE);

View File

@@ -189,18 +189,36 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
return $this->getFormatter()->dayName($dateTime, $short);
}
public function getJavascriptConfiguration(User $user): array
public function getJavascriptConfiguration(?User $user = null): array
{
$language = User::DEFAULT_LANGUAGE;
$browserTitle = false;
$id = null;
$name = 'anonymous';
$admin = false;
$superAdmin = false;
$timezone = date_default_timezone_get();
if ($user !== null) {
$browserTitle = (bool) $user->getPreferenceValue('update_browser_title');
$language = $user->getLanguage();
$id = $user->getId();
$name = $user->getDisplayName();
$admin = $user->isAdmin();
$superAdmin = $user->isSuperAdmin();
$timezone = $user->getTimezone();
}
return [
'locale' => $this->locale,
'language' => $user->getLanguage(),
'language' => $language,
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
'formatDate' => $this->localeService->getDateFormat($this->locale),
'defaultColor' => Constants::DEFAULT_COLOR,
'twentyFourHours' => $this->localeService->is24Hour($this->locale),
'updateBrowserTitle' => (bool) $user->getPreferenceValue('update_browser_title'),
'timezone' => $user->getTimezone(),
'user' => ['id' => $user->getId(), 'name' => $user->getDisplayName(), 'admin' => $user->isAdmin(), 'superAdmin' => $user->isSuperAdmin()],
'updateBrowserTitle' => $browserTitle,
'timezone' => $timezone,
'user' => ['id' => $id, 'name' => $name, 'admin' => $admin, 'superAdmin' => $superAdmin],
];
}

View File

@@ -27,7 +27,14 @@ use Psr\EventDispatcher\EventDispatcherInterface;
*/
final class WorkingTimeService
{
public function __construct(private TimesheetRepository $timesheetRepository, private WorkingTimeRepository $workingTimeRepository, private EventDispatcherInterface $eventDispatcher)
/** @var array<string, WorkingTime|null> */
private array $latestApprovals = [];
public function __construct(
private readonly TimesheetRepository $timesheetRepository,
private readonly WorkingTimeRepository $workingTimeRepository,
private readonly EventDispatcherInterface $eventDispatcher
)
{
}
@@ -43,7 +50,36 @@ final class WorkingTimeService
public function getLatestApproval(User $user): ?WorkingTime
{
return $this->workingTimeRepository->getLatestApproval($user);
if ($user->getId() === null) {
return null;
}
$key = 'u_' . $user->getId();
if (!\array_key_exists($key, $this->latestApprovals)) {
$this->latestApprovals[$key] = $this->workingTimeRepository->getLatestApproval($user);
}
return $this->latestApprovals[$key];
}
public function isApproved(User $user, \DateTimeInterface $dateTime): bool
{
$latestApproval = $this->getLatestApproval($user);
if ($latestApproval === null) {
return false;
}
$latestApprovalDate = $latestApproval->getDate();
$begin = \DateTimeImmutable::createFromInterface($dateTime);
$begin = $begin->setTime(0, 0, 0);
if ($begin > $latestApprovalDate) {
return false;
}
return true;
}
public function getYear(User $user, \DateTimeInterface $yearDate, \DateTimeInterface $until): Year
@@ -124,6 +160,11 @@ final class WorkingTimeService
$this->workingTimeRepository->persistScheduledWorkingTimes();
$key = 'u_' . $user->getId();
if (\array_key_exists($key, $this->latestApprovals)) {
unset($this->latestApprovals[$key]);
}
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approvedBy));
}