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:
Kevin Papst
2021-01-17 14:04:13 +01:00
committed by GitHub
parent e2324b7d51
commit 8d72d114c7
95 changed files with 1381 additions and 510 deletions

View File

@@ -12,6 +12,7 @@ Perform EACH version specific task between your version and the new one, otherwi
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}` - Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
- Changed invoice filename generation (check if you use cronjob for invoices) - Changed invoice filename generation (check if you use cronjob for invoices)
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
## [1.12](https://github.com/kevinpapst/kimai2/releases/tag/1.12) ## [1.12](https://github.com/kevinpapst/kimai2/releases/tag/1.12)
@@ -25,7 +26,7 @@ Perform EACH version specific task between your version and the new one, otherwi
- Sessions are now stored in the database (all users have to re-login after upgrade) - Sessions are now stored in the database (all users have to re-login after upgrade)
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data` - New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission) - Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
- Markdown does not support headings any more, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore - Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
### Developer ### Developer

View File

@@ -28,9 +28,20 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
init() { init() {
const self = this; const self = this;
this.isDirty = false;
this.modal = jQuery('#remote_form_modal'); this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function () { this.modal.on('hide.bs.modal', function (e) {
if (self.isDirty) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
}
e.preventDefault();
return;
}
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
self.isDirty = false;
self.getContainer().getPlugin('event').trigger('modal-hide'); self.getContainer().getPlugin('event').trigger('modal-hide');
}); });
this.modal.on('hidden.bs.modal', function () { this.modal.on('hidden.bs.modal', function () {
@@ -44,7 +55,11 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
}); });
this.modal.on('shown.bs.modal', function () { this.modal.on('shown.bs.modal', function () {
// workaround for autofocus attribute, as the modal "steals" it // workaround for autofocus attribute, as the modal "steals" it
jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus(); let formAutofocus = jQuery(self._getFormIdentifier()).find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
}); });
this._addClickHandler(this.selector, function(href) { this._addClickHandler(this.selector, function(href) {
@@ -97,7 +112,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
form.off('submit'); form.off('submit');
// load new form from given content // load new form from given content
if (jQuery(html).find('#form_modal .modal-content').length > 0 ) { if (jQuery(html).find('#form_modal .modal-content').length > 0) {
// switch classes, in case the modal type changed // switch classes, in case the modal type changed
remoteModal.on('hidden.bs.modal', function () { remoteModal.on('hidden.bs.modal', function () {
if (remoteModal.hasClass('modal-danger')) { if (remoteModal.hasClass('modal-danger')) {
@@ -113,6 +128,10 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
jQuery(html).find('#form_modal .modal-content') jQuery(html).find('#form_modal .modal-content')
); );
jQuery('#remote_form_modal [data-dismiss=modal]').on('click', function() {
self.isDirty = false;
});
// activate new loaded widgets // activate new loaded widgets
self.getContainer().getPlugin('form').activateForm(formIdentifier); self.getContainer().getPlugin('form').activateForm(formIdentifier);
} }
@@ -139,6 +158,11 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
// the new form that was loaded via ajax // the new form that was loaded via ajax
form = jQuery(formIdentifier); form = jQuery(formIdentifier);
this._isDirtyHandler = function(e) {
self.isDirty = true;
}
form.on('change', this._isDirtyHandler);
// click handler for modal save button, to send forms via ajax // click handler for modal save button, to send forms via ajax
form.on('submit', function(event){ form.on('submit', function(event){
const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading'); const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
@@ -181,6 +205,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
if (msg === null || msg === undefined) { if (msg === null || msg === undefined) {
msg = 'action.update.success'; msg = 'action.update.success';
} }
self.isDirty = false;
remoteModal.modal('hide'); remoteModal.modal('hide');
alert.success(msg); alert.success(msg);
} }

View File

@@ -27,3 +27,4 @@
@import 'selectpicker'; @import 'selectpicker';
@import 'reporting'; @import 'reporting';
@import 'forms'; @import 'forms';
@import 'modal';

4
assets/sass/modal.scss Normal file
View File

@@ -0,0 +1,4 @@
.modal-content {
border-radius: 3px;
box-shadow: 0 10px 80px rgba(0, 0, 0, 0.6);
}

View File

@@ -29,6 +29,14 @@ kimai:
# This setting can be changed through the Administration screen # This setting can be changed through the Administration screen
# markdown_content: false # markdown_content: false
# Configures the duration drop-down select.
# null = use rounding rules, 0 = deactivate, every other number is used as minute/step increment
# duration_increment: ~
# Configures the minute select for begin and end date-time.
# null = use rounding rules, every number > 0 is used as minute/step increment
# time_increment: ~
# The time-tracking mode that should be used. # The time-tracking mode that should be used.
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes # See https://www.kimai.org/documentation/timesheet.html#tracking-modes
# mode: default # mode: default

View File

@@ -49,18 +49,6 @@ services:
App\Configuration\LanguageFormattings: App\Configuration\LanguageFormattings:
arguments: ['%kimai.languages%'] arguments: ['%kimai.languages%']
App\Configuration\TimesheetConfiguration:
arguments:
$settings: '%kimai.timesheet%'
App\Configuration\CalendarConfiguration:
arguments:
$settings: '%kimai.calendar%'
App\Configuration\FormConfiguration:
arguments:
$settings: '%kimai.defaults%'
App\Configuration\SystemConfiguration: App\Configuration\SystemConfiguration:
arguments: arguments:
$settings: '%kimai.config%' $settings: '%kimai.config%'

View File

@@ -8,11 +8,6 @@ services:
test.PasswordEncoder: "@security.encoder_factory" test.PasswordEncoder: "@security.encoder_factory"
# added, so we are able to overwrite the service for the CalendarControllerTest
App\Configuration\CalendarConfiguration:
arguments: ['@App\Repository\ConfigurationRepository', "%kimai.calendar%"]
public: true
App\Configuration\SystemConfiguration: App\Configuration\SystemConfiguration:
arguments: ['@App\Repository\ConfigurationRepository', "%kimai.config%"] arguments: ['@App\Repository\ConfigurationRepository', "%kimai.config%"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,10 +5,10 @@
"build/runtime.098eaae1.js", "build/runtime.098eaae1.js",
"build/0.79dbdbb9.js", "build/0.79dbdbb9.js",
"build/1.32489d92.js", "build/1.32489d92.js",
"build/app.14359ac7.js" "build/app.b5e6aadf.js"
], ],
"css": [ "css": [
"build/app.4ac0cd3e.css" "build/app.856a8108.css"
] ]
}, },
"invoice": { "invoice": {
@@ -53,8 +53,8 @@
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd", "build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5", "build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP", "build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
"build/app.14359ac7.js": "sha384-0NC+3yBDQZbvxjD4XvaoY63NbDTbNsuCCkH4wf/mgkcK3w35IZmxZS8h2rWaEaG8", "build/app.b5e6aadf.js": "sha384-ucXoJUHndEr+mEfdM0NmShTqQ8WrbcY64paubzUBfVD3v1Ta2rIgJ4vqQskuA9mg",
"build/app.4ac0cd3e.css": "sha384-+xdvrbdBDKZzKlUgNBm8TzUMuceyw62wIe2IMdm4hy2yYQdBeEZ/RUNf6Z5piG/r", "build/app.856a8108.css": "sha384-LXQ3xtGnzZrgD1R/P0zS2d34VL7+QYCeQvjV0hzFHeoM/IppVbIUAt2FzuORmCuj",
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f", "build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh", "build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
"build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC", "build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",

View File

@@ -2,8 +2,8 @@
"build/0.79dbdbb9.js": "build/0.79dbdbb9.js", "build/0.79dbdbb9.js": "build/0.79dbdbb9.js",
"build/1.32489d92.js": "build/1.32489d92.js", "build/1.32489d92.js": "build/1.32489d92.js",
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js", "build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
"build/app.css": "build/app.4ac0cd3e.css", "build/app.css": "build/app.856a8108.css",
"build/app.js": "build/app.14359ac7.js", "build/app.js": "build/app.b5e6aadf.js",
"build/calendar.css": "build/calendar.1408f57e.css", "build/calendar.css": "build/calendar.1408f57e.css",
"build/calendar.js": "build/calendar.541a15eb.js", "build/calendar.js": "build/calendar.541a15eb.js",
"build/chart.js": "build/chart.34d60a88.js", "build/chart.js": "build/chart.34d60a88.js",

View File

@@ -14,7 +14,7 @@ namespace App\API;
use App\API\Model\I18nConfig; use App\API\Model\I18nConfig;
use App\API\Model\TimesheetConfig; use App\API\Model\TimesheetConfig;
use App\Configuration\LanguageFormattings; use App\Configuration\LanguageFormattings;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\User; use App\Entity\User;
use FOS\RestBundle\Controller\Annotations as Rest; use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View; use FOS\RestBundle\View\View;
@@ -36,20 +36,10 @@ final class ConfigurationController extends BaseApiController
* @var ViewHandlerInterface * @var ViewHandlerInterface
*/ */
private $viewHandler; 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->viewHandler = $viewHandler;
$this->formats = $formats;
$this->timesheetConfiguration = $timesheetConfiguration;
} }
/** /**
@@ -66,7 +56,7 @@ final class ConfigurationController extends BaseApiController
* @ApiSecurity(name="apiUser") * @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken") * @ApiSecurity(name="apiToken")
*/ */
public function i18nAction(): Response public function i18nAction(LanguageFormattings $formats): Response
{ {
/** @var User $user */ /** @var User $user */
$user = $this->getUser(); $user = $this->getUser();
@@ -74,13 +64,13 @@ final class ConfigurationController extends BaseApiController
$model = new I18nConfig(); $model = new I18nConfig();
$model $model
->setFormDateTime($this->formats->getDateTimeTypeFormat($locale)) ->setFormDateTime($formats->getDateTimeTypeFormat($locale))
->setFormDate($this->formats->getDateTypeFormat($locale)) ->setFormDate($formats->getDateTypeFormat($locale))
->setDateTime($this->formats->getDateTimeFormat($locale)) ->setDateTime($formats->getDateTimeFormat($locale))
->setDate($this->formats->getDateFormat($locale)) ->setDate($formats->getDateFormat($locale))
->setDuration($this->formats->getDurationFormat($locale)) ->setDuration($formats->getDurationFormat($locale))
->setTime($this->formats->getTimeFormat($locale)) ->setTime($formats->getTimeFormat($locale))
->setIs24hours($this->formats->isTwentyFourHours($locale)) ->setIs24hours($formats->isTwentyFourHours($locale))
->setNow($this->getDateTimeFactory()->createDateTime()) ->setNow($this->getDateTimeFactory()->createDateTime())
; ;
@@ -104,16 +94,16 @@ final class ConfigurationController extends BaseApiController
* @ApiSecurity(name="apiUser") * @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken") * @ApiSecurity(name="apiToken")
*/ */
public function timesheetConfigAction(): Response public function timesheetConfigAction(SystemConfiguration $configuration): Response
{ {
$model = new TimesheetConfig(); $model = new TimesheetConfig();
$model $model
->setTrackingMode($this->timesheetConfiguration->getTrackingMode()) ->setTrackingMode($configuration->getTimesheetTrackingMode())
->setDefaultBeginTime($this->timesheetConfiguration->getDefaultBeginTime()) ->setDefaultBeginTime($configuration->getTimesheetDefaultBeginTime())
->setActiveEntriesHardLimit($this->timesheetConfiguration->getActiveEntriesHardLimit()) ->setActiveEntriesHardLimit($configuration->getTimesheetActiveEntriesHardLimit())
->setActiveEntriesSoftLimit($this->timesheetConfiguration->getActiveEntriesSoftLimit()) ->setActiveEntriesSoftLimit($configuration->getTimesheetActiveEntriesSoftLimit())
->setIsAllowFutureTimes($this->timesheetConfiguration->isAllowFutureTimes()) ->setIsAllowFutureTimes($configuration->isTimesheetAllowFutureTimes())
->setIsAllowOverlapping($this->timesheetConfiguration->isAllowOverlappingRecords()) ->setIsAllowOverlapping($configuration->isTimesheetAllowOverlappingRecords())
; ;
$view = new View($model, 200); $view = new View($model, 200);

View File

@@ -225,6 +225,7 @@ class ProjectController extends BaseApiController
$project = $this->projectService->createNewProject(); $project = $this->projectService->createNewProject();
$form = $this->createForm(ProjectApiEditForm::class, $project, [ $form = $this->createForm(ProjectApiEditForm::class, $project, [
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'date_format' => self::DATE_FORMAT, 'date_format' => self::DATE_FORMAT,
'include_budget' => $this->isGranted('budget', $project), 'include_budget' => $this->isGranted('budget', $project),
]); ]);
@@ -290,6 +291,7 @@ class ProjectController extends BaseApiController
$this->dispatcher->dispatch($event); $this->dispatcher->dispatch($event);
$form = $this->createForm(ProjectApiEditForm::class, $project, [ $form = $this->createForm(ProjectApiEditForm::class, $project, [
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'date_format' => self::DATE_FORMAT, 'date_format' => self::DATE_FORMAT,
'include_budget' => $this->isGranted('budget', $project), 'include_budget' => $this->isGranted('budget', $project),
]); ]);

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\API; namespace App\API;
use App\Configuration\TimesheetConfiguration;
use App\Entity\User; use App\Entity\User;
use App\Event\RecentActivityEvent; use App\Event\RecentActivityEvent;
use App\Event\TimesheetMetaDefinitionEvent; use App\Event\TimesheetMetaDefinitionEvent;
@@ -62,10 +61,6 @@ class TimesheetController extends BaseApiController
* @var ViewHandlerInterface * @var ViewHandlerInterface
*/ */
private $viewHandler; private $viewHandler;
/**
* @var TimesheetConfiguration
*/
private $configuration;
/** /**
* @var TagRepository * @var TagRepository
*/ */
@@ -86,24 +81,20 @@ class TimesheetController extends BaseApiController
public function __construct( public function __construct(
ViewHandlerInterface $viewHandler, ViewHandlerInterface $viewHandler,
TimesheetRepository $repository, TimesheetRepository $repository,
TimesheetConfiguration $configuration,
TagRepository $tagRepository, TagRepository $tagRepository,
TrackingModeService $trackingModeService,
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
TimesheetService $service TimesheetService $service
) { ) {
$this->viewHandler = $viewHandler; $this->viewHandler = $viewHandler;
$this->repository = $repository; $this->repository = $repository;
$this->configuration = $configuration;
$this->tagRepository = $tagRepository; $this->tagRepository = $tagRepository;
$this->trackingModeService = $trackingModeService;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->service = $service; $this->service = $service;
} }
protected function getTrackingMode(): TrackingModeInterface protected function getTrackingMode(): TrackingModeInterface
{ {
return $this->trackingModeService->getActiveMode(); return $this->service->getActiveTrackingMode();
} }
/** /**

View File

@@ -14,95 +14,79 @@ namespace App\Configuration;
*/ */
class CalendarConfiguration implements SystemBundleConfiguration 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 public function getPrefix(): string
{ {
return 'calendar'; return 'calendar';
} }
/**
* @return array
*/
public function getBusinessDays(): array public function getBusinessDays(): array
{ {
return (array) $this->find('businessHours.days'); return $this->configuration->getCalendarBusinessDays();
} }
/**
* @return string
*/
public function getBusinessTimeBegin(): string public function getBusinessTimeBegin(): string
{ {
return (string) $this->find('businessHours.begin'); return $this->configuration->getCalendarBusinessTimeBegin();
} }
/**
* @return string
*/
public function getBusinessTimeEnd(): string public function getBusinessTimeEnd(): string
{ {
return (string) $this->find('businessHours.end'); return $this->configuration->getCalendarBusinessTimeEnd();
} }
/**
* @return string
*/
public function getTimeframeBegin(): string public function getTimeframeBegin(): string
{ {
return (string) $this->find('visibleHours.begin'); return $this->configuration->getCalendarTimeframeBegin();
} }
/**
* @return string
*/
public function getTimeframeEnd(): string public function getTimeframeEnd(): string
{ {
return (string) $this->find('visibleHours.end'); return $this->configuration->getCalendarTimeframeEnd();
} }
/**
* @return int
*/
public function getDayLimit(): int public function getDayLimit(): int
{ {
return (int) $this->find('day_limit'); return $this->configuration->getCalendarDayLimit();
} }
/**
* @return bool
*/
public function isShowWeekNumbers(): bool public function isShowWeekNumbers(): bool
{ {
return (bool) $this->find('week_numbers'); return $this->configuration->isCalendarShowWeekNumbers();
} }
/**
* @return bool
*/
public function isShowWeekends(): bool public function isShowWeekends(): bool
{ {
return (bool) $this->find('weekends'); return $this->configuration->isCalendarShowWeekends();
} }
/**
* @return null|string
*/
public function getGoogleApiKey(): ?string public function getGoogleApiKey(): ?string
{ {
return $this->find('google.api_key'); return $this->configuration->getCalendarGoogleApiKey();
} }
/**
* @return null|array
*/
public function getGoogleSources(): ?array public function getGoogleSources(): ?array
{ {
return $this->find('google.sources'); return $this->configuration->getCalendarGoogleSources();
} }
public function getSlotDuration(): string public function getSlotDuration(): string
{ {
return (string) $this->find('slot_duration'); return $this->configuration->getCalendarSlotDuration();
} }
} }

View File

@@ -14,7 +14,21 @@ namespace App\Configuration;
*/ */
class FormConfiguration implements SystemBundleConfiguration 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 public function getPrefix(): string
{ {
@@ -23,36 +37,36 @@ class FormConfiguration implements SystemBundleConfiguration
public function getCustomerDefaultTimezone(): ?string public function getCustomerDefaultTimezone(): ?string
{ {
return $this->find('customer.timezone'); return $this->configuration->getCustomerDefaultTimezone();
} }
public function getCustomerDefaultCurrency(): string public function getCustomerDefaultCurrency(): string
{ {
return $this->find('customer.currency'); return $this->configuration->getCustomerDefaultCurrency();
} }
public function getCustomerDefaultCountry(): string public function getCustomerDefaultCountry(): string
{ {
return $this->find('customer.country'); return $this->configuration->getCustomerDefaultCountry();
} }
public function getUserDefaultTimezone(): ?string public function getUserDefaultTimezone(): ?string
{ {
return $this->find('user.timezone'); return $this->configuration->getUserDefaultTimezone();
} }
public function getUserDefaultTheme(): ?string public function getUserDefaultTheme(): ?string
{ {
return $this->find('user.theme'); return $this->configuration->getUserDefaultTheme();
} }
public function getUserDefaultLanguage(): string public function getUserDefaultLanguage(): string
{ {
return $this->find('user.language'); return $this->configuration->getUserDefaultLanguage();
} }
public function getUserDefaultCurrency(): string public function getUserDefaultCurrency(): string
{ {
return $this->find('user.currency'); return $this->configuration->getUserDefaultCurrency();
} }
} }

View File

@@ -23,10 +23,7 @@ class SystemConfiguration implements SystemBundleConfiguration
return $repository->getConfiguration(); return $repository->getConfiguration();
} }
public function getTimesheetDefaultBeginTime(): string // ========== Calendar configurations ==========
{
return (string) $this->find('timesheet.default_begin');
}
public function getCalendarBusinessDays(): array public function getCalendarBusinessDays(): array
{ {
@@ -83,6 +80,8 @@ class SystemConfiguration implements SystemBundleConfiguration
return (string) $this->find('calendar.slot_duration'); return (string) $this->find('calendar.slot_duration');
} }
// ========== Customer configurations ==========
public function getCustomerDefaultTimezone(): ?string public function getCustomerDefaultTimezone(): ?string
{ {
return $this->find('defaults.customer.timezone'); return $this->find('defaults.customer.timezone');
@@ -98,6 +97,8 @@ class SystemConfiguration implements SystemBundleConfiguration
return $this->find('defaults.customer.country'); return $this->find('defaults.customer.country');
} }
// ========== User configurations ==========
public function getUserDefaultTimezone(): ?string public function getUserDefaultTimezone(): ?string
{ {
return $this->find('defaults.user.timezone'); return $this->find('defaults.user.timezone');
@@ -117,4 +118,114 @@ class SystemConfiguration implements SystemBundleConfiguration
{ {
return $this->find('defaults.user.currency'); 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);
}
} }

View File

@@ -10,11 +10,25 @@
namespace App\Configuration; namespace App\Configuration;
/** /**
* @internal will be deprecated soon, use SystemConfiguration instead * @deprecated since 1.13, use SystemConfiguration instead
*/ */
class TimesheetConfiguration implements SystemBundleConfiguration 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 public function getPrefix(): string
{ {
@@ -23,81 +37,81 @@ class TimesheetConfiguration implements SystemBundleConfiguration
public function isAllowFutureTimes(): bool public function isAllowFutureTimes(): bool
{ {
return (bool) $this->find('rules.allow_future_times'); return $this->configuration->isTimesheetAllowFutureTimes();
} }
public function isAllowOverlappingRecords(): bool public function isAllowOverlappingRecords(): bool
{ {
return (bool) $this->find('rules.allow_overlapping_records'); return $this->configuration->isTimesheetAllowOverlappingRecords();
} }
public function getTrackingMode(): string public function getTrackingMode(): string
{ {
return (string) $this->find('mode'); return $this->configuration->getTimesheetTrackingMode();
} }
public function getDefaultBeginTime(): string public function getDefaultBeginTime(): string
{ {
return (string) $this->find('default_begin'); return $this->configuration->getTimesheetDefaultBeginTime();
} }
public function isMarkdownEnabled(): bool public function isMarkdownEnabled(): bool
{ {
return (bool) $this->find('markdown_content'); return $this->configuration->isTimesheetMarkdownEnabled();
} }
public function getActiveEntriesHardLimit(): int public function getActiveEntriesHardLimit(): int
{ {
return (int) $this->find('active_entries.hard_limit'); return $this->configuration->getTimesheetActiveEntriesHardLimit();
} }
public function getActiveEntriesSoftLimit(): int public function getActiveEntriesSoftLimit(): int
{ {
return (int) $this->find('active_entries.soft_limit'); return $this->configuration->getTimesheetActiveEntriesSoftLimit();
} }
public function getDefaultRoundingDays(): string public function getDefaultRoundingDays(): string
{ {
return (string) $this->find('rounding.default.days'); return $this->configuration->getTimesheetDefaultRoundingDays();
} }
public function getDefaultRoundingMode(): string public function getDefaultRoundingMode(): string
{ {
return (string) $this->find('rounding.default.mode'); return $this->configuration->getTimesheetDefaultRoundingMode();
} }
public function getDefaultRoundingBegin(): int public function getDefaultRoundingBegin(): int
{ {
return (int) $this->find('rounding.default.begin'); return $this->configuration->getTimesheetDefaultRoundingBegin();
} }
public function getDefaultRoundingEnd(): int public function getDefaultRoundingEnd(): int
{ {
return (int) $this->find('rounding.default.end'); return $this->configuration->getTimesheetDefaultRoundingEnd();
} }
public function getDefaultRoundingDuration(): int public function getDefaultRoundingDuration(): int
{ {
return (int) $this->find('rounding.default.duration'); return $this->configuration->getTimesheetDefaultRoundingDuration();
} }
public function getLockdownPeriodStart(): string public function getLockdownPeriodStart(): string
{ {
return (string) $this->find('rules.lockdown_period_start'); return $this->configuration->getTimesheetLockdownPeriodStart();
} }
public function getLockdownPeriodEnd(): string public function getLockdownPeriodEnd(): string
{ {
return (string) $this->find('rules.lockdown_period_end'); return $this->configuration->getTimesheetLockdownPeriodEnd();
} }
public function getLockdownGracePeriod(): string public function getLockdownGracePeriod(): string
{ {
return (string) $this->find('rules.lockdown_grace_period'); return $this->configuration->getTimesheetLockdownGracePeriod();
} }
public function isLockdownActive(): bool public function isLockdownActive(): bool
{ {
return !empty($this->find('rules.lockdown_period_start')) && !empty($this->find('rules.lockdown_period_end')); return $this->configuration->isTimesheetLockdownActive();
} }
} }

View File

@@ -143,6 +143,7 @@ class ExportController extends AbstractController
'action' => $this->generateUrl('export', []), 'action' => $this->generateUrl('export', []),
'include_user' => $this->isGranted('view_other_timesheet'), 'include_user' => $this->isGranted('view_other_timesheet'),
'method' => $method, 'method' => $method,
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'attr' => [ 'attr' => [
'id' => 'export-form' 'id' => 'export-form'
] ]

View File

@@ -408,6 +408,7 @@ final class InvoiceController extends AbstractController
'action' => $this->generateUrl('invoice', []), 'action' => $this->generateUrl('invoice', []),
'method' => 'GET', 'method' => 'GET',
'include_user' => $this->isGranted('view_other_timesheet'), 'include_user' => $this->isGranted('view_other_timesheet'),
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'attr' => [ 'attr' => [
'id' => 'invoice-print-form' 'id' => 'invoice-print-form'
], ],

View File

@@ -9,7 +9,7 @@
namespace App\Controller; namespace App\Controller;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Event\RecentActivityEvent; use App\Event\RecentActivityEvent;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -20,7 +20,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
*/ */
class LayoutController extends AbstractController 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(); $user = $this->getUser();
$activeEntries = $repository->getActiveEntries($user); $activeEntries = $repository->getActiveEntries($user);
@@ -32,7 +32,7 @@ class LayoutController extends AbstractController
'navbar/active-entries.html.twig', 'navbar/active-entries.html.twig',
[ [
'entries' => $recentActivity->getRecentActivities(), 'entries' => $recentActivity->getRecentActivities(),
'soft_limit' => $configuration->getActiveEntriesSoftLimit(), 'soft_limit' => $configuration->getTimesheetActiveEntriesSoftLimit(),
] ]
); );
} }

View File

@@ -523,7 +523,9 @@ final class ProjectController extends AbstractController
'action' => $url, 'action' => $url,
'method' => 'POST', 'method' => 'POST',
'currency' => $currency, 'currency' => $currency,
'include_budget' => $this->isGranted('budget', $project) 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'include_budget' => $this->isGranted('budget', $project),
'time_increment' => 15,
]); ]);
} }
} }

