Release 2.19 (#4922)
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -90,7 +90,6 @@ final class ContractController extends AbstractController
|
||||
'boxes' => $controllerEvent->getController(),
|
||||
'year' => $year,
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
48
src/Form/WeekByUserForm.php
Normal file
48
src/Form/WeekByUserForm.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user