improved duration and minute selector (#2264)
* do not close modal if form is dirty * deprecated TimesheetConfiguration * inject timezone in form types * cleanup usage of UserDateTimeFactory * allow to configure increment steps for minutes * use 15 minutes step for datetimepicker in project edit form * use rounding rules for increments in minute select for begin and end * allow duration in multi user and admin timesheet forms * make dropdown values configurable
This commit is contained in:
@@ -14,7 +14,7 @@ namespace App\API;
|
||||
use App\API\Model\I18nConfig;
|
||||
use App\API\Model\TimesheetConfig;
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
@@ -36,20 +36,10 @@ final class ConfigurationController extends BaseApiController
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var LanguageFormattings
|
||||
*/
|
||||
private $formats;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
private $timesheetConfiguration;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, LanguageFormattings $formats, TimesheetConfiguration $timesheetConfiguration)
|
||||
public function __construct(ViewHandlerInterface $viewHandler)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->formats = $formats;
|
||||
$this->timesheetConfiguration = $timesheetConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +56,7 @@ final class ConfigurationController extends BaseApiController
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function i18nAction(): Response
|
||||
public function i18nAction(LanguageFormattings $formats): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
@@ -74,13 +64,13 @@ final class ConfigurationController extends BaseApiController
|
||||
|
||||
$model = new I18nConfig();
|
||||
$model
|
||||
->setFormDateTime($this->formats->getDateTimeTypeFormat($locale))
|
||||
->setFormDate($this->formats->getDateTypeFormat($locale))
|
||||
->setDateTime($this->formats->getDateTimeFormat($locale))
|
||||
->setDate($this->formats->getDateFormat($locale))
|
||||
->setDuration($this->formats->getDurationFormat($locale))
|
||||
->setTime($this->formats->getTimeFormat($locale))
|
||||
->setIs24hours($this->formats->isTwentyFourHours($locale))
|
||||
->setFormDateTime($formats->getDateTimeTypeFormat($locale))
|
||||
->setFormDate($formats->getDateTypeFormat($locale))
|
||||
->setDateTime($formats->getDateTimeFormat($locale))
|
||||
->setDate($formats->getDateFormat($locale))
|
||||
->setDuration($formats->getDurationFormat($locale))
|
||||
->setTime($formats->getTimeFormat($locale))
|
||||
->setIs24hours($formats->isTwentyFourHours($locale))
|
||||
->setNow($this->getDateTimeFactory()->createDateTime())
|
||||
;
|
||||
|
||||
@@ -104,16 +94,16 @@ final class ConfigurationController extends BaseApiController
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function timesheetConfigAction(): Response
|
||||
public function timesheetConfigAction(SystemConfiguration $configuration): Response
|
||||
{
|
||||
$model = new TimesheetConfig();
|
||||
$model
|
||||
->setTrackingMode($this->timesheetConfiguration->getTrackingMode())
|
||||
->setDefaultBeginTime($this->timesheetConfiguration->getDefaultBeginTime())
|
||||
->setActiveEntriesHardLimit($this->timesheetConfiguration->getActiveEntriesHardLimit())
|
||||
->setActiveEntriesSoftLimit($this->timesheetConfiguration->getActiveEntriesSoftLimit())
|
||||
->setIsAllowFutureTimes($this->timesheetConfiguration->isAllowFutureTimes())
|
||||
->setIsAllowOverlapping($this->timesheetConfiguration->isAllowOverlappingRecords())
|
||||
->setTrackingMode($configuration->getTimesheetTrackingMode())
|
||||
->setDefaultBeginTime($configuration->getTimesheetDefaultBeginTime())
|
||||
->setActiveEntriesHardLimit($configuration->getTimesheetActiveEntriesHardLimit())
|
||||
->setActiveEntriesSoftLimit($configuration->getTimesheetActiveEntriesSoftLimit())
|
||||
->setIsAllowFutureTimes($configuration->isTimesheetAllowFutureTimes())
|
||||
->setIsAllowOverlapping($configuration->isTimesheetAllowOverlappingRecords())
|
||||
;
|
||||
|
||||
$view = new View($model, 200);
|
||||
|
||||
@@ -225,6 +225,7 @@ class ProjectController extends BaseApiController
|
||||
$project = $this->projectService->createNewProject();
|
||||
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project, [
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
'include_budget' => $this->isGranted('budget', $project),
|
||||
]);
|
||||
@@ -290,6 +291,7 @@ class ProjectController extends BaseApiController
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project, [
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
'include_budget' => $this->isGranted('budget', $project),
|
||||
]);
|
||||
|
||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\RecentActivityEvent;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
@@ -62,10 +61,6 @@ class TimesheetController extends BaseApiController
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
@@ -86,24 +81,20 @@ class TimesheetController extends BaseApiController
|
||||
public function __construct(
|
||||
ViewHandlerInterface $viewHandler,
|
||||
TimesheetRepository $repository,
|
||||
TimesheetConfiguration $configuration,
|
||||
TagRepository $tagRepository,
|
||||
TrackingModeService $trackingModeService,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
TimesheetService $service
|
||||
) {
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->trackingModeService = $trackingModeService;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
{
|
||||
return $this->trackingModeService->getActiveMode();
|
||||
return $this->service->getActiveTrackingMode();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,95 +14,79 @@ namespace App\Configuration;
|
||||
*/
|
||||
class CalendarConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function find(string $key)
|
||||
{
|
||||
if (strpos($key, $this->getPrefix() . '.') === false) {
|
||||
$key = $this->getPrefix() . '.' . $key;
|
||||
}
|
||||
|
||||
return $this->configuration->find($key);
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
return 'calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getBusinessDays(): array
|
||||
{
|
||||
return (array) $this->find('businessHours.days');
|
||||
return $this->configuration->getCalendarBusinessDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeBegin(): string
|
||||
{
|
||||
return (string) $this->find('businessHours.begin');
|
||||
return $this->configuration->getCalendarBusinessTimeBegin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeEnd(): string
|
||||
{
|
||||
return (string) $this->find('businessHours.end');
|
||||
return $this->configuration->getCalendarBusinessTimeEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTimeframeBegin(): string
|
||||
{
|
||||
return (string) $this->find('visibleHours.begin');
|
||||
return $this->configuration->getCalendarTimeframeBegin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTimeframeEnd(): string
|
||||
{
|
||||
return (string) $this->find('visibleHours.end');
|
||||
return $this->configuration->getCalendarTimeframeEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDayLimit(): int
|
||||
{
|
||||
return (int) $this->find('day_limit');
|
||||
return $this->configuration->getCalendarDayLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isShowWeekNumbers(): bool
|
||||
{
|
||||
return (bool) $this->find('week_numbers');
|
||||
return $this->configuration->isCalendarShowWeekNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isShowWeekends(): bool
|
||||
{
|
||||
return (bool) $this->find('weekends');
|
||||
return $this->configuration->isCalendarShowWeekends();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getGoogleApiKey(): ?string
|
||||
{
|
||||
return $this->find('google.api_key');
|
||||
return $this->configuration->getCalendarGoogleApiKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|array
|
||||
*/
|
||||
public function getGoogleSources(): ?array
|
||||
{
|
||||
return $this->find('google.sources');
|
||||
return $this->configuration->getCalendarGoogleSources();
|
||||
}
|
||||
|
||||
public function getSlotDuration(): string
|
||||
{
|
||||
return (string) $this->find('slot_duration');
|
||||
return $this->configuration->getCalendarSlotDuration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,21 @@ namespace App\Configuration;
|
||||
*/
|
||||
class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function find(string $key)
|
||||
{
|
||||
if (strpos($key, $this->getPrefix() . '.') === false) {
|
||||
$key = $this->getPrefix() . '.' . $key;
|
||||
}
|
||||
|
||||
return $this->configuration->find($key);
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
@@ -23,36 +37,36 @@ class FormConfiguration implements SystemBundleConfiguration
|
||||
|
||||
public function getCustomerDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('customer.timezone');
|
||||
return $this->configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
|
||||
public function getCustomerDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('customer.currency');
|
||||
return $this->configuration->getCustomerDefaultCurrency();
|
||||
}
|
||||
|
||||
public function getCustomerDefaultCountry(): string
|
||||
{
|
||||
return $this->find('customer.country');
|
||||
return $this->configuration->getCustomerDefaultCountry();
|
||||
}
|
||||
|
||||
public function getUserDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('user.timezone');
|
||||
return $this->configuration->getUserDefaultTimezone();
|
||||
}
|
||||
|
||||
public function getUserDefaultTheme(): ?string
|
||||
{
|
||||
return $this->find('user.theme');
|
||||
return $this->configuration->getUserDefaultTheme();
|
||||
}
|
||||
|
||||
public function getUserDefaultLanguage(): string
|
||||
{
|
||||
return $this->find('user.language');
|
||||
return $this->configuration->getUserDefaultLanguage();
|
||||
}
|
||||
|
||||
public function getUserDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('user.currency');
|
||||
return $this->configuration->getUserDefaultCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $repository->getConfiguration();
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultBeginTime(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.default_begin');
|
||||
}
|
||||
// ========== Calendar configurations ==========
|
||||
|
||||
public function getCalendarBusinessDays(): array
|
||||
{
|
||||
@@ -83,6 +80,8 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (string) $this->find('calendar.slot_duration');
|
||||
}
|
||||
|
||||
// ========== Customer configurations ==========
|
||||
|
||||
public function getCustomerDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('defaults.customer.timezone');
|
||||
@@ -98,6 +97,8 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $this->find('defaults.customer.country');
|
||||
}
|
||||
|
||||
// ========== User configurations ==========
|
||||
|
||||
public function getUserDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('defaults.user.timezone');
|
||||
@@ -117,4 +118,114 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $this->find('defaults.user.currency');
|
||||
}
|
||||
|
||||
// ========== Timesheet configurations ==========
|
||||
|
||||
public function getTimesheetDefaultBeginTime(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.default_begin');
|
||||
}
|
||||
|
||||
public function isTimesheetAllowFutureTimes(): bool
|
||||
{
|
||||
return (bool) $this->find('timesheet.rules.allow_future_times');
|
||||
}
|
||||
|
||||
public function isTimesheetAllowOverlappingRecords(): bool
|
||||
{
|
||||
return (bool) $this->find('timesheet.rules.allow_overlapping_records');
|
||||
}
|
||||
|
||||
public function getTimesheetTrackingMode(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.mode');
|
||||
}
|
||||
|
||||
public function isTimesheetMarkdownEnabled(): bool
|
||||
{
|
||||
return (bool) $this->find('timesheet.markdown_content');
|
||||
}
|
||||
|
||||
public function getTimesheetActiveEntriesHardLimit(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.active_entries.hard_limit');
|
||||
}
|
||||
|
||||
public function getTimesheetActiveEntriesSoftLimit(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.active_entries.soft_limit');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultRoundingDays(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.rounding.default.days');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultRoundingMode(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.rounding.default.mode');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultRoundingBegin(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.rounding.default.begin');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultRoundingEnd(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.rounding.default.end');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultRoundingDuration(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.rounding.default.duration');
|
||||
}
|
||||
|
||||
public function getTimesheetLockdownPeriodStart(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.rules.lockdown_period_start');
|
||||
}
|
||||
|
||||
public function getTimesheetLockdownPeriodEnd(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.rules.lockdown_period_end');
|
||||
}
|
||||
|
||||
public function getTimesheetLockdownGracePeriod(): string
|
||||
{
|
||||
return (string) $this->find('timesheet.rules.lockdown_grace_period');
|
||||
}
|
||||
|
||||
public function isTimesheetLockdownActive(): bool
|
||||
{
|
||||
return !empty($this->find('timesheet.rules.lockdown_period_start')) && !empty($this->find('timesheet.rules.lockdown_period_end'));
|
||||
}
|
||||
|
||||
private function getIncrement(string $key, int $fallback, int $min = 1): ?int
|
||||
{
|
||||
$config = $this->find($key);
|
||||
|
||||
if ($config === null || trim($config) === '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$config = (int) $config;
|
||||
|
||||
return $config < $min ? null : $config;
|
||||
}
|
||||
|
||||
public function getTimesheetIncrementDuration(): ?int
|
||||
{
|
||||
return $this->getIncrement('timesheet.duration_increment', $this->getTimesheetDefaultRoundingDuration(), 1);
|
||||
}
|
||||
|
||||
public function getTimesheetIncrementBegin(): ?int
|
||||
{
|
||||
return $this->getIncrement('timesheet.time_increment', $this->getTimesheetDefaultRoundingBegin(), 0);
|
||||
}
|
||||
|
||||
public function getTimesheetIncrementEnd(): ?int
|
||||
{
|
||||
return $this->getIncrement('timesheet.time_increment', $this->getTimesheetDefaultRoundingEnd(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,25 @@
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @internal will be deprecated soon, use SystemConfiguration instead
|
||||
* @deprecated since 1.13, use SystemConfiguration instead
|
||||
*/
|
||||
class TimesheetConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
use StringAccessibleConfigTrait;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function find(string $key)
|
||||
{
|
||||
if (strpos($key, $this->getPrefix() . '.') === false) {
|
||||
$key = $this->getPrefix() . '.' . $key;
|
||||
}
|
||||
|
||||
return $this->configuration->find($key);
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
@@ -23,81 +37,81 @@ class TimesheetConfiguration implements SystemBundleConfiguration
|
||||
|
||||
public function isAllowFutureTimes(): bool
|
||||
{
|
||||
return (bool) $this->find('rules.allow_future_times');
|
||||
return $this->configuration->isTimesheetAllowFutureTimes();
|
||||
}
|
||||
|
||||
public function isAllowOverlappingRecords(): bool
|
||||
{
|
||||
return (bool) $this->find('rules.allow_overlapping_records');
|
||||
return $this->configuration->isTimesheetAllowOverlappingRecords();
|
||||
}
|
||||
|
||||
public function getTrackingMode(): string
|
||||
{
|
||||
return (string) $this->find('mode');
|
||||
return $this->configuration->getTimesheetTrackingMode();
|
||||
}
|
||||
|
||||
public function getDefaultBeginTime(): string
|
||||
{
|
||||
return (string) $this->find('default_begin');
|
||||
return $this->configuration->getTimesheetDefaultBeginTime();
|
||||
}
|
||||
|
||||
public function isMarkdownEnabled(): bool
|
||||
{
|
||||
return (bool) $this->find('markdown_content');
|
||||
return $this->configuration->isTimesheetMarkdownEnabled();
|
||||
}
|
||||
|
||||
public function getActiveEntriesHardLimit(): int
|
||||
{
|
||||
return (int) $this->find('active_entries.hard_limit');
|
||||
return $this->configuration->getTimesheetActiveEntriesHardLimit();
|
||||
}
|
||||
|
||||
public function getActiveEntriesSoftLimit(): int
|
||||
{
|
||||
return (int) $this->find('active_entries.soft_limit');
|
||||
return $this->configuration->getTimesheetActiveEntriesSoftLimit();
|
||||
}
|
||||
|
||||
public function getDefaultRoundingDays(): string
|
||||
{
|
||||
return (string) $this->find('rounding.default.days');
|
||||
return $this->configuration->getTimesheetDefaultRoundingDays();
|
||||
}
|
||||
|
||||
public function getDefaultRoundingMode(): string
|
||||
{
|
||||
return (string) $this->find('rounding.default.mode');
|
||||
return $this->configuration->getTimesheetDefaultRoundingMode();
|
||||
}
|
||||
|
||||
public function getDefaultRoundingBegin(): int
|
||||
{
|
||||
return (int) $this->find('rounding.default.begin');
|
||||
return $this->configuration->getTimesheetDefaultRoundingBegin();
|
||||
}
|
||||
|
||||
public function getDefaultRoundingEnd(): int
|
||||
{
|
||||
return (int) $this->find('rounding.default.end');
|
||||
return $this->configuration->getTimesheetDefaultRoundingEnd();
|
||||
}
|
||||
|
||||
public function getDefaultRoundingDuration(): int
|
||||
{
|
||||
return (int) $this->find('rounding.default.duration');
|
||||
return $this->configuration->getTimesheetDefaultRoundingDuration();
|
||||
}
|
||||
|
||||
public function getLockdownPeriodStart(): string
|
||||
{
|
||||
return (string) $this->find('rules.lockdown_period_start');
|
||||
return $this->configuration->getTimesheetLockdownPeriodStart();
|
||||
}
|
||||
|
||||
public function getLockdownPeriodEnd(): string
|
||||
{
|
||||
return (string) $this->find('rules.lockdown_period_end');
|
||||
return $this->configuration->getTimesheetLockdownPeriodEnd();
|
||||
}
|
||||
|
||||
public function getLockdownGracePeriod(): string
|
||||
{
|
||||
return (string) $this->find('rules.lockdown_grace_period');
|
||||
return $this->configuration->getTimesheetLockdownGracePeriod();
|
||||
}
|
||||
|
||||
public function isLockdownActive(): bool
|
||||
{
|
||||
return !empty($this->find('rules.lockdown_period_start')) && !empty($this->find('rules.lockdown_period_end'));
|
||||
return $this->configuration->isTimesheetLockdownActive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ class ExportController extends AbstractController
|
||||
'action' => $this->generateUrl('export', []),
|
||||
'include_user' => $this->isGranted('view_other_timesheet'),
|
||||
'method' => $method,
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'attr' => [
|
||||
'id' => 'export-form'
|
||||
]
|
||||
|
||||
@@ -408,6 +408,7 @@ final class InvoiceController extends AbstractController
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'GET',
|
||||
'include_user' => $this->isGranted('view_other_timesheet'),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Event\RecentActivityEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -20,7 +20,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
*/
|
||||
class LayoutController extends AbstractController
|
||||
{
|
||||
public function activeEntries(TimesheetRepository $repository, TimesheetConfiguration $configuration, EventDispatcherInterface $dispatcher): Response
|
||||
public function activeEntries(TimesheetRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$activeEntries = $repository->getActiveEntries($user);
|
||||
@@ -32,7 +32,7 @@ class LayoutController extends AbstractController
|
||||
'navbar/active-entries.html.twig',
|
||||
[
|
||||
'entries' => $recentActivity->getRecentActivities(),
|
||||
'soft_limit' => $configuration->getActiveEntriesSoftLimit(),
|
||||
'soft_limit' => $configuration->getTimesheetActiveEntriesSoftLimit(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -523,7 +523,9 @@ final class ProjectController extends AbstractController
|
||||
'action' => $url,
|
||||
'method' => 'POST',
|
||||
'currency' => $currency,
|
||||
'include_budget' => $this->isGranted('budget', $project)
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'include_budget' => $this->isGranted('budget', $project),
|
||||
'time_increment' => 15,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Form\SystemConfigurationForm;
|
||||
use App\Form\Type\DateTimeTextType;
|
||||
use App\Form\Type\DayTimeType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\MinuteIncrementType;
|
||||
use App\Form\Type\RoundingModeType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TrackingModeType;
|
||||
@@ -275,6 +276,21 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 1])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.time_increment')
|
||||
->setType(MinuteIncrementType::class)
|
||||
->setOptions(['deactivate' => false])
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 1])
|
||||
]),
|
||||
(new Configuration())
|
||||
->setName('timesheet.duration_increment')
|
||||
->setType(MinuteIncrementType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_ROUNDING)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
@@ -28,7 +29,6 @@ use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Timesheet\TrackingModeService;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -41,10 +41,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var TrackingModeService
|
||||
*/
|
||||
protected $trackingModeService;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
@@ -57,24 +53,28 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
* @var TimesheetService
|
||||
*/
|
||||
protected $service;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(
|
||||
TimesheetRepository $repository,
|
||||
TrackingModeService $trackingModeService,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
ServiceExport $exportService,
|
||||
TimesheetService $timesheetService
|
||||
TimesheetService $timesheetService,
|
||||
SystemConfiguration $configuration
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->trackingModeService = $trackingModeService;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->exportService = $exportService;
|
||||
$this->service = $timesheetService;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
{
|
||||
return $this->trackingModeService->getActiveMode();
|
||||
return $this->service->getActiveTrackingMode();
|
||||
}
|
||||
|
||||
protected function index($page, Request $request, string $renderTemplate, string $location): Response
|
||||
@@ -201,9 +201,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
|
||||
$this->service->prepareNewTimesheet($entry, $request);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
$createForm = $this->getCreateForm($entry, $mode);
|
||||
$createForm = $this->getCreateForm($entry);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
@@ -440,8 +438,10 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||
protected function getCreateForm(Timesheet $entry): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
||||
'action' => $this->generateUrl($this->getCreateRoute()),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
@@ -450,6 +450,10 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone(),
|
||||
'customer' => true,
|
||||
]);
|
||||
}
|
||||
@@ -474,6 +478,10 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone(),
|
||||
'customer' => true,
|
||||
]);
|
||||
}
|
||||
@@ -488,6 +496,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'action' => $this->generateUrl($this->getTimesheetRoute(), [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'method' => 'GET',
|
||||
'include_user' => $this->includeUserInForms('toolbar'),
|
||||
]);
|
||||
|
||||
@@ -20,7 +20,6 @@ use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -101,8 +100,7 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
$entry->setUser($this->getUser());
|
||||
$this->service->prepareNewTimesheet($entry, $request);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
$createForm = $this->getMultiUserCreateForm($entry, $mode);
|
||||
$createForm = $this->getMultiUserCreateForm($entry);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
@@ -150,8 +148,10 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getMultiUserCreateForm(MultiUserTimesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||
protected function getMultiUserCreateForm(MultiUserTimesheet $entry): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
return $this->createForm(TimesheetMultiUserEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('admin_timesheet_create_multiuser'),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
@@ -160,6 +160,10 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||
'allow_end_datetime' => $mode->canEditEnd(),
|
||||
'allow_duration' => $mode->canEditDuration(),
|
||||
'duration_minutes' => $this->configuration->getTimesheetIncrementDuration(),
|
||||
'begin_minutes' => $this->configuration->getTimesheetIncrementBegin(),
|
||||
'end_minutes' => $this->configuration->getTimesheetIncrementEnd(),
|
||||
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||
'customer' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -61,19 +61,19 @@ class AppExtension extends Extension
|
||||
$this->setLanguageFormats($config['languages'], $container);
|
||||
unset($config['languages']);
|
||||
|
||||
$container->setParameter('kimai.calendar', $config['calendar']);
|
||||
$container->setParameter('kimai.calendar', $config['calendar']); // @deprecated since 1.13
|
||||
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
||||
$container->setParameter('kimai.widgets', $config['widgets']);
|
||||
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
||||
$container->setParameter('kimai.export.documents', $config['export']['documents']);
|
||||
$container->setParameter('kimai.defaults', $config['defaults']);
|
||||
$container->setParameter('kimai.defaults', $config['defaults']); // @deprecated since 1.13
|
||||
|
||||
$this->createPermissionParameter($config['permissions'], $container);
|
||||
$this->createThemeParameter($config['theme'], $container);
|
||||
$this->createUserParameter($config['user'], $container);
|
||||
$container->setParameter('kimai.saml', $config['saml']);
|
||||
$container->setParameter('kimai.saml.connection', $config['saml']['connection']);
|
||||
$container->setParameter('kimai.timesheet', $config['timesheet']);
|
||||
$container->setParameter('kimai.timesheet', $config['timesheet']); // @deprecated since 1.13
|
||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
|
||||
|
||||
@@ -94,6 +94,32 @@ class Configuration implements ConfigurationInterface
|
||||
->booleanNode('markdown_content')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->scalarNode('duration_increment')
|
||||
->defaultNull()
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
if ($value !== null) {
|
||||
return ((int) $value) < 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Duration increment is invalid')
|
||||
->end()
|
||||
->end()
|
||||
->scalarNode('time_increment')
|
||||
->defaultNull()
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
if ($value !== null) {
|
||||
return ((int) $value) < 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Time increment is invalid')
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
|
||||
@@ -48,6 +48,7 @@ final class ThemeJavascriptTranslationsEvent extends Event
|
||||
'confirm.delete' => ['confirm.delete', 'messages'],
|
||||
'delete' => ['action.delete', 'messages'],
|
||||
'login.required' => ['login_required', 'messages'],
|
||||
'modal.dirty' => ['modal.dirty', 'messages']
|
||||
];
|
||||
|
||||
public function getTranslations(): array
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsType;
|
||||
use App\Repository\ActivityRepository;
|
||||
@@ -22,7 +23,6 @@ use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
@@ -136,11 +136,15 @@ trait FormTrait
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.13
|
||||
*/
|
||||
protected function addDescription(FormBuilderInterface $builder)
|
||||
{
|
||||
@trigger_error('FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead', E_USER_DEPRECATED);
|
||||
|
||||
$builder
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'label.description',
|
||||
->add('description', DescriptionType::class, [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
|
||||
@@ -44,12 +44,20 @@ class ProjectEditForm extends AbstractType
|
||||
}
|
||||
}
|
||||
|
||||
$dateTimeOptions = [];
|
||||
$dateTimeOptions = [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
];
|
||||
// primarily for API usage, where we cannot use a user/locale specific format
|
||||
if (null !== $options['date_format']) {
|
||||
$dateTimeOptions['format'] = $options['date_format'];
|
||||
}
|
||||
|
||||
$timeIncrement = 1;
|
||||
if ($options['time_increment'] >= 1 && $options['time_increment'] <= 60) {
|
||||
$timeIncrement = $options['time_increment'];
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
@@ -68,14 +76,17 @@ class ProjectEditForm extends AbstractType
|
||||
->add('orderDate', DateTimePickerType::class, array_merge($dateTimeOptions, [
|
||||
'label' => 'label.orderDate',
|
||||
'required' => false,
|
||||
'time_increment' => $timeIncrement,
|
||||
]))
|
||||
->add('start', DateTimePickerType::class, array_merge($dateTimeOptions, [
|
||||
'label' => 'label.project_start',
|
||||
'required' => false,
|
||||
'time_increment' => $timeIncrement,
|
||||
]))
|
||||
->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
|
||||
'label' => 'label.project_end',
|
||||
'required' => false,
|
||||
'time_increment' => $timeIncrement,
|
||||
]))
|
||||
->add('customer', CustomerType::class, [
|
||||
'placeholder' => (null === $id && null === $customer) ? '' : false,
|
||||
@@ -103,6 +114,8 @@ class ProjectEditForm extends AbstractType
|
||||
'currency' => Customer::DEFAULT_CURRENCY,
|
||||
'date_format' => null,
|
||||
'include_budget' => false,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'time_increment' => 1,
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectUpdate'
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@ class TimesheetAdminEditForm extends TimesheetEditForm
|
||||
{
|
||||
$options['allow_begin_datetime'] = true;
|
||||
$options['allow_end_datetime'] = true;
|
||||
$options['allow_duration'] = false;
|
||||
$options['allow_duration'] = true;
|
||||
|
||||
parent::buildForm($builder, $options);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Form;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
@@ -19,7 +20,6 @@ use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -41,21 +41,11 @@ class TimesheetEditForm extends AbstractType
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $projects;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
|
||||
/**
|
||||
* @param CustomerRepository $customer
|
||||
* @param ProjectRepository $project
|
||||
* @param UserDateTimeFactory $dateTime
|
||||
*/
|
||||
public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime)
|
||||
public function __construct(CustomerRepository $customer, ProjectRepository $project)
|
||||
{
|
||||
$this->customers = $customer;
|
||||
$this->projects = $project;
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +59,7 @@ class TimesheetEditForm extends AbstractType
|
||||
$currency = false;
|
||||
$begin = null;
|
||||
$customerCount = $this->customers->countCustomer(true);
|
||||
$timezone = $this->dateTime->getTimezone()->getName();
|
||||
$timezone = $options['timezone'];
|
||||
$isNew = true;
|
||||
|
||||
if (isset($options['data'])) {
|
||||
@@ -108,23 +98,15 @@ class TimesheetEditForm extends AbstractType
|
||||
}
|
||||
|
||||
if ($options['allow_begin_datetime']) {
|
||||
$this->addBegin($builder, $dateTimeOptions);
|
||||
$this->addBegin($builder, $dateTimeOptions, $options);
|
||||
}
|
||||
|
||||
if ($options['allow_end_datetime']) {
|
||||
$this->addEnd($builder, $dateTimeOptions, $options);
|
||||
}
|
||||
|
||||
if ($options['allow_duration']) {
|
||||
$this->addDuration($builder);
|
||||
} elseif ($options['allow_end_datetime']) {
|
||||
$this->addEnd($builder, $dateTimeOptions);
|
||||
}
|
||||
|
||||
if ($options['allow_begin_datetime'] && $options['allow_end_datetime']) {
|
||||
$builder->add('duration', DurationType::class, [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'placeholder' => '00:00',
|
||||
'pattern' => '[0-9]{2,3}:[0-9]{2}'
|
||||
]
|
||||
]);
|
||||
$this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
|
||||
}
|
||||
|
||||
if ($this->showCustomer($options, $isNew, $customerCount)) {
|
||||
@@ -133,7 +115,13 @@ class TimesheetEditForm extends AbstractType
|
||||
|
||||
$this->addProject($builder, $isNew, $project, $customer);
|
||||
$this->addActivity($builder, $activity, $project);
|
||||
$this->addDescription($builder);
|
||||
|
||||
$descriptionOptions = ['required' => false];
|
||||
if (!$isNew) {
|
||||
$descriptionOptions['attr'] = ['autofocus' => 'autofocus'];
|
||||
}
|
||||
$builder->add('description', DescriptionType::class, $descriptionOptions);
|
||||
|
||||
$this->addTags($builder);
|
||||
$this->addRates($builder, $currency, $options);
|
||||
$this->addUser($builder, $options);
|
||||
@@ -159,30 +147,58 @@ class TimesheetEditForm extends AbstractType
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function addBegin(FormBuilderInterface $builder, array $dateTimeOptions)
|
||||
protected function addBegin(FormBuilderInterface $builder, array $dateTimeOptions, array $options = [])
|
||||
{
|
||||
if ($options['begin_minutes'] >= 1 && $options['begin_minutes'] <= 60) {
|
||||
$dateTimeOptions['time_increment'] = $options['begin_minutes'];
|
||||
}
|
||||
|
||||
$builder->add('begin', DateTimePickerType::class, array_merge($dateTimeOptions, [
|
||||
'label' => 'label.begin'
|
||||
'label' => 'label.begin',
|
||||
]));
|
||||
}
|
||||
|
||||
protected function addEnd(FormBuilderInterface $builder, array $dateTimeOptions)
|
||||
protected function addEnd(FormBuilderInterface $builder, array $dateTimeOptions, array $options = [])
|
||||
{
|
||||
if ($options['end_minutes'] >= 1 && $options['end_minutes'] <= 60) {
|
||||
$dateTimeOptions['time_increment'] = (int) $options['end_minutes'];
|
||||
}
|
||||
|
||||
$builder->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
|
||||
'label' => 'label.end',
|
||||
'required' => false,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function addDuration(FormBuilderInterface $builder)
|
||||
protected function addDuration(FormBuilderInterface $builder, array $options, bool $forceApply = false, bool $autofocus = false)
|
||||
{
|
||||
$builder->add('duration', DurationType::class, [
|
||||
$durationOptions = [
|
||||
'required' => false,
|
||||
'docu_chapter' => 'timesheet.html#duration-format',
|
||||
'attr' => [
|
||||
'placeholder' => '00:00',
|
||||
]
|
||||
]);
|
||||
'placeholder' => '0:00',
|
||||
],
|
||||
];
|
||||
|
||||
if ($autofocus) {
|
||||
$durationOptions['attr']['autofocus'] = 'autofocus';
|
||||
}
|
||||
|
||||
$duration = $options['duration_minutes'];
|
||||
if ($duration !== null && (int) $duration > 0) {
|
||||
$durationOptions = array_merge($durationOptions, [
|
||||
'preset_minutes' => $duration
|
||||
]);
|
||||
}
|
||||
|
||||
$duration = $options['duration_hours'];
|
||||
if ($duration !== null && (int) $duration > 0) {
|
||||
$durationOptions = array_merge($durationOptions, [
|
||||
'preset_hours' => $duration,
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add('duration', DurationType::class, $durationOptions);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::POST_SET_DATA,
|
||||
@@ -198,16 +214,17 @@ class TimesheetEditForm extends AbstractType
|
||||
// make sure that duration is mapped back to end field
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($forceApply) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
$duration = $data->getDuration();
|
||||
$end = null;
|
||||
if (null !== $duration) {
|
||||
// only apply the duration, if the end is not yet set
|
||||
// without that check, the end would be overwritten and the real end time would be lost
|
||||
if (($forceApply && null !== $duration) || (null !== $duration && null === $data->getEnd())) {
|
||||
$end = clone $data->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$data->setEnd($end);
|
||||
}
|
||||
$data->setEnd($end);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -263,10 +280,15 @@ class TimesheetEditForm extends AbstractType
|
||||
'docu_chapter' => 'timesheet.html',
|
||||
'method' => 'POST',
|
||||
'date_format' => null,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'customer' => false, // for API usage
|
||||
'allow_begin_datetime' => true,
|
||||
'allow_end_datetime' => true,
|
||||
'allow_duration' => false,
|
||||
'duration_minutes' => null,
|
||||
'duration_hours' => 10,
|
||||
'begin_minutes' => 1,
|
||||
'end_minutes' => 1,
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.timesheetUpdate',
|
||||
'data-msg-success' => 'action.update.success',
|
||||
|
||||
@@ -20,7 +20,6 @@ class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
|
||||
{
|
||||
$options['allow_begin_datetime'] = true;
|
||||
$options['allow_end_datetime'] = true;
|
||||
$options['allow_duration'] = false;
|
||||
$options['include_user'] = false;
|
||||
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
@@ -32,7 +32,7 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
if ($options['include_user']) {
|
||||
$this->addUsersChoice($builder);
|
||||
}
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
|
||||
$this->addActivityMultiChoice($builder, [], true);
|
||||
@@ -64,6 +64,7 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
'data_class' => ExportQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'include_user' => true,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerChoice($builder, ['required' => true, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
@@ -63,6 +63,7 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
|
||||
'data_class' => InvoiceQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'include_user' => true,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
}
|
||||
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerMultiChoice($builder, $newOptions, true);
|
||||
$this->addProjectMultiChoice($builder, $newOptions, true, true);
|
||||
$this->addActivityMultiChoice($builder, [], true);
|
||||
@@ -55,6 +55,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
'csrf_protection' => false,
|
||||
'include_user' => false,
|
||||
'ignore_date' => true,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
@@ -23,12 +22,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
class DatePickerType extends AbstractType
|
||||
{
|
||||
private $localeSettings;
|
||||
private $dateTime;
|
||||
|
||||
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
|
||||
public function __construct(LocaleSettings $localeSettings)
|
||||
{
|
||||
$this->localeSettings = $localeSettings;
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,15 +35,14 @@ class DatePickerType extends AbstractType
|
||||
{
|
||||
$pickerFormat = $this->localeSettings->getDatePickerFormat();
|
||||
$dateFormat = $this->localeSettings->getDateTypeFormat();
|
||||
$timezone = $this->dateTime->getTimezone()->getName();
|
||||
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => $dateFormat,
|
||||
'format_picker' => $pickerFormat,
|
||||
'model_timezone' => $timezone,
|
||||
'view_timezone' => $timezone,
|
||||
'model_timezone' => date_default_timezone_get(),
|
||||
'view_timezone' => date_default_timezone_get(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Form\Model\DateRange;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
@@ -28,19 +27,11 @@ class DateRangeType extends AbstractType
|
||||
{
|
||||
public const DATE_SPACER = ' - ';
|
||||
|
||||
/**
|
||||
* @var LocaleSettings
|
||||
*/
|
||||
private $localeSettings;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateFactory;
|
||||
|
||||
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
|
||||
public function __construct(LocaleSettings $localeSettings)
|
||||
{
|
||||
$this->localeSettings = $localeSettings;
|
||||
$this->dateFactory = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,8 +117,7 @@ class DateRangeType extends AbstractType
|
||||
$formatDate = $options['format'];
|
||||
$separator = $options['separator'];
|
||||
$allowEmpty = $options['allow_empty'];
|
||||
//$timezone = new \DateTimeZone($options['timezone']);
|
||||
$timezone = $this->dateFactory->getTimezone();
|
||||
$timezone = new \DateTimeZone($options['timezone']);
|
||||
$pattern = $this->formatToPattern($formatDate, $separator);
|
||||
|
||||
$builder->addModelTransformer(new CallbackTransformer(
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\API\BaseApiController;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
@@ -23,20 +22,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class DateTimePickerType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var LocaleSettings
|
||||
*/
|
||||
protected $localeSettings;
|
||||
private $localeSettings;
|
||||
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
|
||||
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
|
||||
public function __construct(LocaleSettings $localeSettings)
|
||||
{
|
||||
$this->localeSettings = $localeSettings;
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +36,6 @@ class DateTimePickerType extends AbstractType
|
||||
{
|
||||
$dateTimePicker = $this->localeSettings->getDateTimePickerFormat();
|
||||
$dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat();
|
||||
$timezone = $this->dateTime->getTimezone()->getName();
|
||||
|
||||
$resolver->setDefaults([
|
||||
'documentation' => [
|
||||
@@ -60,8 +49,7 @@ class DateTimePickerType extends AbstractType
|
||||
'format' => $dateTimeFormat,
|
||||
'format_picker' => $dateTimePicker,
|
||||
'with_seconds' => false,
|
||||
'model_timezone' => $timezone,
|
||||
'view_timezone' => $timezone,
|
||||
'time_increment' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -72,6 +60,7 @@ class DateTimePickerType extends AbstractType
|
||||
'autocomplete' => 'off',
|
||||
'placeholder' => strtoupper($options['format']),
|
||||
'data-format' => $options['format_picker'],
|
||||
'data-time-picker-increment' => $options['time_increment'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
35
src/Form/Type/DescriptionType.php
Normal file
35
src/Form/Type/DescriptionType.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\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DescriptionType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.description',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return TextareaType::class;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ use App\Validator\Constraints\Duration as DurationConstraint;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -29,9 +31,37 @@ class DurationType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.duration',
|
||||
'constraints' => [new DurationConstraint()],
|
||||
'preset_hours' => null,
|
||||
'preset_minutes' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
if ($options['preset_hours'] === null || $options['preset_minutes'] === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$intervalMinutes = (int) $options['preset_minutes'];
|
||||
$maxHours = (int) $options['preset_hours'];
|
||||
|
||||
if ($intervalMinutes < 1 || $maxHours < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$maxMinutes = $maxHours * 60;
|
||||
$presets = [];
|
||||
|
||||
for ($minutes = $intervalMinutes; $minutes <= $maxMinutes; $minutes += $intervalMinutes) {
|
||||
$h = (int) ($minutes / 60);
|
||||
$m = $minutes % 60;
|
||||
$interval = new \DateInterval('PT' . $h . 'H' . $m . 'M');
|
||||
$presets[] = $interval->format('%h:%I');
|
||||
}
|
||||
|
||||
$view->vars['duration_presets'] = $presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
64
src/Form/Type/MinuteIncrementType.php
Normal file
64
src/Form/Type/MinuteIncrementType.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select the minute increment.
|
||||
*/
|
||||
class MinuteIncrementType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'deactivate' => true,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
$choices = ['increment_rounding' => null];
|
||||
|
||||
if ($options['deactivate']) {
|
||||
$choices['off'] = '0';
|
||||
}
|
||||
|
||||
$choices['1'] = '1';
|
||||
$choices['2'] = '2';
|
||||
$choices['3'] = '3';
|
||||
$choices['4'] = '4';
|
||||
$choices['5'] = '5';
|
||||
$choices['10'] = '10';
|
||||
$choices['15'] = '15';
|
||||
$choices['20'] = '20';
|
||||
$choices['25'] = '25';
|
||||
$choices['30'] = '30';
|
||||
$choices['45'] = '45';
|
||||
$choices['60'] = '60';
|
||||
$choices['90'] = '90';
|
||||
$choices['120'] = '120';
|
||||
|
||||
return $choices;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
|
||||
@@ -24,7 +24,7 @@ final class RoundingService
|
||||
*/
|
||||
private $rulesCache;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -33,11 +33,11 @@ final class RoundingService
|
||||
private $roundingModes;
|
||||
|
||||
/**
|
||||
* @param TimesheetConfiguration $configuration
|
||||
* @param SystemConfiguration $configuration
|
||||
* @param RoundingInterface[] $roundingModes
|
||||
* @param array $rules
|
||||
*/
|
||||
public function __construct(TimesheetConfiguration $configuration, iterable $roundingModes, array $rules)
|
||||
public function __construct(SystemConfiguration $configuration, iterable $roundingModes, array $rules)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->roundingModes = $roundingModes;
|
||||
@@ -49,11 +49,11 @@ final class RoundingService
|
||||
if (empty($this->rulesCache)) {
|
||||
$this->rulesCache = $this->rules;
|
||||
if (empty($this->rulesCache) || \array_key_exists('default', $this->rulesCache)) {
|
||||
$this->rulesCache['default']['days'] = $this->configuration->getDefaultRoundingDays();
|
||||
$this->rulesCache['default']['begin'] = $this->configuration->getDefaultRoundingBegin();
|
||||
$this->rulesCache['default']['end'] = $this->configuration->getDefaultRoundingEnd();
|
||||
$this->rulesCache['default']['duration'] = $this->configuration->getDefaultRoundingDuration();
|
||||
$this->rulesCache['default']['mode'] = $this->configuration->getDefaultRoundingMode();
|
||||
$this->rulesCache['default']['days'] = $this->configuration->getTimesheetDefaultRoundingDays();
|
||||
$this->rulesCache['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin();
|
||||
$this->rulesCache['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd();
|
||||
$this->rulesCache['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration();
|
||||
$this->rulesCache['default']['mode'] = $this->configuration->getTimesheetDefaultRoundingMode();
|
||||
}
|
||||
|
||||
// see AppExtension, conversion from string to array due to system configuration ont allowing to store arrays
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Event\TimesheetCreatePostEvent;
|
||||
@@ -27,6 +27,7 @@ use App\Event\TimesheetUpdatePostEvent;
|
||||
use App\Event\TimesheetUpdatePreEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Security\AccessDeniedException;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Validator\ValidationException;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
@@ -42,7 +43,7 @@ final class TimesheetService
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -63,7 +64,7 @@ final class TimesheetService
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
TimesheetConfiguration $configuration,
|
||||
SystemConfiguration $configuration,
|
||||
TimesheetRepository $repository,
|
||||
TrackingModeService $service,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
@@ -293,7 +294,7 @@ final class TimesheetService
|
||||
*/
|
||||
private function stopActiveEntries(Timesheet $timesheet): int
|
||||
{
|
||||
$hardLimit = $this->configuration->getActiveEntriesHardLimit();
|
||||
$hardLimit = $this->configuration->getTimesheetActiveEntriesHardLimit();
|
||||
$activeEntries = $this->repository->getActiveEntries($timesheet->getUser());
|
||||
|
||||
if (empty($activeEntries)) {
|
||||
@@ -314,4 +315,9 @@ final class TimesheetService
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
public function getActiveTrackingMode(): TrackingModeInterface
|
||||
{
|
||||
return $this->trackingModeService->getActiveMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,27 +9,13 @@
|
||||
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
{
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
use TrackingModeTrait;
|
||||
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
@@ -48,7 +34,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$start = $this->dateTime->createDateTimeFromFormat('Y-m-d', $start);
|
||||
$start = DateTime::createFromFormat('Y-m-d', $start, $this->getTimezone($entry));
|
||||
if (false === $start) {
|
||||
return;
|
||||
}
|
||||
@@ -61,7 +47,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$end = $this->dateTime->createDateTimeFromFormat('Y-m-d', $end);
|
||||
$end = DateTime::createFromFormat('Y-m-d', $end, $this->getTimezone($entry));
|
||||
if (false === $end) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +67,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
}
|
||||
|
||||
try {
|
||||
$from = $this->dateTime->createDateTime($from);
|
||||
$from = new DateTime($from, $this->getTimezone($entry));
|
||||
} catch (\Exception $ex) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +80,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
}
|
||||
|
||||
try {
|
||||
$to = $this->dateTime->createDateTime($to);
|
||||
$to = new DateTime($to, $this->getTimezone($entry));
|
||||
} catch (\Exception $ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\RoundingService;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class DefaultMode extends AbstractTrackingMode
|
||||
@@ -22,9 +21,8 @@ final class DefaultMode extends AbstractTrackingMode
|
||||
*/
|
||||
private $rounding;
|
||||
|
||||
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, RoundingService $rounding)
|
||||
public function __construct(RoundingService $rounding)
|
||||
{
|
||||
parent::__construct($dateTime, $configuration);
|
||||
$this->rounding = $rounding;
|
||||
}
|
||||
|
||||
@@ -40,7 +38,7 @@ final class DefaultMode extends AbstractTrackingMode
|
||||
|
||||
public function canEditDuration(): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canUpdateTimesWithAPI(): bool
|
||||
@@ -63,7 +61,7 @@ final class DefaultMode extends AbstractTrackingMode
|
||||
parent::create($timesheet, $request);
|
||||
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
|
||||
$this->rounding->roundBegin($timesheet);
|
||||
|
||||
@@ -9,25 +9,22 @@
|
||||
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class DurationFixedBeginMode implements TrackingModeInterface
|
||||
{
|
||||
use TrackingModeTrait;
|
||||
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateTime;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
@@ -54,11 +51,11 @@ final class DurationFixedBeginMode implements TrackingModeInterface
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
|
||||
$newBegin = clone $timesheet->getBegin();
|
||||
$newBegin->modify($this->configuration->getDefaultBeginTime());
|
||||
$newBegin->modify($this->configuration->getTimesheetDefaultBeginTime());
|
||||
$timesheet->setBegin($newBegin);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,23 @@
|
||||
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class DurationOnlyMode extends AbstractTrackingMode
|
||||
{
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function canEditBegin(): bool
|
||||
{
|
||||
return true;
|
||||
@@ -47,11 +59,11 @@ final class DurationOnlyMode extends AbstractTrackingMode
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
|
||||
$newBegin = clone $timesheet->getBegin();
|
||||
$newBegin->modify($this->configuration->getDefaultBeginTime());
|
||||
$newBegin->modify($this->configuration->getTimesheetDefaultBeginTime());
|
||||
$timesheet->setBegin($newBegin);
|
||||
|
||||
parent::create($timesheet, $request);
|
||||
|
||||
@@ -10,20 +10,12 @@
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class PunchInOutMode implements TrackingModeInterface
|
||||
{
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateTime;
|
||||
|
||||
public function __construct(UserDateTimeFactory $dateTime)
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
use TrackingModeTrait;
|
||||
|
||||
public function canEditBegin(): bool
|
||||
{
|
||||
@@ -48,7 +40,7 @@ final class PunchInOutMode implements TrackingModeInterface
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
src/Timesheet/TrackingMode/TrackingModeTrait.php
Normal file
31
src/Timesheet/TrackingMode/TrackingModeTrait.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Timesheet\TrackingMode;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use DateTimeZone;
|
||||
|
||||
trait TrackingModeTrait
|
||||
{
|
||||
protected function getTimezone(Timesheet $timesheet): DateTimeZone
|
||||
{
|
||||
if ($timesheet->getBegin() !== null) {
|
||||
return $timesheet->getBegin()->getTimezone();
|
||||
}
|
||||
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
if ($timesheet->getUser() !== null) {
|
||||
$timezone = $timesheet->getUser()->getTimezone();
|
||||
}
|
||||
|
||||
return new DateTimeZone($timezone);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
@@ -20,15 +20,15 @@ final class TrackingModeService
|
||||
*/
|
||||
private $modes = [];
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @param TimesheetConfiguration $configuration
|
||||
* @param SystemConfiguration $configuration
|
||||
* @param TrackingModeInterface[] $modes
|
||||
*/
|
||||
public function __construct(TimesheetConfiguration $configuration, iterable $modes)
|
||||
public function __construct(SystemConfiguration $configuration, iterable $modes)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->modes = $modes;
|
||||
@@ -44,7 +44,7 @@ final class TrackingModeService
|
||||
|
||||
public function getActiveMode(): TrackingModeInterface
|
||||
{
|
||||
$trackingMode = $this->configuration->getTrackingMode();
|
||||
$trackingMode = $this->configuration->getTimesheetTrackingMode();
|
||||
|
||||
foreach ($this->getModes() as $mode) {
|
||||
if ($mode->getId() === $trackingMode) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Utils\Markdown;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
@@ -24,14 +24,14 @@ final class MarkdownExtension extends AbstractExtension
|
||||
*/
|
||||
private $markdown;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @param Markdown $parser
|
||||
*/
|
||||
public function __construct(Markdown $parser, TimesheetConfiguration $configuration)
|
||||
public function __construct(Markdown $parser, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->markdown = $parser;
|
||||
$this->configuration = $configuration;
|
||||
@@ -66,7 +66,7 @@ final class MarkdownExtension extends AbstractExtension
|
||||
$content = trim(substr($content, 0, 100)) . ' …';
|
||||
}
|
||||
|
||||
if ($this->configuration->isMarkdownEnabled()) {
|
||||
if ($this->configuration->isTimesheetMarkdownEnabled()) {
|
||||
$content = $this->markdown->toHtml($content, false);
|
||||
} elseif ($fullLength) {
|
||||
$content = '<p>' . nl2br($content) . '</p>';
|
||||
@@ -87,7 +87,7 @@ final class MarkdownExtension extends AbstractExtension
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->configuration->isMarkdownEnabled()) {
|
||||
if ($this->configuration->isTimesheetMarkdownEnabled()) {
|
||||
return $this->markdown->toHtml($content, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ class Duration
|
||||
{
|
||||
public const FORMAT_COLON = 'colon';
|
||||
public const FORMAT_NATURAL = 'natural';
|
||||
public const FORMAT_DECIMAL = 'decimal';
|
||||
|
||||
/**
|
||||
* @deprecated since 1.13
|
||||
*/
|
||||
public const FORMAT_SECONDS = 'seconds';
|
||||
|
||||
public const FORMAT_WITH_SECONDS = '%h:%m:%s';
|
||||
@@ -61,8 +66,12 @@ class Duration
|
||||
return $this->parseDuration($duration, self::FORMAT_COLON);
|
||||
}
|
||||
|
||||
if (strpos($duration, '.') !== false || strpos($duration, ',') !== false) {
|
||||
return $this->parseDuration($duration, self::FORMAT_DECIMAL);
|
||||
}
|
||||
|
||||
if (is_numeric($duration) && $duration == (int) $duration) {
|
||||
return $this->parseDuration($duration, self::FORMAT_SECONDS);
|
||||
return $this->parseDuration($duration, self::FORMAT_DECIMAL);
|
||||
}
|
||||
|
||||
return $this->parseDuration($duration, self::FORMAT_NATURAL);
|
||||
@@ -91,7 +100,12 @@ class Duration
|
||||
$seconds = $this->parseNaturalFormat($duration);
|
||||
break;
|
||||
|
||||
case self::FORMAT_DECIMAL:
|
||||
$seconds = $this->parseDecimalFormat($duration);
|
||||
break;
|
||||
|
||||
case self::FORMAT_SECONDS:
|
||||
@trigger_error('Duration format FORMAT_SECONDS is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
$seconds = (int) $duration;
|
||||
break;
|
||||
|
||||
@@ -119,6 +133,15 @@ class Duration
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseDecimalFormat(string $duration): int
|
||||
{
|
||||
$duration = str_replace(',', '.', $duration);
|
||||
$duration = (float) $duration;
|
||||
$duration = $duration * 3600;
|
||||
|
||||
return (int) $duration;
|
||||
}
|
||||
|
||||
protected function parseColonFormat(string $duration): int
|
||||
{
|
||||
$parts = explode(':', $duration);
|
||||
|
||||
@@ -20,12 +20,18 @@ class Duration extends Regex
|
||||
public function __construct($options = null)
|
||||
{
|
||||
$patterns = [
|
||||
// decimal times (can be separated by comma or dot, depending on the locale)
|
||||
'[0-9]{1,}',
|
||||
'[0-9]{1,}[,.]{1}[0-9]{1,}',
|
||||
// ASP.NET style time spans - https://momentjs.com/docs/#/durations/
|
||||
'[0-9]{1,}:[0-9]{1,}:[0-9]{1,}',
|
||||
'[0-9]{1,}:[0-9]{1,}',
|
||||
'[0-9]{1,}[hmsHMS]{1}',
|
||||
'[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{1}',
|
||||
'[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{1}',
|
||||
// https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
||||
'[0-9]{1,}[hHmMsS]{1}',
|
||||
'[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}',
|
||||
'[0-9]{1,}[hHmM]{1}[0-9]{1,}[sS]{1}',
|
||||
'[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}',
|
||||
'[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}',
|
||||
];
|
||||
$options['pattern'] = '/^' . implode('$|^', $patterns) . '$/';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
@@ -18,11 +18,11 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
final class TimesheetFutureTimesValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(TimesheetConfiguration $configuration)
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
@@ -41,14 +41,14 @@ final class TimesheetFutureTimesValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($this->configuration->isAllowFutureTimes()) {
|
||||
if ($this->configuration->isTimesheetAllowFutureTimes()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = new \DateTime('now', $timesheet->getBegin()->getTimezone());
|
||||
|
||||
// allow configured default rounding time + 1 minute - see #1295
|
||||
$allowedDiff = ($this->configuration->getDefaultRoundingBegin() * 60) + 60;
|
||||
$allowedDiff = ($this->configuration->getTimesheetDefaultRoundingBegin() * 60) + 60;
|
||||
if (($now->getTimestamp() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
|
||||
$this->context->buildViolation('The begin date cannot be in the future.')
|
||||
->atPath('begin')
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
@@ -23,11 +23,11 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
*/
|
||||
private $auth;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration)
|
||||
public function __construct(AuthorizationCheckerInterface $auth, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
$this->configuration = $configuration;
|
||||
@@ -53,14 +53,14 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->configuration->isLockdownActive()) {
|
||||
if (!$this->configuration->isTimesheetLockdownActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lockedStart = $this->configuration->getLockdownPeriodStart();
|
||||
$lockedEnd = $this->configuration->getLockdownPeriodEnd();
|
||||
$lockedStart = $this->configuration->getTimesheetLockdownPeriodStart();
|
||||
$lockedEnd = $this->configuration->getTimesheetLockdownPeriodEnd();
|
||||
|
||||
$gracePeriod = $this->configuration->getLockdownGracePeriod();
|
||||
$gracePeriod = $this->configuration->getTimesheetLockdownGracePeriod();
|
||||
if (!empty($gracePeriod)) {
|
||||
$gracePeriod = $gracePeriod . ' ';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
@@ -19,7 +19,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(TimesheetConfiguration $configuration, TimesheetRepository $repository)
|
||||
public function __construct(SystemConfiguration $configuration, TimesheetRepository $repository)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->repository = $repository;
|
||||
@@ -55,7 +55,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configuration->isAllowOverlappingRecords()) {
|
||||
if ($this->configuration->isTimesheetAllowOverlappingRecords()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user