View File

@@ -17,6 +17,7 @@ use App\Form\SystemConfigurationForm;
use App\Form\Type\DateTimeTextType; use App\Form\Type\DateTimeTextType;
use App\Form\Type\DayTimeType; use App\Form\Type\DayTimeType;
use App\Form\Type\LanguageType; use App\Form\Type\LanguageType;
use App\Form\Type\MinuteIncrementType;
use App\Form\Type\RoundingModeType; use App\Form\Type\RoundingModeType;
use App\Form\Type\SkinType; use App\Form\Type\SkinType;
use App\Form\Type\TrackingModeType; use App\Form\Type\TrackingModeType;
@@ -275,6 +276,21 @@ final class SystemConfigurationController extends AbstractController
->setConstraints([ ->setConstraints([
new GreaterThanOrEqual(['value' => 1]) 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()) (new SystemConfigurationModel())
->setSection(SystemConfigurationModel::SECTION_ROUNDING) ->setSection(SystemConfigurationModel::SECTION_ROUNDING)

View File

@@ -9,6 +9,7 @@
namespace App\Controller; namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Entity\MetaTableTypeInterface; use App\Entity\MetaTableTypeInterface;
use App\Entity\Tag; use App\Entity\Tag;
use App\Entity\Timesheet; use App\Entity\Timesheet;
@@ -28,7 +29,6 @@ use App\Repository\TagRepository;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Timesheet\TimesheetService; use App\Timesheet\TimesheetService;
use App\Timesheet\TrackingMode\TrackingModeInterface; use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Timesheet\TrackingModeService;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -41,10 +41,6 @@ abstract class TimesheetAbstractController extends AbstractController
* @var TimesheetRepository * @var TimesheetRepository
*/ */
protected $repository; protected $repository;
/**
* @var TrackingModeService
*/
protected $trackingModeService;
/** /**
* @var EventDispatcherInterface * @var EventDispatcherInterface
*/ */
@@ -57,24 +53,28 @@ abstract class TimesheetAbstractController extends AbstractController
* @var TimesheetService * @var TimesheetService
*/ */
protected $service; protected $service;
/**
* @var SystemConfiguration
*/
protected $configuration;
public function __construct( public function __construct(
TimesheetRepository $repository, TimesheetRepository $repository,
TrackingModeService $trackingModeService,
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
ServiceExport $exportService, ServiceExport $exportService,
TimesheetService $timesheetService TimesheetService $timesheetService,
SystemConfiguration $configuration
) { ) {
$this->repository = $repository; $this->repository = $repository;
$this->trackingModeService = $trackingModeService;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->exportService = $exportService; $this->exportService = $exportService;
$this->service = $timesheetService; $this->service = $timesheetService;
$this->configuration = $configuration;
} }
protected function getTrackingMode(): TrackingModeInterface protected function getTrackingMode(): TrackingModeInterface
{ {
return $this->trackingModeService->getActiveMode(); return $this->service->getActiveTrackingMode();
} }
protected function index($page, Request $request, string $renderTemplate, string $location): Response 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); $this->service->prepareNewTimesheet($entry, $request);
$createForm = $this->getCreateForm($entry);
$mode = $this->getTrackingMode();
$createForm = $this->getCreateForm($entry, $mode);
$createForm->handleRequest($request); $createForm->handleRequest($request);
if ($createForm->isSubmitted() && $createForm->isValid()) { 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, [ return $this->createForm($this->getCreateFormClassName(), $entry, [
'action' => $this->generateUrl($this->getCreateRoute()), 'action' => $this->generateUrl($this->getCreateRoute()),
'include_rate' => $this->isGranted('edit_rate', $entry), 'include_rate' => $this->isGranted('edit_rate', $entry),
@@ -450,6 +450,10 @@ abstract class TimesheetAbstractController extends AbstractController
'allow_begin_datetime' => $mode->canEditBegin(), 'allow_begin_datetime' => $mode->canEditBegin(),
'allow_end_datetime' => $mode->canEditEnd(), 'allow_end_datetime' => $mode->canEditEnd(),
'allow_duration' => $mode->canEditDuration(), '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, 'customer' => true,
]); ]);
} }
@@ -474,6 +478,10 @@ abstract class TimesheetAbstractController extends AbstractController
'allow_begin_datetime' => $mode->canEditBegin(), 'allow_begin_datetime' => $mode->canEditBegin(),
'allow_end_datetime' => $mode->canEditEnd(), 'allow_end_datetime' => $mode->canEditEnd(),
'allow_duration' => $mode->canEditDuration(), '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, 'customer' => true,
]); ]);
} }
@@ -488,6 +496,7 @@ abstract class TimesheetAbstractController extends AbstractController
'action' => $this->generateUrl($this->getTimesheetRoute(), [ 'action' => $this->generateUrl($this->getTimesheetRoute(), [
'page' => $query->getPage(), 'page' => $query->getPage(),
]), ]),
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'method' => 'GET', 'method' => 'GET',
'include_user' => $this->includeUserInForms('toolbar'), 'include_user' => $this->includeUserInForms('toolbar'),
]); ]);

View File

@@ -20,7 +20,6 @@ use App\Repository\ActivityRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\TimesheetQuery; use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository; use App\Repository\TagRepository;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -101,8 +100,7 @@ class TimesheetTeamController extends TimesheetAbstractController
$entry->setUser($this->getUser()); $entry->setUser($this->getUser());
$this->service->prepareNewTimesheet($entry, $request); $this->service->prepareNewTimesheet($entry, $request);
$mode = $this->getTrackingMode(); $createForm = $this->getMultiUserCreateForm($entry);
$createForm = $this->getMultiUserCreateForm($entry, $mode);
$createForm->handleRequest($request); $createForm->handleRequest($request);
if ($createForm->isSubmitted() && $createForm->isValid()) { 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, [ return $this->createForm(TimesheetMultiUserEditForm::class, $entry, [
'action' => $this->generateUrl('admin_timesheet_create_multiuser'), 'action' => $this->generateUrl('admin_timesheet_create_multiuser'),
'include_rate' => $this->isGranted('edit_rate', $entry), 'include_rate' => $this->isGranted('edit_rate', $entry),
@@ -160,6 +160,10 @@ class TimesheetTeamController extends TimesheetAbstractController
'allow_begin_datetime' => $mode->canEditBegin(), 'allow_begin_datetime' => $mode->canEditBegin(),
'allow_end_datetime' => $mode->canEditEnd(), 'allow_end_datetime' => $mode->canEditEnd(),
'allow_duration' => $mode->canEditDuration(), '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, 'customer' => true,
]); ]);
} }

View File

@@ -61,19 +61,19 @@ class AppExtension extends Extension
$this->setLanguageFormats($config['languages'], $container); $this->setLanguageFormats($config['languages'], $container);
unset($config['languages']); 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.dashboard', $config['dashboard']);
$container->setParameter('kimai.widgets', $config['widgets']); $container->setParameter('kimai.widgets', $config['widgets']);
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']); $container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
$container->setParameter('kimai.export.documents', $config['export']['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->createPermissionParameter($config['permissions'], $container);
$this->createThemeParameter($config['theme'], $container); $this->createThemeParameter($config['theme'], $container);
$this->createUserParameter($config['user'], $container); $this->createUserParameter($config['user'], $container);
$container->setParameter('kimai.saml', $config['saml']); $container->setParameter('kimai.saml', $config['saml']);
$container->setParameter('kimai.saml.connection', $config['saml']['connection']); $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.rates', $config['timesheet']['rates']);
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']); $container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);

View File

@@ -94,6 +94,32 @@ class Configuration implements ConfigurationInterface
->booleanNode('markdown_content') ->booleanNode('markdown_content')
->defaultValue(false) ->defaultValue(false)
->end() ->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') ->arrayNode('rounding')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->useAttributeAsKey('key') ->useAttributeAsKey('key')

View File

@@ -48,6 +48,7 @@ final class ThemeJavascriptTranslationsEvent extends Event
'confirm.delete' => ['confirm.delete', 'messages'], 'confirm.delete' => ['confirm.delete', 'messages'],
'delete' => ['action.delete', 'messages'], 'delete' => ['action.delete', 'messages'],
'login.required' => ['login_required', 'messages'], 'login.required' => ['login_required', 'messages'],
'modal.dirty' => ['modal.dirty', 'messages']
]; ];
public function getTranslations(): array public function getTranslations(): array

View File

@@ -14,6 +14,7 @@ use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Form\Type\ActivityType; use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType; use App\Form\Type\CustomerType;
use App\Form\Type\DescriptionType;
use App\Form\Type\ProjectType; use App\Form\Type\ProjectType;
use App\Form\Type\TagsType; use App\Form\Type\TagsType;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
@@ -22,7 +23,6 @@ use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery; use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\CustomerFormTypeQuery; use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery; use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
@@ -136,11 +136,15 @@ trait FormTrait
); );
} }
/**
* @deprecated since 1.13
*/
protected function addDescription(FormBuilderInterface $builder) 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 $builder
->add('description', TextareaType::class, [ ->add('description', DescriptionType::class, [
'label' => 'label.description',
'required' => false, 'required' => false,
'attr' => [ 'attr' => [
'autofocus' => 'autofocus' 'autofocus' => 'autofocus'

View File

@@ -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 // primarily for API usage, where we cannot use a user/locale specific format
if (null !== $options['date_format']) { if (null !== $options['date_format']) {
$dateTimeOptions['format'] = $options['date_format']; $dateTimeOptions['format'] = $options['date_format'];
} }
$timeIncrement = 1;
if ($options['time_increment'] >= 1 && $options['time_increment'] <= 60) {
$timeIncrement = $options['time_increment'];
}
$builder $builder
->add('name', TextType::class, [ ->add('name', TextType::class, [
'label' => 'label.name', 'label' => 'label.name',
@@ -68,14 +76,17 @@ class ProjectEditForm extends AbstractType
->add('orderDate', DateTimePickerType::class, array_merge($dateTimeOptions, [ ->add('orderDate', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.orderDate', 'label' => 'label.orderDate',
'required' => false, 'required' => false,
'time_increment' => $timeIncrement,
])) ]))
->add('start', DateTimePickerType::class, array_merge($dateTimeOptions, [ ->add('start', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.project_start', 'label' => 'label.project_start',
'required' => false, 'required' => false,
'time_increment' => $timeIncrement,
])) ]))
->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [ ->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.project_end', 'label' => 'label.project_end',
'required' => false, 'required' => false,
'time_increment' => $timeIncrement,
])) ]))
->add('customer', CustomerType::class, [ ->add('customer', CustomerType::class, [
'placeholder' => (null === $id && null === $customer) ? '' : false, 'placeholder' => (null === $id && null === $customer) ? '' : false,
@@ -103,6 +114,8 @@ class ProjectEditForm extends AbstractType
'currency' => Customer::DEFAULT_CURRENCY, 'currency' => Customer::DEFAULT_CURRENCY,
'date_format' => null, 'date_format' => null,
'include_budget' => false, 'include_budget' => false,
'timezone' => date_default_timezone_get(),
'time_increment' => 1,
'attr' => [ 'attr' => [
'data-form-event' => 'kimai.projectUpdate' 'data-form-event' => 'kimai.projectUpdate'
], ],

View File

@@ -17,7 +17,7 @@ class TimesheetAdminEditForm extends TimesheetEditForm
{ {
$options['allow_begin_datetime'] = true; $options['allow_begin_datetime'] = true;
$options['allow_end_datetime'] = true; $options['allow_end_datetime'] = true;
$options['allow_duration'] = false; $options['allow_duration'] = true;
parent::buildForm($builder, $options); parent::buildForm($builder, $options);
} }

View File

@@ -11,6 +11,7 @@ namespace App\Form;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Form\Type\DateTimePickerType; use App\Form\Type\DateTimePickerType;
use App\Form\Type\DescriptionType;
use App\Form\Type\DurationType; use App\Form\Type\DurationType;
use App\Form\Type\FixedRateType; use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType; use App\Form\Type\HourlyRateType;
@@ -19,7 +20,6 @@ use App\Form\Type\UserType;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Timesheet\UserDateTimeFactory;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
@@ -41,21 +41,11 @@ class TimesheetEditForm extends AbstractType
* @var ProjectRepository * @var ProjectRepository
*/ */
private $projects; private $projects;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/** public function __construct(CustomerRepository $customer, ProjectRepository $project)
* @param CustomerRepository $customer
* @param ProjectRepository $project
* @param UserDateTimeFactory $dateTime
*/
public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime)
{ {
$this->customers = $customer; $this->customers = $customer;
$this->projects = $project; $this->projects = $project;
$this->dateTime = $dateTime;
} }
/** /**
@@ -69,7 +59,7 @@ class TimesheetEditForm extends AbstractType
$currency = false; $currency = false;
$begin = null; $begin = null;
$customerCount = $this->customers->countCustomer(true); $customerCount = $this->customers->countCustomer(true);
$timezone = $this->dateTime->getTimezone()->getName(); $timezone = $options['timezone'];
$isNew = true; $isNew = true;
if (isset($options['data'])) { if (isset($options['data'])) {
@@ -108,23 +98,15 @@ class TimesheetEditForm extends AbstractType
} }
if ($options['allow_begin_datetime']) { 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']) { if ($options['allow_duration']) {
$this->addDuration($builder); $this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
} 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}'
]
]);
} }
if ($this->showCustomer($options, $isNew, $customerCount)) { if ($this->showCustomer($options, $isNew, $customerCount)) {
@@ -133,7 +115,13 @@ class TimesheetEditForm extends AbstractType
$this->addProject($builder, $isNew, $project, $customer); $this->addProject($builder, $isNew, $project, $customer);
$this->addActivity($builder, $activity, $project); $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->addTags($builder);
$this->addRates($builder, $currency, $options); $this->addRates($builder, $currency, $options);
$this->addUser($builder, $options); $this->addUser($builder, $options);
@@ -159,30 +147,58 @@ class TimesheetEditForm extends AbstractType
return true; 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, [ $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, [ $builder->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.end', 'label' => 'label.end',
'required' => false, '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, 'required' => false,
'docu_chapter' => 'timesheet.html#duration-format', 'docu_chapter' => 'timesheet.html#duration-format',
'attr' => [ '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( $builder->addEventListener(
FormEvents::POST_SET_DATA, FormEvents::POST_SET_DATA,
@@ -198,17 +214,18 @@ class TimesheetEditForm extends AbstractType
// make sure that duration is mapped back to end field // make sure that duration is mapped back to end field
$builder->addEventListener( $builder->addEventListener(
FormEvents::SUBMIT, FormEvents::SUBMIT,
function (FormEvent $event) { function (FormEvent $event) use ($forceApply) {
/** @var Timesheet $data */ /** @var Timesheet $data */
$data = $event->getData(); $data = $event->getData();
$duration = $data->getDuration(); $duration = $data->getDuration();
$end = null; // only apply the duration, if the end is not yet set
if (null !== $duration) { // 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 = clone $data->getBegin();
$end->modify('+ ' . $duration . 'seconds'); $end->modify('+ ' . $duration . 'seconds');
}
$data->setEnd($end); $data->setEnd($end);
} }
}
); );
} }
@@ -263,10 +280,15 @@ class TimesheetEditForm extends AbstractType
'docu_chapter' => 'timesheet.html', 'docu_chapter' => 'timesheet.html',
'method' => 'POST', 'method' => 'POST',
'date_format' => null, 'date_format' => null,
'timezone' => date_default_timezone_get(),
'customer' => false, // for API usage 'customer' => false, // for API usage
'allow_begin_datetime' => true, 'allow_begin_datetime' => true,
'allow_end_datetime' => true, 'allow_end_datetime' => true,
'allow_duration' => false, 'allow_duration' => false,
'duration_minutes' => null,
'duration_hours' => 10,
'begin_minutes' => 1,
'end_minutes' => 1,
'attr' => [ 'attr' => [
'data-form-event' => 'kimai.timesheetUpdate', 'data-form-event' => 'kimai.timesheetUpdate',
'data-msg-success' => 'action.update.success', 'data-msg-success' => 'action.update.success',

View File

@@ -20,7 +20,6 @@ class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
{ {
$options['allow_begin_datetime'] = true; $options['allow_begin_datetime'] = true;
$options['allow_end_datetime'] = true; $options['allow_end_datetime'] = true;
$options['allow_duration'] = false;
$options['include_user'] = false; $options['include_user'] = false;
parent::buildForm($builder, $options); parent::buildForm($builder, $options);

View File

@@ -32,7 +32,7 @@ class ExportToolbarForm extends AbstractToolbarForm
if ($options['include_user']) { if ($options['include_user']) {
$this->addUsersChoice($builder); $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->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true); $this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivityMultiChoice($builder, [], true); $this->addActivityMultiChoice($builder, [], true);
@@ -64,6 +64,7 @@ class ExportToolbarForm extends AbstractToolbarForm
'data_class' => ExportQuery::class, 'data_class' => ExportQuery::class,
'csrf_protection' => false, 'csrf_protection' => false,
'include_user' => true, 'include_user' => true,
'timezone' => date_default_timezone_get(),
]); ]);
} }
} }

View File

@@ -27,7 +27,7 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$this->addTemplateChoice($builder); $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->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); $this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
$builder->add('markAsExported', CheckboxType::class, [ $builder->add('markAsExported', CheckboxType::class, [
@@ -63,6 +63,7 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
'data_class' => InvoiceQuery::class, 'data_class' => InvoiceQuery::class,
'csrf_protection' => false, 'csrf_protection' => false,
'include_user' => true, 'include_user' => true,
'timezone' => date_default_timezone_get(),
]); ]);
} }
} }

View File

@@ -29,7 +29,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
} }
$this->addSearchTermInputField($builder); $this->addSearchTermInputField($builder);
$this->addDateRangeChoice($builder); $this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, $newOptions, true); $this->addCustomerMultiChoice($builder, $newOptions, true);
$this->addProjectMultiChoice($builder, $newOptions, true, true); $this->addProjectMultiChoice($builder, $newOptions, true, true);
$this->addActivityMultiChoice($builder, [], true); $this->addActivityMultiChoice($builder, [], true);
@@ -55,6 +55,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
'csrf_protection' => false, 'csrf_protection' => false,
'include_user' => false, 'include_user' => false,
'ignore_date' => true, 'ignore_date' => true,
'timezone' => date_default_timezone_get(),
]); ]);
} }
} }

View File

@@ -9,7 +9,6 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings; use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\DateType;
@@ -23,12 +22,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class DatePickerType extends AbstractType class DatePickerType extends AbstractType
{ {
private $localeSettings; private $localeSettings;
private $dateTime;
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime) public function __construct(LocaleSettings $localeSettings)
{ {
$this->localeSettings = $localeSettings; $this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
} }
/** /**
@@ -38,15 +35,14 @@ class DatePickerType extends AbstractType
{ {
$pickerFormat = $this->localeSettings->getDatePickerFormat(); $pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat(); $dateFormat = $this->localeSettings->getDateTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([ $resolver->setDefaults([
'widget' => 'single_text', 'widget' => 'single_text',
'html5' => false, 'html5' => false,
'format' => $dateFormat, 'format' => $dateFormat,
'format_picker' => $pickerFormat, 'format_picker' => $pickerFormat,
'model_timezone' => $timezone, 'model_timezone' => date_default_timezone_get(),
'view_timezone' => $timezone, 'view_timezone' => date_default_timezone_get(),
]); ]);
} }

View File

@@ -10,7 +10,6 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\Form\Model\DateRange; use App\Form\Model\DateRange;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings; use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\CallbackTransformer;
@@ -28,19 +27,11 @@ class DateRangeType extends AbstractType
{ {
public const DATE_SPACER = ' - '; public const DATE_SPACER = ' - ';
/**
* @var LocaleSettings
*/
private $localeSettings; private $localeSettings;
/**
* @var UserDateTimeFactory
*/
private $dateFactory;
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime) public function __construct(LocaleSettings $localeSettings)
{ {
$this->localeSettings = $localeSettings; $this->localeSettings = $localeSettings;
$this->dateFactory = $dateTime;
} }
/** /**
@@ -126,8 +117,7 @@ class DateRangeType extends AbstractType
$formatDate = $options['format']; $formatDate = $options['format'];
$separator = $options['separator']; $separator = $options['separator'];
$allowEmpty = $options['allow_empty']; $allowEmpty = $options['allow_empty'];
//$timezone = new \DateTimeZone($options['timezone']); $timezone = new \DateTimeZone($options['timezone']);
$timezone = $this->dateFactory->getTimezone();
$pattern = $this->formatToPattern($formatDate, $separator); $pattern = $this->formatToPattern($formatDate, $separator);
$builder->addModelTransformer(new CallbackTransformer( $builder->addModelTransformer(new CallbackTransformer(

View File

@@ -10,7 +10,6 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\API\BaseApiController; use App\API\BaseApiController;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings; use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
@@ -23,20 +22,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/ */
class DateTimePickerType extends AbstractType class DateTimePickerType extends AbstractType
{ {
/** private $localeSettings;
* @var LocaleSettings
*/
protected $localeSettings;
/** public function __construct(LocaleSettings $localeSettings)
* @var UserDateTimeFactory
*/
protected $dateTime;
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
{ {
$this->localeSettings = $localeSettings; $this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
} }
/** /**
@@ -46,7 +36,6 @@ class DateTimePickerType extends AbstractType
{ {
$dateTimePicker = $this->localeSettings->getDateTimePickerFormat(); $dateTimePicker = $this->localeSettings->getDateTimePickerFormat();
$dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat(); $dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([ $resolver->setDefaults([
'documentation' => [ 'documentation' => [
@@ -60,8 +49,7 @@ class DateTimePickerType extends AbstractType
'format' => $dateTimeFormat, 'format' => $dateTimeFormat,
'format_picker' => $dateTimePicker, 'format_picker' => $dateTimePicker,
'with_seconds' => false, 'with_seconds' => false,
'model_timezone' => $timezone, 'time_increment' => 1,
'view_timezone' => $timezone,
]); ]);
} }
@@ -72,6 +60,7 @@ class DateTimePickerType extends AbstractType
'autocomplete' => 'off', 'autocomplete' => 'off',
'placeholder' => strtoupper($options['format']), 'placeholder' => strtoupper($options['format']),
'data-format' => $options['format_picker'], 'data-format' => $options['format_picker'],
'data-time-picker-increment' => $options['time_increment'],
]); ]);
} }

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\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;
}
}

View File

@@ -14,6 +14,8 @@ use App\Validator\Constraints\Duration as DurationConstraint;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
/** /**
@@ -29,9 +31,37 @@ class DurationType extends AbstractType
$resolver->setDefaults([ $resolver->setDefaults([
'label' => 'label.duration', 'label' => 'label.duration',
'constraints' => [new DurationConstraint()], '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} * {@inheritdoc}
*/ */

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

View File

@@ -9,7 +9,7 @@
namespace App\Timesheet; namespace App\Timesheet;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\Rounding\RoundingInterface; use App\Timesheet\Rounding\RoundingInterface;
@@ -24,7 +24,7 @@ final class RoundingService
*/ */
private $rulesCache; private $rulesCache;
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
/** /**
@@ -33,11 +33,11 @@ final class RoundingService
private $roundingModes; private $roundingModes;
/** /**
* @param TimesheetConfiguration $configuration * @param SystemConfiguration $configuration
* @param RoundingInterface[] $roundingModes * @param RoundingInterface[] $roundingModes
* @param array $rules * @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->configuration = $configuration;
$this->roundingModes = $roundingModes; $this->roundingModes = $roundingModes;
@@ -49,11 +49,11 @@ final class RoundingService
if (empty($this->rulesCache)) { if (empty($this->rulesCache)) {
$this->rulesCache = $this->rules; $this->rulesCache = $this->rules;
if (empty($this->rulesCache) || \array_key_exists('default', $this->rulesCache)) { if (empty($this->rulesCache) || \array_key_exists('default', $this->rulesCache)) {
$this->rulesCache['default']['days'] = $this->configuration->getDefaultRoundingDays(); $this->rulesCache['default']['days'] = $this->configuration->getTimesheetDefaultRoundingDays();
$this->rulesCache['default']['begin'] = $this->configuration->getDefaultRoundingBegin(); $this->rulesCache['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin();
$this->rulesCache['default']['end'] = $this->configuration->getDefaultRoundingEnd(); $this->rulesCache['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd();
$this->rulesCache['default']['duration'] = $this->configuration->getDefaultRoundingDuration(); $this->rulesCache['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration();
$this->rulesCache['default']['mode'] = $this->configuration->getDefaultRoundingMode(); $this->rulesCache['default']['mode'] = $this->configuration->getTimesheetDefaultRoundingMode();
} }
// see AppExtension, conversion from string to array due to system configuration ont allowing to store arrays // see AppExtension, conversion from string to array due to system configuration ont allowing to store arrays

View File

@@ -9,7 +9,7 @@
namespace App\Timesheet; namespace App\Timesheet;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Entity\User; use App\Entity\User;
use App\Event\TimesheetCreatePostEvent; use App\Event\TimesheetCreatePostEvent;
@@ -27,6 +27,7 @@ use App\Event\TimesheetUpdatePostEvent;
use App\Event\TimesheetUpdatePreEvent; use App\Event\TimesheetUpdatePreEvent;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Security\AccessDeniedException; use App\Security\AccessDeniedException;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Validator\ValidationException; use App\Validator\ValidationException;
use App\Validator\ValidationFailedException; use App\Validator\ValidationFailedException;
use InvalidArgumentException; use InvalidArgumentException;
@@ -42,7 +43,7 @@ final class TimesheetService
*/ */
private $repository; private $repository;
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
/** /**
@@ -63,7 +64,7 @@ final class TimesheetService
private $validator; private $validator;
public function __construct( public function __construct(
TimesheetConfiguration $configuration, SystemConfiguration $configuration,
TimesheetRepository $repository, TimesheetRepository $repository,
TrackingModeService $service, TrackingModeService $service,
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
@@ -293,7 +294,7 @@ final class TimesheetService
*/ */
private function stopActiveEntries(Timesheet $timesheet): int private function stopActiveEntries(Timesheet $timesheet): int
{ {
$hardLimit = $this->configuration->getActiveEntriesHardLimit(); $hardLimit = $this->configuration->getTimesheetActiveEntriesHardLimit();
$activeEntries = $this->repository->getActiveEntries($timesheet->getUser()); $activeEntries = $this->repository->getActiveEntries($timesheet->getUser());
if (empty($activeEntries)) { if (empty($activeEntries)) {
@@ -314,4 +315,9 @@ final class TimesheetService
return $counter; return $counter;
} }
public function getActiveTrackingMode(): TrackingModeInterface
{
return $this->trackingModeService->getActiveMode();
}
} }

View File

@@ -9,27 +9,13 @@
namespace App\Timesheet\TrackingMode; namespace App\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\UserDateTimeFactory; use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
abstract class AbstractTrackingMode implements TrackingModeInterface abstract class AbstractTrackingMode implements TrackingModeInterface
{ {
/** use TrackingModeTrait;
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @var TimesheetConfiguration
*/
protected $configuration;
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
{
$this->dateTime = $dateTime;
$this->configuration = $configuration;
}
public function create(Timesheet $timesheet, ?Request $request = null): void public function create(Timesheet $timesheet, ?Request $request = null): void
{ {
@@ -48,7 +34,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
return; return;
} }
$start = $this->dateTime->createDateTimeFromFormat('Y-m-d', $start); $start = DateTime::createFromFormat('Y-m-d', $start, $this->getTimezone($entry));
if (false === $start) { if (false === $start) {
return; return;
} }
@@ -61,7 +47,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
return; return;
} }
$end = $this->dateTime->createDateTimeFromFormat('Y-m-d', $end); $end = DateTime::createFromFormat('Y-m-d', $end, $this->getTimezone($entry));
if (false === $end) { if (false === $end) {
return; return;
} }
@@ -81,7 +67,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
} }
try { try {
$from = $this->dateTime->createDateTime($from); $from = new DateTime($from, $this->getTimezone($entry));
} catch (\Exception $ex) { } catch (\Exception $ex) {
return; return;
} }
@@ -94,7 +80,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
} }
try { try {
$to = $this->dateTime->createDateTime($to); $to = new DateTime($to, $this->getTimezone($entry));
} catch (\Exception $ex) { } catch (\Exception $ex) {
return; return;
} }

View File

@@ -9,10 +9,9 @@
namespace App\Timesheet\TrackingMode; namespace App\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\RoundingService; use App\Timesheet\RoundingService;
use App\Timesheet\UserDateTimeFactory; use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
final class DefaultMode extends AbstractTrackingMode final class DefaultMode extends AbstractTrackingMode
@@ -22,9 +21,8 @@ final class DefaultMode extends AbstractTrackingMode
*/ */
private $rounding; private $rounding;
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, RoundingService $rounding) public function __construct(RoundingService $rounding)
{ {
parent::__construct($dateTime, $configuration);
$this->rounding = $rounding; $this->rounding = $rounding;
} }
@@ -40,7 +38,7 @@ final class DefaultMode extends AbstractTrackingMode
public function canEditDuration(): bool public function canEditDuration(): bool
{ {
return false; return true;
} }
public function canUpdateTimesWithAPI(): bool public function canUpdateTimesWithAPI(): bool
@@ -63,7 +61,7 @@ final class DefaultMode extends AbstractTrackingMode
parent::create($timesheet, $request); parent::create($timesheet, $request);
if (null === $timesheet->getBegin()) { if (null === $timesheet->getBegin()) {
$timesheet->setBegin($this->dateTime->createDateTime()); $timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
} }
$this->rounding->roundBegin($timesheet); $this->rounding->roundBegin($timesheet);

View File

@@ -9,25 +9,22 @@
namespace App\Timesheet\TrackingMode; namespace App\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\UserDateTimeFactory; use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
final class DurationFixedBeginMode implements TrackingModeInterface final class DurationFixedBeginMode implements TrackingModeInterface
{ {
use TrackingModeTrait;
/** /**
* @var UserDateTimeFactory * @var SystemConfiguration
*/
private $dateTime;
/**
* @var TimesheetConfiguration
*/ */
private $configuration; private $configuration;
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration) public function __construct(SystemConfiguration $configuration)
{ {
$this->dateTime = $dateTime;
$this->configuration = $configuration; $this->configuration = $configuration;
} }
@@ -54,11 +51,11 @@ final class DurationFixedBeginMode implements TrackingModeInterface
public function create(Timesheet $timesheet, ?Request $request = null): void public function create(Timesheet $timesheet, ?Request $request = null): void
{ {
if (null === $timesheet->getBegin()) { if (null === $timesheet->getBegin()) {
$timesheet->setBegin($this->dateTime->createDateTime()); $timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
} }
$newBegin = clone $timesheet->getBegin(); $newBegin = clone $timesheet->getBegin();
$newBegin->modify($this->configuration->getDefaultBeginTime()); $newBegin->modify($this->configuration->getTimesheetDefaultBeginTime());
$timesheet->setBegin($newBegin); $timesheet->setBegin($newBegin);
} }

View File

@@ -9,11 +9,23 @@
namespace App\Timesheet\TrackingMode; namespace App\Timesheet\TrackingMode;
use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
final class DurationOnlyMode extends AbstractTrackingMode final class DurationOnlyMode extends AbstractTrackingMode
{ {
/**
* @var SystemConfiguration
*/
private $configuration;
public function __construct(SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
public function canEditBegin(): bool public function canEditBegin(): bool
{ {
return true; return true;
@@ -47,11 +59,11 @@ final class DurationOnlyMode extends AbstractTrackingMode
public function create(Timesheet $timesheet, ?Request $request = null): void public function create(Timesheet $timesheet, ?Request $request = null): void
{ {
if (null === $timesheet->getBegin()) { if (null === $timesheet->getBegin()) {
$timesheet->setBegin($this->dateTime->createDateTime()); $timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
} }
$newBegin = clone $timesheet->getBegin(); $newBegin = clone $timesheet->getBegin();
$newBegin->modify($this->configuration->getDefaultBeginTime()); $newBegin->modify($this->configuration->getTimesheetDefaultBeginTime());
$timesheet->setBegin($newBegin); $timesheet->setBegin($newBegin);
parent::create($timesheet, $request); parent::create($timesheet, $request);

View File

@@ -10,20 +10,12 @@
namespace App\Timesheet\TrackingMode; namespace App\Timesheet\TrackingMode;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\UserDateTimeFactory; use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
final class PunchInOutMode implements TrackingModeInterface final class PunchInOutMode implements TrackingModeInterface
{ {
/** use TrackingModeTrait;
* @var UserDateTimeFactory
*/
private $dateTime;
public function __construct(UserDateTimeFactory $dateTime)
{
$this->dateTime = $dateTime;
}
public function canEditBegin(): bool public function canEditBegin(): bool
{ {
@@ -48,7 +40,7 @@ final class PunchInOutMode implements TrackingModeInterface
public function create(Timesheet $timesheet, ?Request $request = null): void public function create(Timesheet $timesheet, ?Request $request = null): void
{ {
if (null === $timesheet->getBegin()) { if (null === $timesheet->getBegin()) {
$timesheet->setBegin($this->dateTime->createDateTime()); $timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
} }
} }

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

View File

@@ -9,7 +9,7 @@
namespace App\Timesheet; namespace App\Timesheet;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Timesheet\TrackingMode\TrackingModeInterface; use App\Timesheet\TrackingMode\TrackingModeInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
@@ -20,15 +20,15 @@ final class TrackingModeService
*/ */
private $modes = []; private $modes = [];
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
/** /**
* @param TimesheetConfiguration $configuration * @param SystemConfiguration $configuration
* @param TrackingModeInterface[] $modes * @param TrackingModeInterface[] $modes
*/ */
public function __construct(TimesheetConfiguration $configuration, iterable $modes) public function __construct(SystemConfiguration $configuration, iterable $modes)
{ {
$this->configuration = $configuration; $this->configuration = $configuration;
$this->modes = $modes; $this->modes = $modes;
@@ -44,7 +44,7 @@ final class TrackingModeService
public function getActiveMode(): TrackingModeInterface public function getActiveMode(): TrackingModeInterface
{ {
$trackingMode = $this->configuration->getTrackingMode(); $trackingMode = $this->configuration->getTimesheetTrackingMode();
foreach ($this->getModes() as $mode) { foreach ($this->getModes() as $mode) {
if ($mode->getId() === $trackingMode) { if ($mode->getId() === $trackingMode) {

View File

@@ -9,7 +9,7 @@
namespace App\Twig; namespace App\Twig;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Utils\Markdown; use App\Utils\Markdown;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
@@ -24,14 +24,14 @@ final class MarkdownExtension extends AbstractExtension
*/ */
private $markdown; private $markdown;
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
/** /**
* @param Markdown $parser * @param Markdown $parser
*/ */
public function __construct(Markdown $parser, TimesheetConfiguration $configuration) public function __construct(Markdown $parser, SystemConfiguration $configuration)
{ {
$this->markdown = $parser; $this->markdown = $parser;
$this->configuration = $configuration; $this->configuration = $configuration;
@@ -66,7 +66,7 @@ final class MarkdownExtension extends AbstractExtension
$content = trim(substr($content, 0, 100)) . ' &hellip;'; $content = trim(substr($content, 0, 100)) . ' &hellip;';
} }
if ($this->configuration->isMarkdownEnabled()) { if ($this->configuration->isTimesheetMarkdownEnabled()) {
$content = $this->markdown->toHtml($content, false); $content = $this->markdown->toHtml($content, false);
} elseif ($fullLength) { } elseif ($fullLength) {
$content = '<p>' . nl2br($content) . '</p>'; $content = '<p>' . nl2br($content) . '</p>';
@@ -87,7 +87,7 @@ final class MarkdownExtension extends AbstractExtension
return ''; return '';
} }
if ($this->configuration->isMarkdownEnabled()) { if ($this->configuration->isTimesheetMarkdownEnabled()) {
return $this->markdown->toHtml($content, false); return $this->markdown->toHtml($content, false);
} }

View File

@@ -16,6 +16,11 @@ class Duration
{ {
public const FORMAT_COLON = 'colon'; public const FORMAT_COLON = 'colon';
public const FORMAT_NATURAL = 'natural'; public const FORMAT_NATURAL = 'natural';
public const FORMAT_DECIMAL = 'decimal';
/**
* @deprecated since 1.13
*/
public const FORMAT_SECONDS = 'seconds'; public const FORMAT_SECONDS = 'seconds';
public const FORMAT_WITH_SECONDS = '%h:%m:%s'; public const FORMAT_WITH_SECONDS = '%h:%m:%s';
@@ -61,8 +66,12 @@ class Duration
return $this->parseDuration($duration, self::FORMAT_COLON); 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) { 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); return $this->parseDuration($duration, self::FORMAT_NATURAL);
@@ -91,7 +100,12 @@ class Duration
$seconds = $this->parseNaturalFormat($duration); $seconds = $this->parseNaturalFormat($duration);
break; break;
case self::FORMAT_DECIMAL:
$seconds = $this->parseDecimalFormat($duration);
break;
case self::FORMAT_SECONDS: 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; $seconds = (int) $duration;
break; 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 protected function parseColonFormat(string $duration): int
{ {
$parts = explode(':', $duration); $parts = explode(':', $duration);

View File

@@ -20,12 +20,18 @@ class Duration extends Regex
public function __construct($options = null) public function __construct($options = null)
{ {
$patterns = [ $patterns = [
// decimal times (can be separated by comma or dot, depending on the locale)
'[0-9]{1,}', '[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,}',
'[0-9]{1,}:[0-9]{1,}', '[0-9]{1,}:[0-9]{1,}',
'[0-9]{1,}[hmsHMS]{1}', // https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
'[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{1}', '[0-9]{1,}[hHmMsS]{1}',
'[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{1}[0-9]{1,}[hmsHMS]{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) . '$/'; $options['pattern'] = '/^' . implode('$|^', $patterns) . '$/';

View File

@@ -9,7 +9,7 @@
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
@@ -18,11 +18,11 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetFutureTimesValidator extends ConstraintValidator final class TimesheetFutureTimesValidator extends ConstraintValidator
{ {
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
public function __construct(TimesheetConfiguration $configuration) public function __construct(SystemConfiguration $configuration)
{ {
$this->configuration = $configuration; $this->configuration = $configuration;
} }
@@ -41,14 +41,14 @@ final class TimesheetFutureTimesValidator extends ConstraintValidator
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class); throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
} }
if ($this->configuration->isAllowFutureTimes()) { if ($this->configuration->isTimesheetAllowFutureTimes()) {
return; return;
} }
$now = new \DateTime('now', $timesheet->getBegin()->getTimezone()); $now = new \DateTime('now', $timesheet->getBegin()->getTimezone());
// allow configured default rounding time + 1 minute - see #1295 // 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()) { if (($now->getTimestamp() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
$this->context->buildViolation('The begin date cannot be in the future.') $this->context->buildViolation('The begin date cannot be in the future.')
->atPath('begin') ->atPath('begin')

View File

@@ -9,7 +9,7 @@
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
@@ -23,11 +23,11 @@ final class TimesheetLockdownValidator extends ConstraintValidator
*/ */
private $auth; private $auth;
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration) public function __construct(AuthorizationCheckerInterface $auth, SystemConfiguration $configuration)
{ {
$this->auth = $auth; $this->auth = $auth;
$this->configuration = $configuration; $this->configuration = $configuration;
@@ -53,14 +53,14 @@ final class TimesheetLockdownValidator extends ConstraintValidator
return; return;
} }
if (!$this->configuration->isLockdownActive()) { if (!$this->configuration->isTimesheetLockdownActive()) {
return; return;
} }
$lockedStart = $this->configuration->getLockdownPeriodStart(); $lockedStart = $this->configuration->getTimesheetLockdownPeriodStart();
$lockedEnd = $this->configuration->getLockdownPeriodEnd(); $lockedEnd = $this->configuration->getTimesheetLockdownPeriodEnd();
$gracePeriod = $this->configuration->getLockdownGracePeriod(); $gracePeriod = $this->configuration->getTimesheetLockdownGracePeriod();
if (!empty($gracePeriod)) { if (!empty($gracePeriod)) {
$gracePeriod = $gracePeriod . ' '; $gracePeriod = $gracePeriod . ' ';
} }

View File

@@ -9,7 +9,7 @@
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
@@ -19,7 +19,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetOverlappingValidator extends ConstraintValidator final class TimesheetOverlappingValidator extends ConstraintValidator
{ {
/** /**
* @var TimesheetConfiguration * @var SystemConfiguration
*/ */
private $configuration; private $configuration;
/** /**
@@ -27,7 +27,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
*/ */
private $repository; private $repository;
public function __construct(TimesheetConfiguration $configuration, TimesheetRepository $repository) public function __construct(SystemConfiguration $configuration, TimesheetRepository $repository)
{ {
$this->configuration = $configuration; $this->configuration = $configuration;
$this->repository = $repository; $this->repository = $repository;
@@ -55,7 +55,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
return; return;
} }
if ($this->configuration->isAllowOverlappingRecords()) { if ($this->configuration->isTimesheetAllowOverlappingRecords()) {
return; return;
} }

View File

@@ -20,6 +20,26 @@
</div> </div>
{% endblock daterange_widget %} {% endblock daterange_widget %}
{% block duration_widget %}
{% if form.vars.duration_presets is defined and form.vars.duration_presets is not empty %}
<div class="input-group">
{{ block('form_widget_simple') }}
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right pre-scrollable">
{% for value in form.vars.duration_presets %}
<li class="text-center">
<a href="#" onclick="$('#{{ form.vars.id }}').val('{{ value }}');$('#{{ form.vars.id }}').trigger('change');return false;">{{ value }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endblock duration_widget %}
{% block datetime_widget -%} {% block datetime_widget -%}
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">

View File

@@ -94,18 +94,23 @@
{% if form.begin is defined and form.end is defined and form.duration is defined %} {% if form.begin is defined and form.end is defined and form.duration is defined %}
{% set blockPrefix = form.vars.id %} {% set blockPrefix = form.vars.id %}
<script type="text/javascript"> <script type="text/javascript">
$('body').on('blur change', '#{{ blockPrefix }}_begin', function(ev) { $('body').on('change', '#{{ blockPrefix }}_begin', function(ev) {
changedBegin($(this).val()); changedBegin($(this).val());
}); });
$('body').on('blur change', '#{{ blockPrefix }}_end', function(ev) { $('body').on('change', '#{{ blockPrefix }}_end', function(ev) {
changedEnd($(this).val()); changedEnd($(this).val());
}); });
$('body').on('blur change', '#{{ blockPrefix }}_duration', function(ev) { $('body').on('change', '#{{ blockPrefix }}_duration', function(ev) {
changedDuration($(this).val()); changedDuration();
}); });
function getDurationField()
{
return document.getElementById('{{ blockPrefix }}_duration');
}
{# {#
Ruleset: Ruleset:
- invalid begin => skip - invalid begin => skip
@@ -117,13 +122,12 @@
function changedBegin(value) function changedBegin(value)
{ {
var endField = document.getElementById('{{ blockPrefix }}_end'); var endField = document.getElementById('{{ blockPrefix }}_end');
var durationField = document.getElementById('{{ blockPrefix }}_duration');
var format = endField.dataset.format; var format = endField.dataset.format;
var momentDuration = moment.duration(durationField.value); var momentDuration = getParsedDuration();
var momentBegin = moment(value, format); var momentBegin = moment(value, format);
if (!momentBegin.isValid()) { if (!momentBegin.isValid()) {
return; setDurationAsString(null);
} }
if (endField.value === '' && momentDuration.asSeconds() > 0) { if (endField.value === '' && momentDuration.asSeconds() > 0) {
@@ -143,12 +147,8 @@
momentEnd = moment(endField.value, format); momentEnd = moment(endField.value, format);
var durationMoment = moment.duration(momentEnd.diff(momentBegin)); var durationMoment = moment.duration(momentEnd.diff(momentBegin));
var hours = Math.floor(durationMoment.asHours());
if (hours < 10) {
hours = '0' + hours;
}
durationField.value = hours + ':' + ('0' + durationMoment.minutes()).slice(-2); setDurationAsString(durationMoment);
} }
{# {#
@@ -162,13 +162,12 @@
function changedEnd(value) function changedEnd(value)
{ {
var beginField = document.getElementById('{{ blockPrefix }}_begin'); var beginField = document.getElementById('{{ blockPrefix }}_begin');
var durationField = document.getElementById('{{ blockPrefix }}_duration');
var format = beginField.dataset.format; var format = beginField.dataset.format;
var momentDuration = moment.duration(durationField.value); var momentDuration = getParsedDuration();
var momentEnd = moment(value, format); var momentEnd = moment(value, format);
if (!momentEnd.isValid()) { if (!momentEnd.isValid()) {
return; setDurationAsString(null);
} }
if (beginField.value === '') { if (beginField.value === '') {
@@ -188,12 +187,8 @@
momentEnd = moment(value, format); momentEnd = moment(value, format);
var durationMoment = moment.duration(momentEnd.diff(momentBegin)); var durationMoment = moment.duration(momentEnd.diff(momentBegin));
var hours = Math.floor(durationMoment.asHours());
if (hours < 10) {
hours = '0' + hours;
}
durationField.value = hours + ':' + ('0' + durationMoment.minutes()).slice(-2); setDurationAsString(durationMoment);
} }
{# {#
@@ -204,9 +199,9 @@
- if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration
#} #}
function changedDuration(value) function changedDuration()
{ {
var momentDuration = moment.duration(value); var momentDuration = getParsedDuration();
if (!momentDuration.isValid()) { if (!momentDuration.isValid()) {
return; return;
} }
@@ -228,6 +223,44 @@
} }
} }
{# writes the value of a moment-duration object as human readable string into the duration field #}
function setDurationAsString(durationMoment)
{
if (durationMoment === null) {
getDurationField().value = '';
}
if (!durationMoment.isValid()) {
return;
}
var hours = Math.floor(durationMoment.asHours());
if (hours < 10) {
hours = '0' + hours;
}
getDurationField().value = hours + ':' + ('0' + durationMoment.minutes()).slice(-2);
}
{# returns a moment duration object from the duration input field #}
function getParsedDuration()
{
var duration = getDurationField().value.toUpperCase();
var momentDuration = moment.duration(NaN);
if (duration.indexOf(':') !== -1) {
momentDuration = moment.duration(duration);
} else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) {
duration = duration.replace(/,/, '.');
duration = parseFloat(duration) * 3600;
momentDuration = moment.duration('PT' + duration + 'S');
} else if (duration.indexOf('H') !== -1 || duration.indexOf('M') !== -1 || duration.indexOf('S') !== -1) {
momentDuration = moment.duration('PT' + duration);
}
return momentDuration;
}
function applyDateToField(field, momentObj, format) function applyDateToField(field, momentObj, format)
{ {
field.value = momentObj.format(format); field.value = momentObj.format(format);

View File

@@ -18,8 +18,8 @@ use App\Entity\Timesheet;
use App\Entity\TimesheetMeta; use App\Entity\TimesheetMeta;
use App\Entity\User; use App\Entity\User;
use App\Tests\DataFixtures\TimesheetFixtures; use App\Tests\DataFixtures\TimesheetFixtures;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock; use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
use App\Timesheet\DateTimeFactory;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
/** /**
@@ -360,7 +360,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPostAction() public function testPostAction()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE); $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [ $data = [
'activity' => 1, 'activity' => 1,
@@ -384,7 +384,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPostActionWithFullExpandedResponse() public function testPostActionWithFullExpandedResponse()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE); $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [ $data = [
'activity' => 1, 'activity' => 1,
@@ -408,7 +408,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPostActionForDifferentUser() public function testPostActionForDifferentUser()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE); $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$admin = $this->getUserByRole(User::ROLE_ADMIN); $admin = $this->getUserByRole(User::ROLE_ADMIN);
$user = $this->getUserByRole(User::ROLE_USER); $user = $this->getUserByRole(User::ROLE_USER);
@@ -494,7 +494,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testPatchAction() public function testPatchAction()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE); $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER);
$data = [ $data = [
@@ -959,7 +959,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testDuplicateAction() public function testDuplicateAction()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(self::TEST_TIMEZONE); $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [ $data = [
'activity' => 1, 'activity' => 1,

View File

@@ -10,11 +10,11 @@
namespace App\Tests\Configuration; namespace App\Tests\Configuration;
use App\Configuration\CalendarConfiguration; use App\Configuration\CalendarConfiguration;
use App\Configuration\SystemConfiguration;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @covers \App\Configuration\CalendarConfiguration * @covers \App\Configuration\CalendarConfiguration
* @covers \App\Configuration\StringAccessibleConfigTrait
* @group legacy * @group legacy
*/ */
class CalendarConfigurationTest extends TestCase class CalendarConfigurationTest extends TestCase
@@ -28,7 +28,7 @@ class CalendarConfigurationTest extends TestCase
{ {
$loader = new TestConfigLoader($loaderSettings); $loader = new TestConfigLoader($loaderSettings);
return new CalendarConfiguration($loader, $settings); return new CalendarConfiguration(new SystemConfiguration($loader, ['calendar' => $settings]));
} }
/** /**
@@ -90,4 +90,11 @@ class CalendarConfigurationTest extends TestCase
self::assertEquals('09:00', $sut->getTimeframeBegin()); self::assertEquals('09:00', $sut->getTimeframeBegin());
self::assertEquals('21:34', $sut->getTimeframeEnd()); self::assertEquals('21:34', $sut->getTimeframeEnd());
} }
public function testFindByKey()
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertFalse($sut->find('week_numbers'));
$this->assertFalse($sut->find('calendar.week_numbers'));
}
} }

View File

@@ -10,12 +10,12 @@
namespace App\Tests\Configuration; namespace App\Tests\Configuration;
use App\Configuration\FormConfiguration; use App\Configuration\FormConfiguration;
use App\Configuration\SystemConfiguration;
use App\Entity\Configuration; use App\Entity\Configuration;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @covers \App\Configuration\FormConfiguration * @covers \App\Configuration\FormConfiguration
* @covers \App\Configuration\StringAccessibleConfigTrait
* @group legacy * @group legacy
*/ */
class FormConfigurationTest extends TestCase class FormConfigurationTest extends TestCase
@@ -24,7 +24,7 @@ class FormConfigurationTest extends TestCase
{ {
$loader = new TestConfigLoader($loaderSettings); $loader = new TestConfigLoader($loaderSettings);
return new FormConfiguration($loader, $settings); return new FormConfiguration(new SystemConfiguration($loader, ['defaults' => $settings]));
} }
protected function getDefaultSettings() protected function getDefaultSettings()
@@ -83,7 +83,7 @@ class FormConfigurationTest extends TestCase
$this->assertEquals('RU', $sut->getUserDefaultLanguage()); $this->assertEquals('RU', $sut->getUserDefaultLanguage());
$this->assertEquals('black', $sut->getUserDefaultTheme()); $this->assertEquals('black', $sut->getUserDefaultTheme());
$this->assertEquals('Russia/Moscov', $sut->getUserDefaultTimezone()); $this->assertEquals('Russia/Moscov', $sut->getUserDefaultTimezone());
$this->assertEquals('Russia/Moscov', $sut->offsetGet('defaults.user.timezone')); $this->assertEquals('Russia/Moscov', $sut->find('defaults.user.timezone'));
} }
public function testDefaultWithMixedConfigs() public function testDefaultWithMixedConfigs()
@@ -109,8 +109,6 @@ class FormConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), [ $sut = $this->getSut($this->getDefaultSettings(), [
(new Configuration())->setName('defaults.customer.foobar')->setValue('hello'), (new Configuration())->setName('defaults.customer.foobar')->setValue('hello'),
]); ]);
$this->assertTrue($sut->has('customer.foobar'));
$this->assertFalse($sut->has('xxxx.foobar'));
$this->assertEquals('hello', $sut->find('customer.foobar')); $this->assertEquals('hello', $sut->find('customer.foobar'));
} }
} }

View File

@@ -37,6 +37,9 @@ class SystemConfigurationTest extends TestCase
'timesheet' => [ 'timesheet' => [
'rules' => [ 'rules' => [
'allow_future_times' => false, 'allow_future_times' => false,
'lockdown_period_start' => null,
'lockdown_period_end' => null,
'lockdown_grace_period' => null,
], ],
'mode' => 'duration_only', 'mode' => 'duration_only',
'markdown_content' => false, 'markdown_content' => false,
@@ -44,6 +47,9 @@ class SystemConfigurationTest extends TestCase
'hard_limit' => 99, 'hard_limit' => 99,
'soft_limit' => 15, 'soft_limit' => 15,
], ],
'default_begin' => 'now',
'duration_increment' => 10,
'time_increment' => 5,
], ],
'defaults' => [ 'defaults' => [
'customer' => [ 'customer' => [
@@ -94,12 +100,16 @@ class SystemConfigurationTest extends TestCase
return [ return [
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'), (new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
(new Configuration())->setName('defaults.customer.currency')->setValue('RUB'), (new Configuration())->setName('defaults.customer.currency')->setValue('RUB'),
(new Configuration())->setName('calendar.slot_duration')->setValue('00:30:00'),
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue('1'), (new Configuration())->setName('timesheet.rules.allow_future_times')->setValue('1'),
(new Configuration())->setName('timesheet.rules.lockdown_period_start')->setValue('first day of last month'),
(new Configuration())->setName('timesheet.rules.lockdown_period_end')->setValue('last day of last month'),
(new Configuration())->setName('timesheet.rules.lockdown_grace_period')->setValue('+5 days'),
(new Configuration())->setName('timesheet.mode')->setValue('default'), (new Configuration())->setName('timesheet.mode')->setValue('default'),
(new Configuration())->setName('timesheet.markdown_content')->setValue('1'), (new Configuration())->setName('timesheet.markdown_content')->setValue('1'),
(new Configuration())->setName('timesheet.default_begin')->setValue('07:00'),
(new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'), (new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'),
(new Configuration())->setName('timesheet.active_entries.soft_limit')->setValue('3'), (new Configuration())->setName('timesheet.active_entries.soft_limit')->setValue('3'),
(new Configuration())->setName('calendar.slot_duration')->setValue('00:30:00'),
]; ];
} }
@@ -114,7 +124,7 @@ class SystemConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals('Europe/London', $sut->find('defaults.customer.timezone')); $this->assertEquals('Europe/London', $sut->find('defaults.customer.timezone'));
$this->assertEquals('GBP', $sut->find('defaults.customer.currency')); $this->assertEquals('GBP', $sut->find('defaults.customer.currency'));
$this->assertEquals(false, $sut->find('timesheet.rules.allow_future_times')); $this->assertFalse($sut->find('timesheet.rules.allow_future_times'));
$this->assertEquals(99, $sut->find('timesheet.active_entries.hard_limit')); $this->assertEquals(99, $sut->find('timesheet.active_entries.hard_limit'));
} }
@@ -123,7 +133,7 @@ class SystemConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings()); $sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals('Russia/Moscov', $sut->find('defaults.customer.timezone')); $this->assertEquals('Russia/Moscov', $sut->find('defaults.customer.timezone'));
$this->assertEquals('RUB', $sut->find('defaults.customer.currency')); $this->assertEquals('RUB', $sut->find('defaults.customer.currency'));
$this->assertEquals(true, $sut->find('timesheet.rules.allow_future_times')); $this->assertTrue($sut->find('timesheet.rules.allow_future_times'));
$this->assertEquals(7, $sut->find('timesheet.active_entries.hard_limit')); $this->assertEquals(7, $sut->find('timesheet.active_entries.hard_limit'));
} }
@@ -132,7 +142,7 @@ class SystemConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), [ $sut = $this->getSut($this->getDefaultSettings(), [
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue(''), (new Configuration())->setName('timesheet.rules.allow_future_times')->setValue(''),
]); ]);
$this->assertEquals(false, $sut->find('timesheet.rules.allow_future_times')); $this->assertFalse($sut->find('timesheet.rules.allow_future_times'));
} }
public function testUnknownConfigs() public function testUnknownConfigs()
@@ -194,4 +204,52 @@ class SystemConfigurationTest extends TestCase
$this->assertEquals('IT', $sut->getUserDefaultLanguage()); $this->assertEquals('IT', $sut->getUserDefaultLanguage());
$this->assertEquals('USD', $sut->getUserDefaultCurrency()); $this->assertEquals('USD', $sut->getUserDefaultCurrency());
} }
public function testTimesheetWithoutLoader()
{
$sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals(99, $sut->getTimesheetActiveEntriesHardLimit());
$this->assertEquals(15, $sut->getTimesheetActiveEntriesSoftLimit());
$this->assertFalse($sut->isTimesheetAllowFutureTimes());
$this->assertFalse($sut->isTimesheetMarkdownEnabled());
$this->assertEquals('duration_only', $sut->getTimesheetTrackingMode());
$this->assertEquals('now', $sut->getTimesheetDefaultBeginTime());
$this->assertFalse($sut->isTimesheetLockdownActive());
$this->assertEquals('', $sut->getTimesheetLockdownPeriodStart());
$this->assertEquals('', $sut->getTimesheetLockdownPeriodEnd());
$this->assertEquals('', $sut->getTimesheetLockdownGracePeriod());
$this->assertEquals('', $sut->isTimesheetAllowOverlappingRecords());
$this->assertEquals('', $sut->getTimesheetDefaultRoundingDays());
$this->assertEquals('', $sut->getTimesheetDefaultRoundingMode());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingDuration());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingEnd());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingBegin());
$this->assertEquals(10, $sut->getTimesheetIncrementDuration());
$this->assertEquals(5, $sut->getTimesheetIncrementBegin());
$this->assertEquals(5, $sut->getTimesheetIncrementEnd());
}
public function testTimesheetWithLoader()
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals(7, $sut->getTimesheetActiveEntriesHardLimit());
$this->assertEquals(3, $sut->getTimesheetActiveEntriesSoftLimit());
$this->assertTrue($sut->isTimesheetAllowFutureTimes());
$this->assertTrue($sut->isTimesheetMarkdownEnabled());
$this->assertEquals('default', $sut->getTimesheetTrackingMode());
$this->assertEquals('07:00', $sut->getTimesheetDefaultBeginTime());
$this->assertTrue($sut->isTimesheetLockdownActive());
$this->assertEquals('first day of last month', $sut->getTimesheetLockdownPeriodStart());
$this->assertEquals('last day of last month', $sut->getTimesheetLockdownPeriodEnd());
$this->assertEquals('+5 days', $sut->getTimesheetLockdownGracePeriod());
$this->assertEquals('', $sut->isTimesheetAllowOverlappingRecords());
$this->assertEquals('', $sut->getTimesheetDefaultRoundingDays());
$this->assertEquals('', $sut->getTimesheetDefaultRoundingMode());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingDuration());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingEnd());
$this->assertEquals(0, $sut->getTimesheetDefaultRoundingBegin());
$this->assertEquals(10, $sut->getTimesheetIncrementDuration());
$this->assertEquals(5, $sut->getTimesheetIncrementBegin());
$this->assertEquals(5, $sut->getTimesheetIncrementEnd());
}
} }

View File

@@ -9,13 +9,14 @@
namespace App\Tests\Configuration; namespace App\Tests\Configuration;
use App\Configuration\SystemConfiguration;
use App\Configuration\TimesheetConfiguration; use App\Configuration\TimesheetConfiguration;
use App\Entity\Configuration; use App\Entity\Configuration;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @covers \App\Configuration\TimesheetConfiguration * @covers \App\Configuration\TimesheetConfiguration
* @covers \App\Configuration\StringAccessibleConfigTrait * @group legacy
*/ */
class TimesheetConfigurationTest extends TestCase class TimesheetConfigurationTest extends TestCase
{ {
@@ -28,7 +29,9 @@ class TimesheetConfigurationTest extends TestCase
{ {
$loader = new TestConfigLoader($loaderSettings); $loader = new TestConfigLoader($loaderSettings);
return new TimesheetConfiguration($loader, $settings); $config = new SystemConfiguration($loader, ['timesheet' => $settings]);
return new TimesheetConfiguration($config);
} }
protected function getDefaultSettings() protected function getDefaultSettings()
@@ -74,6 +77,7 @@ class TimesheetConfigurationTest extends TestCase
public function testDefaultWithoutLoader() public function testDefaultWithoutLoader()
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals(99, $sut->getActiveEntriesHardLimit()); $this->assertEquals(99, $sut->getActiveEntriesHardLimit());
$this->assertEquals(15, $sut->getActiveEntriesSoftLimit()); $this->assertEquals(15, $sut->getActiveEntriesSoftLimit());
$this->assertFalse($sut->isAllowFutureTimes()); $this->assertFalse($sut->isAllowFutureTimes());
@@ -84,6 +88,12 @@ class TimesheetConfigurationTest extends TestCase
$this->assertEquals('', $sut->getLockdownPeriodStart()); $this->assertEquals('', $sut->getLockdownPeriodStart());
$this->assertEquals('', $sut->getLockdownPeriodEnd()); $this->assertEquals('', $sut->getLockdownPeriodEnd());
$this->assertEquals('', $sut->getLockdownGracePeriod()); $this->assertEquals('', $sut->getLockdownGracePeriod());
$this->assertEquals('', $sut->isAllowOverlappingRecords());
$this->assertEquals('', $sut->getDefaultRoundingDays());
$this->assertEquals('', $sut->getDefaultRoundingMode());
$this->assertEquals(0, $sut->getDefaultRoundingBegin());
$this->assertEquals(0, $sut->getDefaultRoundingEnd());
$this->assertEquals(0, $sut->getDefaultRoundingDuration());
} }
public function testDefaultWithLoader() public function testDefaultWithLoader()
@@ -91,8 +101,8 @@ class TimesheetConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings()); $sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
$this->assertEquals(7, $sut->getActiveEntriesHardLimit()); $this->assertEquals(7, $sut->getActiveEntriesHardLimit());
$this->assertEquals(3, $sut->getActiveEntriesSoftLimit()); $this->assertEquals(3, $sut->getActiveEntriesSoftLimit());
$this->assertEquals(true, $sut->isAllowFutureTimes()); $this->assertTrue($sut->isAllowFutureTimes());
$this->assertEquals(true, $sut->isMarkdownEnabled()); $this->assertTrue($sut->isMarkdownEnabled());
$this->assertEquals('default', $sut->getTrackingMode()); $this->assertEquals('default', $sut->getTrackingMode());
$this->assertEquals('07:00', $sut->getDefaultBeginTime()); $this->assertEquals('07:00', $sut->getDefaultBeginTime());
$this->assertTrue($sut->isLockdownActive()); $this->assertTrue($sut->isLockdownActive());
@@ -112,8 +122,8 @@ class TimesheetConfigurationTest extends TestCase
public function testFindByKey() public function testFindByKey()
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals(false, $sut->find('rules.allow_future_times')); $this->assertFalse($sut->find('rules.allow_future_times'));
$this->assertEquals(false, $sut->find('timesheet.rules.allow_future_times')); $this->assertFalse($sut->find('timesheet.rules.allow_future_times'));
} }
public function testUnknownConfigAreImported() public function testUnknownConfigAreImported()
@@ -121,7 +131,6 @@ class TimesheetConfigurationTest extends TestCase
$sut = $this->getSut($this->getDefaultSettings(), [ $sut = $this->getSut($this->getDefaultSettings(), [
(new Configuration())->setName('timesheet.foo')->setValue('hello'), (new Configuration())->setName('timesheet.foo')->setValue('hello'),
]); ]);
$this->assertTrue($sut->has('foo'));
$this->assertEquals('hello', $sut->find('foo')); $this->assertEquals('hello', $sut->find('foo'));
} }
} }

View File

@@ -71,6 +71,7 @@ class CalendarControllerTest extends ControllerBaseTest
], ],
'timesheet' => [ 'timesheet' => [
'default_begin' => '08:30:00', 'default_begin' => '08:30:00',
'mode' => 'default'
], ],
'calendar' => [ 'calendar' => [
'businessHours' => [ 'businessHours' => [

View File

@@ -89,7 +89,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class); $configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$this->assertEquals('default', $configService->find('timesheet.mode')); $this->assertEquals('default', $configService->find('timesheet.mode'));
$this->assertEquals(true, $configService->find('timesheet.rules.allow_future_times')); $this->assertTrue($configService->find('timesheet.rules.allow_future_times'));
$this->assertEquals(1, $configService->find('timesheet.active_entries.hard_limit')); $this->assertEquals(1, $configService->find('timesheet.active_entries.hard_limit'));
$this->assertEquals(1, $configService->find('timesheet.active_entries.soft_limit')); $this->assertEquals(1, $configService->find('timesheet.active_entries.soft_limit'));
@@ -117,8 +117,8 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class); $configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$this->assertEquals('duration_only', $configService->find('timesheet.mode')); $this->assertEquals('duration_only', $configService->find('timesheet.mode'));
$this->assertEquals(false, $configService->find('timesheet.rules.allow_future_times')); $this->assertFalse($configService->find('timesheet.rules.allow_future_times'));
$this->assertEquals(false, $configService->find('timesheet.rules.allow_overlapping_records')); $this->assertFalse($configService->find('timesheet.rules.allow_overlapping_records'));
$this->assertEquals(99, $configService->find('timesheet.active_entries.hard_limit')); $this->assertEquals(99, $configService->find('timesheet.active_entries.hard_limit'));
$this->assertEquals(77, $configService->find('timesheet.active_entries.soft_limit')); $this->assertEquals(77, $configService->find('timesheet.active_entries.soft_limit'));
} }
@@ -247,7 +247,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/system-config/'); $this->assertAccessIsGranted($client, '/admin/system-config/');
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class); $configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$this->assertEquals(false, $configService->find('timesheet.markdown_content')); $this->assertFalse($configService->find('timesheet.markdown_content'));
$this->assertEquals('selectpicker', $configService->find('theme.select_type')); $this->assertEquals('selectpicker', $configService->find('theme.select_type'));
$form = $client->getCrawler()->filter('form[name=system_configuration_form_theme]')->form(); $form = $client->getCrawler()->filter('form[name=system_configuration_form_theme]')->form();
@@ -267,7 +267,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class); $configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$this->assertEquals('selectpicker', $configService->find('theme.select_type')); $this->assertEquals('selectpicker', $configService->find('theme.select_type'));
$this->assertEquals(true, $configService->find('timesheet.markdown_content')); $this->assertTrue($configService->find('timesheet.markdown_content'));
} }
public function testUpdateThemeConfigValidation() public function testUpdateThemeConfigValidation()

View File

@@ -190,6 +190,64 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertNull($timesheet->getFixedRate()); $this->assertNull($timesheet->getFixedRate());
} }
/**
* @dataProvider getTestDataForDurationValues
*/
public function testCreateActionWithDurationValues($begin, $end, $duration, $expectedDuration, $expectedEnd)
{
$client = $this->getClientForAuthenticatedUser();
$this->request($client, '/timesheet/create');
$this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
$client->submit($form, [
'timesheet_edit_form' => [
'description' => 'Testing is fun!',
'begin' => $begin,
'end' => $end,
'duration' => $duration,
'project' => 1,
'activity' => 1,
]
]);
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasFlashSuccess($client);
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
$this->assertEquals($expectedDuration, $timesheet->getDuration());
$this->assertEquals($expectedEnd, $timesheet->getEnd()->format('Y-m-d H:i:s'));
$this->assertEquals('Testing is fun!', $timesheet->getDescription());
}
public function getTestDataForDurationValues()
{
// duration is ignored, because end is set and the duration might come from a rounding rule (by default seconds are rounded down with 1)
yield ['2018-12-31 00:00:00', '2018-12-31 02:10:10', '01:00', 7800, '2018-12-31 02:10:00'];
yield ['2018-12-31 00:00:00', '2018-12-31 02:09:59', '01:00', 7740, '2018-12-31 02:09:00'];
// if seconds are given, they are first rounded up (default for duration rounding is 1)
yield ['2018-12-31 00:00:00', null, '01:00', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '01:00:10', 3660, '2018-12-31 01:01:00'];
yield ['2018-12-31 00:00:00', null, '1h', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '1h10m', 4200, '2018-12-31 01:10:00'];
yield ['2018-12-31 00:00:00', null, '1h10s', 3660, '2018-12-31 01:01:00'];
yield ['2018-12-31 00:00:00', null, '60m', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '60M1s', 3660, '2018-12-31 01:01:00'];
yield ['2018-12-31 00:00:00', null, '3600s', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '59m60s', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '1', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '1,0', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '1.0', 3600, '2018-12-31 01:00:00'];
yield ['2018-12-31 00:00:00', null, '1.5', 5400, '2018-12-31 01:30:00'];
yield ['2018-12-31 00:00:00', null, '1,25', 4500, '2018-12-31 01:15:00'];
}
public function testCreateActionShowsMetaFields() public function testCreateActionShowsMetaFields()
{ {
$client = $this->getClientForAuthenticatedUser(); $client = $this->getClientForAuthenticatedUser();

View File

@@ -207,6 +207,8 @@ class AppExtensionTest extends TestCase
'lockdown_grace_period' => null, 'lockdown_grace_period' => null,
], ],
'default_begin' => 'now', 'default_begin' => 'now',
'duration_increment' => null,
'time_increment' => null,
], ],
'kimai.timesheet.rates' => [], 'kimai.timesheet.rates' => [],
'kimai.timesheet.rounding' => [ 'kimai.timesheet.rounding' => [

View File

@@ -286,6 +286,8 @@ class ConfigurationTest extends TestCase
'lockdown_period_end' => null, 'lockdown_period_end' => null,
'lockdown_grace_period' => null, 'lockdown_grace_period' => null,
], ],
'duration_increment' => null,
'time_increment' => null,
], ],
'user' => [ 'user' => [
'registration' => true, 'registration' => true,

View File

@@ -21,7 +21,7 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
{ {
$sut = new ThemeJavascriptTranslationsEvent(); $sut = new ThemeJavascriptTranslationsEvent();
$this->assertCount(23, $sut->getTranslations()); $this->assertCount(24, $sut->getTranslations());
} }
public function testGetterAndSetter() public function testGetterAndSetter()
@@ -31,7 +31,7 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
$sut->setTranslation('hello', 'world', 'testing'); $sut->setTranslation('hello', 'world', 'testing');
$result = $sut->getTranslations(); $result = $sut->getTranslations();
self::assertCount(25, $result); self::assertCount(26, $result);
self::assertArrayHasKey('foo', $result); self::assertArrayHasKey('foo', $result);
self::assertEquals(['bar', 'messages'], $result['foo']); self::assertEquals(['bar', 'messages'], $result['foo']);
self::assertArrayHasKey('hello', $result); self::assertArrayHasKey('hello', $result);

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form;
use App\Form\FormTrait;
use App\Tests\Form\Type\TypeTestModel;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @covers \App\Form\FormTrait
*/
class FormTraitTest extends TypeTestCase
{
use FormTrait;
/**
* @expectedDeprecation FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead
* @group legacy
*/
public function testAddDescription()
{
$data = ['description' => 'foo'];
$model = new TypeTestModel(['description' => 'bar']);
$form = $this->factory->createBuilder(FormType::class, $model);
$this->addDescription($form);
$desc = $form->get('description');
self::assertArrayHasKey('autofocus', $desc->getOption('attr'));
self::assertEquals('autofocus', $desc->getOption('attr')['autofocus']);
$form = $form->getForm();
$desc = $form->get('description');
self::assertFalse($desc->isRequired());
$expected = new TypeTestModel([
'description' => 'foo'
]);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($expected, $model);
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form\Type;
use App\Form\Type\DurationType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @covers \App\Form\Type\DurationType
*/
class DurationTypeTest extends TypeTestCase
{
public function getTestData()
{
yield [4.5, 16200];
yield ['4,5', 16200];
yield ['4:30', 16200];
yield ['4h30m', 16200];
}
/**
* @dataProvider getTestData
*/
public function testSubmitValidData($value, $expected)
{
$data = ['duration' => $value];
$model = new TypeTestModel(['duration' => 3600]);
$form = $this->factory->createBuilder(FormType::class, $model);
$form->add('duration', DurationType::class);
$form = $form->getForm();
$expected = new TypeTestModel([
'duration' => $expected
]);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($expected, $model);
}
public function testPresetPopulatesView()
{
$view = $this->factory->create(DurationType::class, 3600, [
'preset_minutes' => 15,
'preset_hours' => 5,
])->createView();
self::assertArrayHasKey('duration_presets', $view->vars);
self::assertCount(20, $view->vars['duration_presets']);
self::assertEquals('0:30', $view->vars['duration_presets'][1]);
self::assertEquals('4:45', $view->vars['duration_presets'][18]);
}
public function testPresetsAreNotGeneratedOnMissingHours()
{
$view = $this->factory->create(DurationType::class, 3600, [
'preset_minutes' => 5,
])->createView();
self::assertArrayNotHasKey('duration_presets', $view->vars);
}
public function testPresetsAreNotGeneratedOnMissingMinutes()
{
$view = $this->factory->create(DurationType::class, 3600, [
'preset_hours' => 5,
])->createView();
self::assertArrayNotHasKey('duration_presets', $view->vars);
}
public function testPresetsAreNotGeneratedOnNegativeMinutes()
{
$view = $this->factory->create(DurationType::class, 3600, [
'preset_minutes' => -1,
'preset_hours' => 5,
])->createView();
self::assertArrayNotHasKey('duration_presets', $view->vars);
}
public function testPresetsAreNotGeneratedOnNegativeHours()
{
$view = $this->factory->create(DurationType::class, 3600, [
'preset_minutes' => 5,
'preset_hours' => -1,
])->createView();
self::assertArrayNotHasKey('duration_presets', $view->vars);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form\Type;
use App\Form\Type\MinuteIncrementType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @covers \App\Form\Type\MinuteIncrementType
*/
class MinuteIncrementTypeTest extends TypeTestCase
{
public function testSubmitValidData()
{
$data = ['increment' => 4];
$model = new TypeTestModel(['increment' => 5]);
$form = $this->factory->createBuilder(FormType::class, $model);
$form->add('increment', MinuteIncrementType::class);
$form = $form->getForm();
$expected = new TypeTestModel([
'increment' => '3'
]);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($expected, $model);
}
public function testSubmitValidDataWithoutDeactivate()
{
$data = ['increment' => 4];
$model = new TypeTestModel(['increment' => 5]);
$form = $this->factory->createBuilder(FormType::class, $model);
$form->add('increment', MinuteIncrementType::class, ['deactivate' => false]);
$form = $form->getForm();
$expected = new TypeTestModel([
'increment' => '4'
]);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($expected, $model);
}
public function testPresetPopulatesView()
{
$view = $this->factory->create(MinuteIncrementType::class, 3600, [])->createView();
self::assertArrayHasKey('choices', $view->vars);
self::assertCount(16, $view->vars['choices']);
self::assertEquals(null, $view->vars['choices'][0]->data);
self::assertEquals(0, $view->vars['choices'][1]->data);
self::assertEquals(1, $view->vars['choices'][2]->data);
}
public function testPresetPopulatesViewWithoutDeactivate()
{
$view = $this->factory->create(MinuteIncrementType::class, 3600, ['deactivate' => false])->createView();
self::assertArrayHasKey('choices', $view->vars);
self::assertCount(15, $view->vars['choices']);
self::assertEquals(null, $view->vars['choices'][0]->data);
self::assertEquals(1, $view->vars['choices'][1]->data);
self::assertEquals(2, $view->vars['choices'][2]->data);
}
}

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Mocks; namespace App\Tests\Mocks;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Tests\Configuration\TestConfigLoader; use App\Tests\Configuration\TestConfigLoader;
use App\Timesheet\Rounding\CeilRounding; use App\Timesheet\Rounding\CeilRounding;
use App\Timesheet\Rounding\ClosestRounding; use App\Timesheet\Rounding\ClosestRounding;
@@ -35,8 +35,8 @@ class RoundingServiceFactory extends AbstractMockFactory
]; ];
} }
$configuration = new TimesheetConfiguration($loader, [ $configuration = new SystemConfiguration($loader, [
'rounding' => $rules 'timesheet' => ['rounding' => $rules]
]); ]);
$modes = [ $modes = [

View File

@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Mocks\Security;
use App\Entity\User;
use App\Tests\Mocks\AbstractMockFactory;
use App\Timesheet\UserDateTimeFactory;
class UserDateTimeFactoryFactory extends AbstractMockFactory
{
public function create(?string $timezone = null): UserDateTimeFactory
{
$userFactory = new CurrentUserFactory($this->getTestCase());
$currentUser = $userFactory->create(new User(), $timezone);
return new UserDateTimeFactory($currentUser);
}
}

View File

@@ -9,9 +9,8 @@
namespace App\Tests\Mocks; namespace App\Tests\Mocks;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Tests\Configuration\TestConfigLoader; use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Timesheet\TrackingMode\DefaultMode; use App\Timesheet\TrackingMode\DefaultMode;
use App\Timesheet\TrackingMode\DurationFixedBeginMode; use App\Timesheet\TrackingMode\DurationFixedBeginMode;
use App\Timesheet\TrackingMode\DurationOnlyMode; use App\Timesheet\TrackingMode\DurationOnlyMode;
@@ -26,17 +25,16 @@ class TrackingModeServiceFactory extends AbstractMockFactory
$mode = 'default'; $mode = 'default';
} }
$dateTime = (new UserDateTimeFactoryFactory($this->getTestCase()))->create();
$loader = new TestConfigLoader([]); $loader = new TestConfigLoader([]);
$configuration = new TimesheetConfiguration($loader, ['mode' => $mode]); $configuration = new SystemConfiguration($loader, ['timesheet' => ['mode' => $mode]]);
if (null === $modes) { if (null === $modes) {
$modes = [ $modes = [
new DefaultMode($dateTime, $configuration, (new RoundingServiceFactory($this->getTestCase()))->create()), new DefaultMode((new RoundingServiceFactory($this->getTestCase()))->create()),
new PunchInOutMode($dateTime), new PunchInOutMode(),
new DurationOnlyMode($dateTime, $configuration), new DurationOnlyMode($configuration),
new DurationFixedBeginMode($dateTime, $configuration), new DurationFixedBeginMode($configuration),
]; ];
} }

View File

@@ -44,7 +44,7 @@ class SamlLogoutHandlerTest extends TestCase
$auth->expects($this->once())->method('getSLOurl')->willReturn('/logout'); $auth->expects($this->once())->method('getSLOurl')->willReturn('/logout');
$auth->expects($this->once())->method('logout')->willReturnCallback(function () { $auth->expects($this->once())->method('logout')->willReturnCallback(function () {
$args = \func_get_args(); $args = \func_get_args();
self::assertEquals(null, $args[0]); self::assertNull($args[0]);
self::assertEquals([], $args[1]); self::assertEquals([], $args[1]);
self::assertEquals('tony', $args[2]); self::assertEquals('tony', $args[2]);
self::assertEquals('foo-bar', $args[3]); self::assertEquals('foo-bar', $args[3]);

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Timesheet; namespace App\Tests\Timesheet;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Entity\User; use App\Entity\User;
use App\Event\TimesheetCreatePostEvent; use App\Event\TimesheetCreatePostEvent;
@@ -42,8 +42,8 @@ class TimesheetServiceTest extends TestCase
?ValidatorInterface $validator = null, ?ValidatorInterface $validator = null,
?TimesheetRepository $repository = null ?TimesheetRepository $repository = null
): TimesheetService { ): TimesheetService {
$configuration = $this->createMock(TimesheetConfiguration::class); $configuration = $this->createMock(SystemConfiguration::class);
$configuration->method('getActiveEntriesHardLimit')->willReturn(1); $configuration->method('getTimesheetActiveEntriesHardLimit')->willReturn(1);
if ($repository === null) { if ($repository === null) {
$repository = $this->createMock(TimesheetRepository::class); $repository = $this->createMock(TimesheetRepository::class);

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Timesheet\TrackingMode; namespace App\Tests\Timesheet\TrackingMode;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Entity\User;
use App\Timesheet\TrackingMode\AbstractTrackingMode; use App\Timesheet\TrackingMode\AbstractTrackingMode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -24,6 +25,14 @@ abstract class AbstractTrackingModeTest extends TestCase
*/ */
abstract protected function createSut(); abstract protected function createSut();
protected function createTimesheet(): Timesheet
{
$timesheet = new Timesheet();
$timesheet->setUser(new User());
return $timesheet;
}
protected function assertDefaultBegin(Timesheet $timesheet) protected function assertDefaultBegin(Timesheet $timesheet)
{ {
self::assertNull($timesheet->getBegin()); self::assertNull($timesheet->getBegin());
@@ -33,7 +42,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
self::assertNull($timesheet->getBegin()); self::assertNull($timesheet->getBegin());
self::assertNull($timesheet->getEnd()); self::assertNull($timesheet->getEnd());
@@ -48,7 +57,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'begin' => '2017-07-23', 'begin' => '2017-07-23',
]); ]);
@@ -65,7 +74,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'begin' => '2017-07-23', 'begin' => '2017-07-23',
'end' => '2017-07-23', 'end' => '2017-07-23',
@@ -85,7 +94,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'begin' => '10x0-99-99', 'begin' => '10x0-99-99',
'end' => '2017-07-23', 'end' => '2017-07-23',
@@ -102,7 +111,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'begin' => '2017-07-23', 'begin' => '2017-07-23',
'end' => '20xx-07-23', 'end' => '20xx-07-23',
@@ -120,7 +129,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'from' => '2018-05-23 21:47:55', 'from' => '2018-05-23 21:47:55',
]); ]);
@@ -136,7 +145,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'from' => '2018-05-23 21:47:55', 'from' => '2018-05-23 21:47:55',
'to' => '2018-05-24 01:11:11', 'to' => '2018-05-24 01:11:11',
@@ -156,7 +165,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'begin' => '2017-07-23', 'begin' => '2017-07-23',
'end' => '2017-07-23', 'end' => '2017-07-23',
@@ -178,7 +187,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'from' => '2018-xx-23 21:47:55', 'from' => '2018-xx-23 21:47:55',
'to' => '2018-05-24 01:11:11', 'to' => '2018-05-24 01:11:11',
@@ -195,7 +204,7 @@ abstract class AbstractTrackingModeTest extends TestCase
{ {
$sut = $this->createSut(); $sut = $this->createSut();
$timesheet = new Timesheet(); $timesheet = $this->createTimesheet();
$request = new Request([ $request = new Request([
'from' => '2018-05-23 21:47:55', 'from' => '2018-05-23 21:47:55',
'to' => '2018-xx-24 01:11:11', 'to' => '2018-xx-24 01:11:11',

View File

@@ -9,11 +9,8 @@
namespace App\Tests\Timesheet\TrackingMode; namespace App\Tests\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\RoundingServiceFactory; use App\Tests\Mocks\RoundingServiceFactory;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Timesheet\TrackingMode\DefaultMode; use App\Timesheet\TrackingMode\DefaultMode;
/** /**
@@ -32,11 +29,7 @@ class DefaultModeTest extends AbstractTrackingModeTest
*/ */
protected function createSut() protected function createSut()
{ {
$loader = new TestConfigLoader([]); return new DefaultMode((new RoundingServiceFactory($this))->create());
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
return new DefaultMode($dateTime, $configuration, (new RoundingServiceFactory($this))->create());
} }
public function testDefaultValues() public function testDefaultValues()
@@ -45,7 +38,7 @@ class DefaultModeTest extends AbstractTrackingModeTest
self::assertTrue($sut->canEditBegin()); self::assertTrue($sut->canEditBegin());
self::assertTrue($sut->canEditEnd()); self::assertTrue($sut->canEditEnd());
self::assertFalse($sut->canEditDuration()); self::assertTrue($sut->canEditDuration());
self::assertTrue($sut->canUpdateTimesWithAPI()); self::assertTrue($sut->canUpdateTimesWithAPI());
self::assertTrue($sut->canSeeBeginAndEndTimes()); self::assertTrue($sut->canSeeBeginAndEndTimes());
self::assertEquals('default', $sut->getId()); self::assertEquals('default', $sut->getId());

View File

@@ -9,10 +9,10 @@
namespace App\Tests\Timesheet\TrackingMode; namespace App\Tests\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Entity\User;
use App\Tests\Configuration\TestConfigLoader; use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Timesheet\TrackingMode\DurationFixedBeginMode; use App\Timesheet\TrackingMode\DurationFixedBeginMode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -25,10 +25,9 @@ class DurationFixedBeginModeTest extends TestCase
protected function createSut() protected function createSut()
{ {
$loader = new TestConfigLoader([]); $loader = new TestConfigLoader([]);
$dateTime = (new UserDateTimeFactoryFactory($this))->create(); $configuration = new SystemConfiguration($loader, ['timesheet' => ['default_begin' => '13:47']]);
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
return new DurationFixedBeginMode($dateTime, $configuration); return new DurationFixedBeginMode($configuration);
} }
public function testDefaultValues() public function testDefaultValues()
@@ -57,7 +56,7 @@ class DurationFixedBeginModeTest extends TestCase
public function testCreateWithoutBeginInjectsBegin() public function testCreateWithoutBeginInjectsBegin()
{ {
$timesheet = new Timesheet(); $timesheet = (new Timesheet())->setUser(new User());
$request = new Request(); $request = new Request();
$sut = $this->createSut(); $sut = $this->createSut();

View File

@@ -9,10 +9,9 @@
namespace App\Tests\Timesheet\TrackingMode; namespace App\Tests\Timesheet\TrackingMode;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Tests\Configuration\TestConfigLoader; use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
use App\Timesheet\TrackingMode\DurationOnlyMode; use App\Timesheet\TrackingMode\DurationOnlyMode;
/** /**
@@ -29,10 +28,9 @@ class DurationOnlyModeTest extends AbstractTrackingModeTest
protected function createSut() protected function createSut()
{ {
$loader = new TestConfigLoader([]); $loader = new TestConfigLoader([]);
$dateTime = (new UserDateTimeFactoryFactory($this))->create(); $configuration = new SystemConfiguration($loader, ['timesheet' => ['default_begin' => '13:45:37']]);
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:45:37']);
return new DurationOnlyMode($dateTime, $configuration); return new DurationOnlyMode($configuration);
} }
public function testDefaultValues() public function testDefaultValues()

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Timesheet\TrackingMode; namespace App\Tests\Timesheet\TrackingMode;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory; use App\Entity\User;
use App\Timesheet\TrackingMode\PunchInOutMode; use App\Timesheet\TrackingMode\PunchInOutMode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -22,8 +22,7 @@ class PunchInOutModeTest extends TestCase
{ {
public function testDefaultValues() public function testDefaultValues()
{ {
$dateTime = (new UserDateTimeFactoryFactory($this))->create(); $sut = new PunchInOutMode();
$sut = new PunchInOutMode($dateTime);
self::assertFalse($sut->canEditBegin()); self::assertFalse($sut->canEditBegin());
self::assertFalse($sut->canEditEnd()); self::assertFalse($sut->canEditEnd());
@@ -40,19 +39,17 @@ class PunchInOutModeTest extends TestCase
$timesheet->setBegin($startingTime); $timesheet->setBegin($startingTime);
$request = new Request(); $request = new Request();
$dateTime = (new UserDateTimeFactoryFactory($this))->create(); $sut = new PunchInOutMode();
$sut = new PunchInOutMode($dateTime);
$sut->create($timesheet, $request); $sut->create($timesheet, $request);
self::assertEquals($timesheet->getBegin(), $startingTime); self::assertEquals($timesheet->getBegin(), $startingTime);
} }
public function testCreateWithoutBegin() public function testCreateWithoutBegin()
{ {
$timesheet = new Timesheet(); $timesheet = (new Timesheet())->setUser(new User());
$request = new Request(); $request = new Request();
$dateTime = (new UserDateTimeFactoryFactory($this))->create(); $sut = new PunchInOutMode();
$sut = new PunchInOutMode($dateTime);
$sut->create($timesheet, $request); $sut->create($timesheet, $request);
self::assertInstanceOf(\DateTime::class, $timesheet->getBegin()); self::assertInstanceOf(\DateTime::class, $timesheet->getBegin());
} }

View File

@@ -9,13 +9,15 @@
namespace App\Tests\Timesheet; namespace App\Tests\Timesheet;
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory; use App\Entity\User;
use App\Tests\Mocks\Security\CurrentUserFactory;
use App\Timesheet\UserDateTimeFactory; use App\Timesheet\UserDateTimeFactory;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* @covers \App\Timesheet\DateTimeFactory * @covers \App\Timesheet\DateTimeFactory
* @covers \App\Timesheet\UserDateTimeFactory * @covers \App\Timesheet\UserDateTimeFactory
* @group legacy
*/ */
class UserDateTimeFactoryTest extends TestCase class UserDateTimeFactoryTest extends TestCase
{ {
@@ -23,7 +25,10 @@ class UserDateTimeFactoryTest extends TestCase
protected function createUserDateTimeFactory(?string $timezone = null): UserDateTimeFactory protected function createUserDateTimeFactory(?string $timezone = null): UserDateTimeFactory
{ {
return (new UserDateTimeFactoryFactory($this))->create($timezone); $userFactory = new CurrentUserFactory($this);
$currentUser = $userFactory->create(new User(), $timezone);
return new UserDateTimeFactory($currentUser);
} }
public function testGetTimezone() public function testGetTimezone()

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Twig; namespace App\Tests\Twig;
use App\Configuration\ConfigLoaderInterface; use App\Configuration\ConfigLoaderInterface;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Twig\MarkdownExtension; use App\Twig\MarkdownExtension;
use App\Utils\Markdown; use App\Utils\Markdown;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -24,7 +24,7 @@ class MarkdownExtensionTest extends TestCase
public function testGetFilters() public function testGetFilters()
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$filters = $sut->getFilters(); $filters = $sut->getFilters();
$this->assertCount(3, $filters); $this->assertCount(3, $filters);
@@ -48,7 +48,7 @@ class MarkdownExtensionTest extends TestCase
public function testMarkdownToHtml() public function testMarkdownToHtml()
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*')); $this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
$this->assertEquals('<p># foobar</p>', $sut->markdownToHtml('# foobar')); $this->assertEquals('<p># foobar</p>', $sut->markdownToHtml('# foobar'));
@@ -57,7 +57,7 @@ class MarkdownExtensionTest extends TestCase
public function testTimesheetContent() public function testTimesheetContent()
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => false]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => false]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals( $this->assertEquals(
"- test<br />\n- foo", "- test<br />\n- foo",
@@ -66,7 +66,7 @@ class MarkdownExtensionTest extends TestCase
$this->assertEquals('', $sut->timesheetContent(null)); $this->assertEquals('', $sut->timesheetContent(null));
$this->assertEquals('', $sut->timesheetContent('')); $this->assertEquals('', $sut->timesheetContent(''));
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals( $this->assertEquals(
"<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>", "<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>",
@@ -77,7 +77,7 @@ class MarkdownExtensionTest extends TestCase
public function testCommentContent() public function testCommentContent()
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, ['markdown_content' => false]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => false]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals( $this->assertEquals(
"<p>- test<br />\n- foo</p>", "<p>- test<br />\n- foo</p>",
@@ -95,7 +95,7 @@ class MarkdownExtensionTest extends TestCase
$this->assertEquals('<p>' . $loremIpsum . '</p>', $sut->commentContent($loremIpsum, true)); $this->assertEquals('<p>' . $loremIpsum . '</p>', $sut->commentContent($loremIpsum, true));
$this->assertEquals('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l &hellip;', $sut->commentContent($loremIpsum)); $this->assertEquals('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l &hellip;', $sut->commentContent($loremIpsum));
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]); $config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config); $sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals( $this->assertEquals(
"<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>", "<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>",

View File

@@ -50,6 +50,6 @@ class ThemeEventExtensionTest extends TestCase
{ {
$sut = $this->getSut(); $sut = $this->getSut();
$values = $sut->getJavascriptTranslations(); $values = $sut->getJavascriptTranslations();
self::assertCount(23, $values); self::assertCount(24, $values);
} }
} }

View File

@@ -26,6 +26,19 @@ class DurationTest extends TestCase
$this->assertEquals('02:38:14', $sut->format(9494, Duration::FORMAT_WITH_SECONDS)); $this->assertEquals('02:38:14', $sut->format(9494, Duration::FORMAT_WITH_SECONDS));
} }
/**
* @group legacy
*/
public function testParseDurationStringSpecials()
{
$sut = new Duration();
$this->assertEquals(0, $sut->parseDuration('-1', Duration::FORMAT_SECONDS));
$this->assertEquals(0, $sut->parseDuration('0', Duration::FORMAT_SECONDS));
$this->assertEquals(3600, $sut->parseDuration('3600', Duration::FORMAT_SECONDS));
$this->assertEquals(0, $sut->parseDuration('', Duration::FORMAT_SECONDS));
$this->assertEquals(0, $sut->parseDuration('-12', Duration::FORMAT_SECONDS));
}
/** /**
* @dataProvider getParseDurationTestData * @dataProvider getParseDurationTestData
*/ */
@@ -47,13 +60,15 @@ class DurationTest extends TestCase
public function getParseDurationTestData() public function getParseDurationTestData()
{ {
return [ return [
[0, '', Duration::FORMAT_SECONDS], [3600, 1, Duration::FORMAT_DECIMAL],
[0, 0, Duration::FORMAT_SECONDS], [5400, 1.5, Duration::FORMAT_DECIMAL],
[0, -12, Duration::FORMAT_SECONDS], [3600, '1', Duration::FORMAT_DECIMAL],
[3600, 3600, Duration::FORMAT_SECONDS], [5400, '1.5', Duration::FORMAT_DECIMAL],
[5400, '1,5', Duration::FORMAT_DECIMAL],
[0, '', Duration::FORMAT_NATURAL], [0, '', Duration::FORMAT_NATURAL],
[0, 0, Duration::FORMAT_NATURAL], [0, 0, Duration::FORMAT_NATURAL],
[99, '99s', Duration::FORMAT_NATURAL], [99, '99s', Duration::FORMAT_NATURAL],
[7200, '2h', Duration::FORMAT_NATURAL], [7200, '2h', Duration::FORMAT_NATURAL],
[2280, '38m', Duration::FORMAT_NATURAL], [2280, '38m', Duration::FORMAT_NATURAL],
@@ -63,6 +78,10 @@ class DurationTest extends TestCase
[0, '', Duration::FORMAT_COLON], [0, '', Duration::FORMAT_COLON],
[0, 0, Duration::FORMAT_COLON], [0, 0, Duration::FORMAT_COLON],
[12420, '3:27', Duration::FORMAT_COLON],
[12420, '3h27m', Duration::FORMAT_NATURAL],
[48420, '13:27', Duration::FORMAT_COLON], [48420, '13:27', Duration::FORMAT_COLON],
[48474, '13:27:54', Duration::FORMAT_COLON], [48474, '13:27:54', Duration::FORMAT_COLON],
[48474, '12:87:54', Duration::FORMAT_COLON], [48474, '12:87:54', Duration::FORMAT_COLON],

View File

@@ -29,14 +29,18 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
public function getValidData() public function getValidData()
{ {
return [ return [
['99s'],
['2h'], ['2h'],
['38m'], ['38m'],
['99s'],
['2h38m'], ['2h38m'],
['2h38s'],
['2m38s'],
['2h38m17s'], ['2h38m17s'],
['1h96m137s'], ['1h96m137s'],
[''], [''],
['0'], ['0'],
['1.2'],
['2,3'],
[null], [null],
[0], [0],
[11257200], [11257200],
@@ -64,6 +68,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
{ {
$constraint = new Duration(); $constraint = new Duration();
$this->validator->validate($input, $constraint); $this->validator->validate($input, $constraint);
$this->validator->validate(strtoupper($input), $constraint);
$this->assertNoViolation(); $this->assertNoViolation();
} }
@@ -71,7 +76,12 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
{ {
return [ return [
['13-13'], ['13-13'],
['13.13'], ['2m3m'],
['2s3s'],
['2h3h'],
['2m3h'],
['2s3h'],
['2s3m'],
['3127::00'], ['3127::00'],
['3127:00:'], ['3127:00:'],
[':3127:00'], [':3127:00'],
@@ -92,10 +102,27 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
$this->validator->validate($input, $constraint); $this->validator->validate($input, $constraint);
$expectedFormat = \is_string($input) ? '"' . $input . '"' : $input; $this->buildViolation('myMessage')
->setParameter('{{ value }}', '"' . $input . '"')
->setCode(Regex::REGEX_FAILED_ERROR)
->assertRaised();
}
/**
* @dataProvider getInvalidData
* @param mixed $input
*/
public function testValidationErrorUpperCase($input)
{
$input = strtoupper($input);
$constraint = new Duration([
'message' => 'myMessage',
]);
$this->validator->validate($input, $constraint);
$this->buildViolation('myMessage') $this->buildViolation('myMessage')
->setParameter('{{ value }}', $expectedFormat) ->setParameter('{{ value }}', '"' . $input . '"')
->setCode(Regex::REGEX_FAILED_ERROR) ->setCode(Regex::REGEX_FAILED_ERROR)
->assertRaised(); ->assertRaised();
} }

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Validator\Constraints; namespace App\Tests\Validator\Constraints;
use App\Configuration\ConfigLoaderInterface; use App\Configuration\ConfigLoaderInterface;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Validator\Constraints\TimesheetFutureTimes; use App\Validator\Constraints\TimesheetFutureTimes;
use App\Validator\Constraints\TimesheetFutureTimesValidator; use App\Validator\Constraints\TimesheetFutureTimesValidator;
@@ -31,7 +31,8 @@ class TimesheetFutureTimesValidatorTest extends ConstraintValidatorTestCase
protected function createMyValidator(bool $allowFutureTimes = false) protected function createMyValidator(bool $allowFutureTimes = false)
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, [ $config = new SystemConfiguration($loader, [
'timesheet' => [
'rules' => [ 'rules' => [
'allow_future_times' => $allowFutureTimes, 'allow_future_times' => $allowFutureTimes,
], ],
@@ -40,6 +41,7 @@ class TimesheetFutureTimesValidatorTest extends ConstraintValidatorTestCase
'begin' => 1 'begin' => 1
] ]
] ]
]
]); ]);
return new TimesheetFutureTimesValidator($config); return new TimesheetFutureTimesValidator($config);

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Validator\Constraints; namespace App\Tests\Validator\Constraints;
use App\Configuration\ConfigLoaderInterface; use App\Configuration\ConfigLoaderInterface;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Validator\Constraints\TimesheetLockdown; use App\Validator\Constraints\TimesheetLockdown;
use App\Validator\Constraints\TimesheetLockdownValidator; use App\Validator\Constraints\TimesheetLockdownValidator;
@@ -46,12 +46,14 @@ class TimesheetLockdownValidatorTest extends ConstraintValidatorTestCase
); );
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, [ $config = new SystemConfiguration($loader, [
'timesheet' => [
'rules' => [ 'rules' => [
'lockdown_period_start' => $start, 'lockdown_period_start' => $start,
'lockdown_period_end' => $end, 'lockdown_period_end' => $end,
'lockdown_grace_period' => $grace, 'lockdown_grace_period' => $grace,
], ],
]
]); ]);
return new TimesheetLockdownValidator($auth, $config); return new TimesheetLockdownValidator($auth, $config);

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Validator\Constraints; namespace App\Tests\Validator\Constraints;
use App\Configuration\ConfigLoaderInterface; use App\Configuration\ConfigLoaderInterface;
use App\Configuration\TimesheetConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Validator\Constraints\TimesheetOverlapping; use App\Validator\Constraints\TimesheetOverlapping;
@@ -32,10 +32,12 @@ class TimesheetOverlappingValidatorTest extends ConstraintValidatorTestCase
protected function createMyValidator(bool $allowOverlappingRecords = false, bool $hasRecords = true) protected function createMyValidator(bool $allowOverlappingRecords = false, bool $hasRecords = true)
{ {
$loader = $this->createMock(ConfigLoaderInterface::class); $loader = $this->createMock(ConfigLoaderInterface::class);
$config = new TimesheetConfiguration($loader, [ $config = new SystemConfiguration($loader, [
'timesheet' => [
'rules' => [ 'rules' => [
'allow_overlapping_records' => $allowOverlappingRecords, 'allow_overlapping_records' => $allowOverlappingRecords,
], ],
],
]); ]);
$repository = $this->createMock(TimesheetRepository::class); $repository = $this->createMock(TimesheetRepository::class);
$repository->method('hasRecordForTime')->willReturn($hasRecords); $repository->method('hasRecordForTime')->willReturn($hasRecords);

View File

@@ -84,6 +84,10 @@
<source>sum.total</source> <source>sum.total</source>
<target>Gesamt</target> <target>Gesamt</target>
</trans-unit> </trans-unit>
<trans-unit id="modal.dirty">
<source>modal.dirty</source>
<target>Das Formular wurde geändert. Bitte klicken Sie "Speichern" um die Änderungen zu sichern oder "Schließen" um abzubrechen.</target>
</trans-unit>
<!-- <!--
Login / Security Login / Security
--> -->

View File

@@ -84,6 +84,10 @@
<source>sum.total</source> <source>sum.total</source>
<target>Total</target> <target>Total</target>
</trans-unit> </trans-unit>
<trans-unit id="modal.dirty">
<source>modal.dirty</source>
<target>The form has changed. Please click "Save" to save the changes or "Close" to cancel.</target>
</trans-unit>
<!-- <!--
Login / Security Login / Security
--> -->

View File

@@ -226,6 +226,22 @@
<source>first_weekday</source> <source>first_weekday</source>
<target>Erster Tag der Woche</target> <target>Erster Tag der Woche</target>
</trans-unit> </trans-unit>
<trans-unit id="label.timesheet.duration_increment">
<source>label.timesheet.duration_increment</source>
<target>Minuten Auswahl für Dauer</target>
</trans-unit>
<trans-unit id="label.timesheet.time_increment">
<source>label.timesheet.time_increment</source>
<target>Minuten Auswahl für Von &amp; Bis</target>
</trans-unit>
<trans-unit id="increment_rounding">
<source>increment_rounding</source>
<target>Verwende konfigurierten Wert der Rundungsregel</target>
</trans-unit>
<trans-unit id="off">
<source>off</source>
<target>Aus</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -226,6 +226,22 @@
<source>first_weekday</source> <source>first_weekday</source>
<target>First day of the week</target> <target>First day of the week</target>
</trans-unit> </trans-unit>
<trans-unit id="label.timesheet.duration_increment">
<source>label.timesheet.duration_increment</source>
<target>Minute selection for Duration</target>
</trans-unit>
<trans-unit id="label.timesheet.time_increment">
<source>label.timesheet.time_increment</source>
<target>Minute selection for From &amp; To</target>
</trans-unit>
<trans-unit id="increment_rounding">
<source>increment_rounding</source>
<target>Use configured value of the rounding rule</target>
</trans-unit>
<trans-unit id="off">
<source>off</source>
<target>Off</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>