Release 1.19.7 (#3286)
* fix isWeekend() test for sunday fdow * re-use the pattern for optgroup title * prevent method on null * composer update * pre-select an option if it is the only available one * added command to stop all active timesheets
This commit is contained in:
17
README.md
17
README.md
@@ -27,19 +27,20 @@ It is built with modern technologies such as [Symfony](https://github.com/symfon
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- PHP 7.4 or higher (PHP 8 supported, PHP 8.1 does not support LDAP yet)
|
- PHP 7.4, 8.0 or 8.1
|
||||||
- MariaDB or MySQL
|
- MariaDB or MySQL
|
||||||
- A webserver and subdomain
|
- A webserver and subdomain
|
||||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `xsl`, `zip`
|
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `xsl`, `zip`
|
||||||
|
|
||||||
### About
|
### About
|
||||||
|
|
||||||
This is the new version of the open source time tracker Kimai. It is stable and production ready, ships
|
The evolution of the most known(?) open source project time-tracker Kimai. It is stable, production ready and ships
|
||||||
with many advanced features, including but not limited to:
|
with many advanced features, including but not limited to:
|
||||||
|
|
||||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||||
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
|
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
|
||||||
user specific rates, advanced search & filtering, money and time budgets, reporting, support for plugins and many more.
|
user/customer/project specific rates, advanced search & filtering, money and time budgets, reporting, support for plugins
|
||||||
|
and so many more.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -63,9 +64,9 @@ user specific rates, advanced search & filtering, money and time budgets, report
|
|||||||
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
|
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
|
||||||
|
|
||||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
> Kimai uses a rolling release concept for delivering updates.
|
||||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
|
> You don't have to wait for the next official release, upgrade it at any time from the master branch,
|
||||||
> The master branch is always deployable, release tags are only snapshots of the current development version.
|
> which is always deployable - release tags are simple snapshots of the development version.
|
||||||
|
|
||||||
Release versions will be created on a regular base (approx. one release every 2 months).
|
Release versions will be created on a regular base (approx. one release every 2 months).
|
||||||
Every code change, whether it's a new feature or a bugfix, will be done on the master branch.
|
Every code change, whether it's a new feature or a bugfix, will be done on the master branch.
|
||||||
@@ -82,7 +83,7 @@ In case you want to contribute, but you wouldn't know how, here are some suggest
|
|||||||
- Answer questions: You know the answer to another user's problem? Share your knowledge!
|
- Answer questions: You know the answer to another user's problem? Share your knowledge!
|
||||||
- Make a feature request: Something can be done better? Something essential missing? Let us know!
|
- Make a feature request: Something can be done better? Something essential missing? Let us know!
|
||||||
- Report bugs
|
- Report bugs
|
||||||
- Contribute: You don't have to be programmer to help. The documentation and translation could use some love as well.
|
- You don't have to be programmer to help. The documentation and translation could use some love as well.
|
||||||
- Sponsor the project
|
- Sponsor the project, free software still costs money
|
||||||
|
|
||||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||||
|
|||||||
@@ -204,6 +204,23 @@ export default class KimaiFormSelect extends KimaiPlugin {
|
|||||||
// if available, re-select the previous selected option (mostly usable for global activities)
|
// if available, re-select the previous selected option (mostly usable for global activities)
|
||||||
select.val(selectedValue);
|
select.val(selectedValue);
|
||||||
|
|
||||||
|
// pre-select an option if it is the only available one
|
||||||
|
if (select.val() === '') {
|
||||||
|
const allOptions = select.find('option');
|
||||||
|
const optionLength = allOptions.length;
|
||||||
|
let selectOption = '';
|
||||||
|
|
||||||
|
if (optionLength === 1) {
|
||||||
|
selectOption = allOptions[0].value;
|
||||||
|
} else if (optionLength === 2 && emptyOption.length === 1) {
|
||||||
|
selectOption = allOptions[1].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectOption !== '') {
|
||||||
|
select.val(selectOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if we don't trigger the change, the other selects won't reset
|
// if we don't trigger the change, the other selects won't reset
|
||||||
select.trigger('change');
|
select.trigger('change');
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"doctrine/orm": "^2.8",
|
"doctrine/orm": "^2.8",
|
||||||
"erusev/parsedown": "^1.6",
|
"erusev/parsedown": "^1.6",
|
||||||
"friendsofsymfony/rest-bundle": "^3.0",
|
"friendsofsymfony/rest-bundle": "^3.0",
|
||||||
"gedmo/doctrine-extensions": "^3.0",
|
"gedmo/doctrine-extensions": "^3.6",
|
||||||
"handcraftedinthealps/rest-routing-bundle": "^1.0",
|
"handcraftedinthealps/rest-routing-bundle": "^1.0",
|
||||||
"jms/metadata": "^2.0",
|
"jms/metadata": "^2.0",
|
||||||
"jms/serializer-bundle": "^3.9",
|
"jms/serializer-bundle": "^3.9",
|
||||||
|
|||||||
566
composer.lock
generated
566
composer.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"build/runtime.b8e7bb04.js",
|
"build/runtime.b8e7bb04.js",
|
||||||
"build/app.120bbf21.js"
|
"build/app.a8c84f0c.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"build/app.7beab68a.css"
|
"build/app.7beab68a.css"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "build/app.7beab68a.css",
|
"build/app.css": "build/app.7beab68a.css",
|
||||||
"build/app.js": "build/app.120bbf21.js",
|
"build/app.js": "build/app.a8c84f0c.js",
|
||||||
"build/invoice.css": "build/invoice.ccdecd42.css",
|
"build/invoice.css": "build/invoice.ccdecd42.css",
|
||||||
"build/invoice.js": "build/invoice.19f36eca.js",
|
"build/invoice.js": "build/invoice.19f36eca.js",
|
||||||
"build/invoice-pdf.css": "build/invoice-pdf.e73a6dda.css",
|
"build/invoice-pdf.css": "build/invoice-pdf.e73a6dda.css",
|
||||||
|
|||||||
48
src/Command/TimesheetStopAllCommand.php
Normal file
48
src/Command/TimesheetStopAllCommand.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Timesheet\TimesheetService;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
class TimesheetStopAllCommand extends Command
|
||||||
|
{
|
||||||
|
private $timesheetService;
|
||||||
|
|
||||||
|
public function __construct(TimesheetService $timesheetService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->timesheetService = $timesheetService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setName('kimai:timesheet:stop-all');
|
||||||
|
$this->setDescription('Stop all running timesheets immediately');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
|
{
|
||||||
|
$amount = $this->timesheetService->stopAll();
|
||||||
|
|
||||||
|
if (!$output->isQuiet()) {
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$io->success(sprintf('Stopped %s timesheet records.', $amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '1.19.6';
|
public const VERSION = '1.19.7';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 11906;
|
public const VERSION_ID = 11907;
|
||||||
/**
|
/**
|
||||||
* The current release status, either "stable" or "dev"
|
* The current release status, either "stable" or "dev"
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ abstract class AbstractUserReportController extends AbstractController
|
|||||||
/** @var StatisticDate $date */
|
/** @var StatisticDate $date */
|
||||||
foreach ($activityValues['data']->getData() as $date) {
|
foreach ($activityValues['data']->getData() as $date) {
|
||||||
$statisticDate = $dailyProjectStatistic->getByDateTime($date->getDate());
|
$statisticDate = $dailyProjectStatistic->getByDateTime($date->getDate());
|
||||||
|
if ($statisticDate === null) {
|
||||||
|
// this should not happen, but sometimes it does ...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$statisticDate->setTotalDuration($statisticDate->getTotalDuration() + $date->getTotalDuration());
|
$statisticDate->setTotalDuration($statisticDate->getTotalDuration() + $date->getTotalDuration());
|
||||||
$statisticDate->setTotalRate($statisticDate->getTotalRate() + $date->getTotalRate());
|
$statisticDate->setTotalRate($statisticDate->getTotalRate() + $date->getTotalRate());
|
||||||
$statisticDate->setTotalInternalRate($statisticDate->getTotalInternalRate() + $date->getTotalInternalRate());
|
$statisticDate->setTotalInternalRate($statisticDate->getTotalInternalRate() + $date->getTotalInternalRate());
|
||||||
|
|||||||
60
src/Form/Helper/ActivityHelper.php
Normal file
60
src/Form/Helper/ActivityHelper.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?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\Helper;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Activity;
|
||||||
|
|
||||||
|
final class ActivityHelper
|
||||||
|
{
|
||||||
|
public const PATTERN_NAME = '{name}';
|
||||||
|
public const PATTERN_COMMENT = '{comment}';
|
||||||
|
public const PATTERN_SPACER = '{spacer}';
|
||||||
|
public const SPACER = ' - ';
|
||||||
|
|
||||||
|
private $configuration;
|
||||||
|
private $pattern;
|
||||||
|
|
||||||
|
public function __construct(SystemConfiguration $configuration)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoicePattern(): string
|
||||||
|
{
|
||||||
|
if ($this->pattern === null) {
|
||||||
|
$this->pattern = $this->configuration->find('activity.choice_pattern');
|
||||||
|
|
||||||
|
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
||||||
|
$this->pattern = self::PATTERN_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoiceLabel(Activity $activity): string
|
||||||
|
{
|
||||||
|
$name = $this->getChoicePattern();
|
||||||
|
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
|
||||||
|
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
|
||||||
|
|
||||||
|
$name = ltrim($name, self::SPACER);
|
||||||
|
$name = rtrim($name, self::SPACER);
|
||||||
|
|
||||||
|
if ($name === '' || $name === self::SPACER) {
|
||||||
|
$name = $activity->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($name, 0, 110);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/Form/Helper/CustomerHelper.php
Normal file
64
src/Form/Helper/CustomerHelper.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form\Helper;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
|
||||||
|
final class CustomerHelper
|
||||||
|
{
|
||||||
|
public const PATTERN_NAME = '{name}';
|
||||||
|
public const PATTERN_NUMBER = '{number}';
|
||||||
|
public const PATTERN_COMPANY = '{company}';
|
||||||
|
public const PATTERN_COMMENT = '{comment}';
|
||||||
|
public const PATTERN_SPACER = '{spacer}';
|
||||||
|
public const SPACER = ' - ';
|
||||||
|
|
||||||
|
private $configuration;
|
||||||
|
private $pattern;
|
||||||
|
|
||||||
|
public function __construct(SystemConfiguration $configuration)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoicePattern(): string
|
||||||
|
{
|
||||||
|
if ($this->pattern === null) {
|
||||||
|
$this->pattern = $this->configuration->find('customer.choice_pattern');
|
||||||
|
|
||||||
|
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
||||||
|
$this->pattern = self::PATTERN_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoiceLabel(Customer $customer): string
|
||||||
|
{
|
||||||
|
$name = $this->getChoicePattern();
|
||||||
|
$name = str_replace(self::PATTERN_NAME, $customer->getName(), $name);
|
||||||
|
$name = str_replace(self::PATTERN_COMMENT, $customer->getComment() ?? '', $name);
|
||||||
|
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
|
||||||
|
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
|
||||||
|
|
||||||
|
$name = ltrim($name, self::SPACER);
|
||||||
|
$name = rtrim($name, self::SPACER);
|
||||||
|
|
||||||
|
if ($name === '' || $name === self::SPACER) {
|
||||||
|
$name = $customer->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($name, 0, 110);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Form/Helper/ProjectHelper.php
Normal file
87
src/Form/Helper/ProjectHelper.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?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\Helper;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Utils\LocaleSettings;
|
||||||
|
|
||||||
|
final class ProjectHelper
|
||||||
|
{
|
||||||
|
public const PATTERN_NAME = '{name}';
|
||||||
|
public const PATTERN_COMMENT = '{comment}';
|
||||||
|
public const PATTERN_ORDERNUMBER = '{ordernumber}';
|
||||||
|
public const PATTERN_DATERANGE = '{daterange}';
|
||||||
|
public const PATTERN_START = '{start}';
|
||||||
|
public const PATTERN_END = '{end}';
|
||||||
|
public const PATTERN_SPACER = '{spacer}';
|
||||||
|
public const SPACER = ' - ';
|
||||||
|
|
||||||
|
private $configuration;
|
||||||
|
private $localeSettings;
|
||||||
|
private $dateFormat;
|
||||||
|
private $pattern;
|
||||||
|
|
||||||
|
public function __construct(SystemConfiguration $configuration, LocaleSettings $localeSettings)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
$this->localeSettings = $localeSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoicePattern(): string
|
||||||
|
{
|
||||||
|
if ($this->pattern === null) {
|
||||||
|
$this->pattern = $this->configuration->find('project.choice_pattern');
|
||||||
|
|
||||||
|
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
||||||
|
$this->pattern = self::PATTERN_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pattern = str_replace(self::PATTERN_DATERANGE, self::PATTERN_START . '-' . self::PATTERN_END, $this->pattern);
|
||||||
|
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChoiceLabel(Project $project): string
|
||||||
|
{
|
||||||
|
if ($this->dateFormat === null) {
|
||||||
|
$this->dateFormat = $this->localeSettings->getDateFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = '?';
|
||||||
|
if ($project->getStart() !== null) {
|
||||||
|
$start = $project->getStart()->format($this->dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
$end = '?';
|
||||||
|
if ($project->getEnd() !== null) {
|
||||||
|
$end = $project->getEnd()->format($this->dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->getChoicePattern();
|
||||||
|
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
|
||||||
|
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
|
||||||
|
$name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber() ?? '', $name);
|
||||||
|
$name = str_replace(self::PATTERN_START, $start, $name);
|
||||||
|
$name = str_replace(self::PATTERN_END, $end, $name);
|
||||||
|
|
||||||
|
$name = ltrim($name, self::SPACER);
|
||||||
|
$name = rtrim($name, self::SPACER);
|
||||||
|
$name = str_replace('- ?-?', '', $name);
|
||||||
|
|
||||||
|
if ($name === '' || $name === self::SPACER) {
|
||||||
|
$name = $project->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($name, 0, 110);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
|
use App\Form\Helper\ActivityHelper;
|
||||||
|
use App\Form\Helper\ProjectHelper;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\Query\ActivityFormTypeQuery;
|
use App\Repository\Query\ActivityFormTypeQuery;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
@@ -25,48 +26,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
*/
|
*/
|
||||||
class ActivityType extends AbstractType
|
class ActivityType extends AbstractType
|
||||||
{
|
{
|
||||||
public const PATTERN_NAME = '{name}';
|
private $activityHelper;
|
||||||
public const PATTERN_COMMENT = '{comment}';
|
private $projectHelper;
|
||||||
public const PATTERN_SPACER = '{spacer}';
|
|
||||||
public const SPACER = ' - ';
|
|
||||||
|
|
||||||
private $configuration;
|
public function __construct(ActivityHelper $activityHelper, ProjectHelper $projectHelper)
|
||||||
private $pattern;
|
|
||||||
|
|
||||||
public function __construct(SystemConfiguration $configuration)
|
|
||||||
{
|
{
|
||||||
$this->configuration = $configuration;
|
$this->activityHelper = $activityHelper;
|
||||||
}
|
$this->projectHelper = $projectHelper;
|
||||||
|
|
||||||
private function getPattern(): string
|
|
||||||
{
|
|
||||||
if ($this->pattern === null) {
|
|
||||||
$this->pattern = $this->configuration->find('activity.choice_pattern');
|
|
||||||
|
|
||||||
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
|
||||||
$this->pattern = self::PATTERN_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->pattern;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getChoiceLabel(Activity $activity): string
|
public function getChoiceLabel(Activity $activity): string
|
||||||
{
|
{
|
||||||
$name = $this->getPattern();
|
return $this->activityHelper->getChoiceLabel($activity);
|
||||||
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
|
|
||||||
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
|
|
||||||
|
|
||||||
$name = ltrim($name, self::SPACER);
|
|
||||||
$name = rtrim($name, self::SPACER);
|
|
||||||
|
|
||||||
if ($name === '' || $name === self::SPACER) {
|
|
||||||
$name = $activity->getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($name, 0, 110);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +49,7 @@ class ActivityType extends AbstractType
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $activity->getProject()->getName();
|
return $this->projectHelper->getChoiceLabel($activity->getProject());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +112,7 @@ class ActivityType extends AbstractType
|
|||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
{
|
{
|
||||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||||
'data-option-pattern' => $this->getPattern(),
|
'data-option-pattern' => $this->activityHelper->getChoicePattern(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
|
use App\Form\Helper\ActivityHelper;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -33,14 +34,14 @@ class ActivityTypePatternType extends AbstractType
|
|||||||
{
|
{
|
||||||
$name = $this->translator->trans('label.name');
|
$name = $this->translator->trans('label.name');
|
||||||
$comment = $this->translator->trans('label.description');
|
$comment = $this->translator->trans('label.description');
|
||||||
$spacer = ActivityType::SPACER;
|
$spacer = ActivityHelper::SPACER;
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'label' => 'label.choice_pattern',
|
'label' => 'label.choice_pattern',
|
||||||
'choices' => [
|
'choices' => [
|
||||||
$name => ActivityType::PATTERN_NAME,
|
$name => ActivityHelper::PATTERN_NAME,
|
||||||
$comment => ActivityType::PATTERN_COMMENT,
|
$comment => ActivityHelper::PATTERN_COMMENT,
|
||||||
$name . $spacer . $comment => ActivityType::PATTERN_NAME . ActivityType::PATTERN_SPACER . ActivityType::PATTERN_COMMENT,
|
$name . $spacer . $comment => ActivityHelper::PATTERN_NAME . ActivityHelper::PATTERN_SPACER . ActivityHelper::PATTERN_COMMENT,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Form\Helper\CustomerHelper;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Repository\Query\CustomerFormTypeQuery;
|
use App\Repository\Query\CustomerFormTypeQuery;
|
||||||
use App\Repository\Query\ProjectQuery;
|
use App\Repository\Query\ProjectQuery;
|
||||||
@@ -26,52 +26,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
*/
|
*/
|
||||||
class CustomerType extends AbstractType
|
class CustomerType extends AbstractType
|
||||||
{
|
{
|
||||||
public const PATTERN_NAME = '{name}';
|
private $customerHelper;
|
||||||
public const PATTERN_NUMBER = '{number}';
|
|
||||||
public const PATTERN_COMPANY = '{company}';
|
|
||||||
public const PATTERN_COMMENT = '{comment}';
|
|
||||||
public const PATTERN_SPACER = '{spacer}';
|
|
||||||
public const SPACER = ' - ';
|
|
||||||
|
|
||||||
private $configuration;
|
public function __construct(CustomerHelper $customerHelper)
|
||||||
private $pattern;
|
|
||||||
|
|
||||||
public function __construct(SystemConfiguration $configuration)
|
|
||||||
{
|
{
|
||||||
$this->configuration = $configuration;
|
$this->customerHelper = $customerHelper;
|
||||||
}
|
|
||||||
|
|
||||||
private function getPattern(): string
|
|
||||||
{
|
|
||||||
if ($this->pattern === null) {
|
|
||||||
$this->pattern = $this->configuration->find('customer.choice_pattern');
|
|
||||||
|
|
||||||
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
|
||||||
$this->pattern = self::PATTERN_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->pattern;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getChoiceLabel(Customer $customer): string
|
public function getChoiceLabel(Customer $customer): string
|
||||||
{
|
{
|
||||||
$name = $this->getPattern();
|
return $this->customerHelper->getChoiceLabel($customer);
|
||||||
$name = str_replace(self::PATTERN_NAME, $customer->getName(), $name);
|
|
||||||
$name = str_replace(self::PATTERN_COMMENT, $customer->getComment() ?? '', $name);
|
|
||||||
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
|
|
||||||
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
|
|
||||||
|
|
||||||
$name = ltrim($name, self::SPACER);
|
|
||||||
$name = rtrim($name, self::SPACER);
|
|
||||||
|
|
||||||
if ($name === '' || $name === self::SPACER) {
|
|
||||||
$name = $customer->getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($name, 0, 110);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getChoiceAttributes(Customer $customer, $key, $value): array
|
public function getChoiceAttributes(Customer $customer, $key, $value): array
|
||||||
@@ -158,7 +122,7 @@ class CustomerType extends AbstractType
|
|||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
{
|
{
|
||||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||||
'data-option-pattern' => $this->getPattern(),
|
'data-option-pattern' => $this->customerHelper->getChoicePattern(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
|
use App\Form\Helper\CustomerHelper;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -35,22 +36,22 @@ class CustomerTypePatternType extends AbstractType
|
|||||||
$company = $this->translator->trans('label.company');
|
$company = $this->translator->trans('label.company');
|
||||||
$number = $this->translator->trans('label.number');
|
$number = $this->translator->trans('label.number');
|
||||||
$comment = $this->translator->trans('label.description');
|
$comment = $this->translator->trans('label.description');
|
||||||
$spacer = CustomerType::SPACER;
|
$spacer = CustomerHelper::SPACER;
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'label' => 'label.choice_pattern',
|
'label' => 'label.choice_pattern',
|
||||||
'choices' => [
|
'choices' => [
|
||||||
$name => CustomerType::PATTERN_NAME,
|
$name => CustomerHelper::PATTERN_NAME,
|
||||||
$company => CustomerType::PATTERN_COMPANY,
|
$company => CustomerHelper::PATTERN_COMPANY,
|
||||||
$number => CustomerType::PATTERN_NUMBER,
|
$number => CustomerHelper::PATTERN_NUMBER,
|
||||||
$comment => CustomerType::PATTERN_COMMENT,
|
$comment => CustomerHelper::PATTERN_COMMENT,
|
||||||
$name . $spacer . $company => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMPANY,
|
$name . $spacer . $company => CustomerHelper::PATTERN_NAME . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_COMPANY,
|
||||||
$name . $spacer . $number => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_NUMBER,
|
$name . $spacer . $number => CustomerHelper::PATTERN_NAME . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_NUMBER,
|
||||||
$name . $spacer . $comment => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
|
$name . $spacer . $comment => CustomerHelper::PATTERN_NAME . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_COMMENT,
|
||||||
$number . $spacer . $name => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_NAME,
|
$number . $spacer . $name => CustomerHelper::PATTERN_NUMBER . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_NAME,
|
||||||
$number . $spacer . $company => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMPANY,
|
$number . $spacer . $company => CustomerHelper::PATTERN_NUMBER . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_COMPANY,
|
||||||
$number . $spacer . $comment => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
|
$number . $spacer . $comment => CustomerHelper::PATTERN_NUMBER . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_COMMENT,
|
||||||
$company . $spacer . $comment => CustomerType::PATTERN_COMPANY . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
|
$company . $spacer . $comment => CustomerHelper::PATTERN_COMPANY . CustomerHelper::PATTERN_SPACER . CustomerHelper::PATTERN_COMMENT,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Form\Helper\CustomerHelper;
|
||||||
|
use App\Form\Helper\ProjectHelper;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\ActivityQuery;
|
use App\Repository\Query\ActivityQuery;
|
||||||
use App\Repository\Query\ProjectFormTypeQuery;
|
use App\Repository\Query\ProjectFormTypeQuery;
|
||||||
use App\Utils\LocaleSettings;
|
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
@@ -27,74 +27,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
*/
|
*/
|
||||||
class ProjectType extends AbstractType
|
class ProjectType extends AbstractType
|
||||||
{
|
{
|
||||||
public const PATTERN_NAME = '{name}';
|
private $projectHelper;
|
||||||
public const PATTERN_COMMENT = '{comment}';
|
private $customerHelper;
|
||||||
public const PATTERN_ORDERNUMBER = '{ordernumber}';
|
|
||||||
public const PATTERN_DATERANGE = '{daterange}';
|
|
||||||
public const PATTERN_START = '{start}';
|
|
||||||
public const PATTERN_END = '{end}';
|
|
||||||
public const PATTERN_SPACER = '{spacer}';
|
|
||||||
public const SPACER = ' - ';
|
|
||||||
|
|
||||||
private $configuration;
|
public function __construct(ProjectHelper $projectHelper, CustomerHelper $customerHelper)
|
||||||
private $localeSettings;
|
|
||||||
private $dateFormat;
|
|
||||||
private $pattern;
|
|
||||||
|
|
||||||
public function __construct(SystemConfiguration $configuration, LocaleSettings $localeSettings)
|
|
||||||
{
|
{
|
||||||
$this->configuration = $configuration;
|
$this->projectHelper = $projectHelper;
|
||||||
$this->localeSettings = $localeSettings;
|
$this->customerHelper = $customerHelper;
|
||||||
}
|
|
||||||
|
|
||||||
private function getPattern(): string
|
|
||||||
{
|
|
||||||
if ($this->pattern === null) {
|
|
||||||
$this->pattern = $this->configuration->find('project.choice_pattern');
|
|
||||||
|
|
||||||
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
|
|
||||||
$this->pattern = self::PATTERN_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pattern = str_replace(self::PATTERN_DATERANGE, self::PATTERN_START . '-' . self::PATTERN_END, $this->pattern);
|
|
||||||
$this->pattern = str_replace(self::PATTERN_SPACER, self::SPACER, $this->pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->pattern;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getChoiceLabel(Project $project): string
|
public function getChoiceLabel(Project $project): string
|
||||||
{
|
{
|
||||||
if ($this->dateFormat === null) {
|
return $this->projectHelper->getChoiceLabel($project);
|
||||||
$this->dateFormat = $this->localeSettings->getDateFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
$start = '?';
|
|
||||||
if ($project->getStart() !== null) {
|
|
||||||
$start = $project->getStart()->format($this->dateFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
$end = '?';
|
|
||||||
if ($project->getEnd() !== null) {
|
|
||||||
$end = $project->getEnd()->format($this->dateFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $this->getPattern();
|
|
||||||
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
|
|
||||||
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
|
|
||||||
$name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber() ?? '', $name);
|
|
||||||
$name = str_replace(self::PATTERN_START, $start, $name);
|
|
||||||
$name = str_replace(self::PATTERN_END, $end, $name);
|
|
||||||
|
|
||||||
$name = ltrim($name, self::SPACER);
|
|
||||||
$name = rtrim($name, self::SPACER);
|
|
||||||
$name = str_replace('- ?-?', '', $name);
|
|
||||||
|
|
||||||
if ($name === '' || $name === self::SPACER) {
|
|
||||||
$name = $project->getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($name, 0, 110);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +72,7 @@ class ProjectType extends AbstractType
|
|||||||
'choice_label' => [$this, 'getChoiceLabel'],
|
'choice_label' => [$this, 'getChoiceLabel'],
|
||||||
'choice_attr' => [$this, 'getChoiceAttributes'],
|
'choice_attr' => [$this, 'getChoiceAttributes'],
|
||||||
'group_by' => function (Project $project, $key, $index) {
|
'group_by' => function (Project $project, $key, $index) {
|
||||||
return $project->getCustomer()->getName();
|
return $this->customerHelper->getChoiceLabel($project->getCustomer());
|
||||||
},
|
},
|
||||||
'query_builder_for_user' => true,
|
'query_builder_for_user' => true,
|
||||||
'activity_enabled' => false,
|
'activity_enabled' => false,
|
||||||
@@ -197,7 +141,7 @@ class ProjectType extends AbstractType
|
|||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
{
|
{
|
||||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||||
'data-option-pattern' => $this->getPattern(),
|
'data-option-pattern' => $this->projectHelper->getChoicePattern(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
|
use App\Form\Helper\ProjectHelper;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -37,16 +38,16 @@ class ProjectTypePatternType extends AbstractType
|
|||||||
$projectStart = $this->translator->trans('label.project_start');
|
$projectStart = $this->translator->trans('label.project_start');
|
||||||
$projectEnd = $this->translator->trans('label.project_end');
|
$projectEnd = $this->translator->trans('label.project_end');
|
||||||
|
|
||||||
$spacer = ProjectType::SPACER;
|
$spacer = ProjectHelper::SPACER;
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'label' => 'label.choice_pattern',
|
'label' => 'label.choice_pattern',
|
||||||
'choices' => [
|
'choices' => [
|
||||||
$name => ProjectType::PATTERN_NAME,
|
$name => ProjectHelper::PATTERN_NAME,
|
||||||
$comment => ProjectType::PATTERN_COMMENT,
|
$comment => ProjectHelper::PATTERN_COMMENT,
|
||||||
$name . $spacer . $orderNumber => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_ORDERNUMBER,
|
$name . $spacer . $orderNumber => ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER . ProjectHelper::PATTERN_ORDERNUMBER,
|
||||||
$name . $spacer . $comment => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_COMMENT,
|
$name . $spacer . $comment => ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER . ProjectHelper::PATTERN_COMMENT,
|
||||||
$name . $spacer . $projectStart . '-' . $projectEnd => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_DATERANGE,
|
$name . $spacer . $projectStart . '-' . $projectEnd => ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER . ProjectHelper::PATTERN_DATERANGE,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,10 +223,11 @@ final class TimesheetService
|
|||||||
* But also to check that all required data is set.
|
* But also to check that all required data is set.
|
||||||
*
|
*
|
||||||
* @param Timesheet $timesheet
|
* @param Timesheet $timesheet
|
||||||
|
* @param bool $validate
|
||||||
* @throws ValidationException for already stopped timesheets
|
* @throws ValidationException for already stopped timesheets
|
||||||
* @throws ValidationFailedException
|
* @throws ValidationFailedException
|
||||||
*/
|
*/
|
||||||
public function stopTimesheet(Timesheet $timesheet): void
|
public function stopTimesheet(Timesheet $timesheet, bool $validate = true): void
|
||||||
{
|
{
|
||||||
if (null !== $timesheet->getEnd()) {
|
if (null !== $timesheet->getEnd()) {
|
||||||
// timesheet already stopped, nothing to do. in previous version, this method did throw a:
|
// timesheet already stopped, nothing to do. in previous version, this method did throw a:
|
||||||
@@ -242,7 +243,9 @@ final class TimesheetService
|
|||||||
$timesheet->setBegin($begin);
|
$timesheet->setBegin($begin);
|
||||||
$timesheet->setEnd($now);
|
$timesheet->setEnd($now);
|
||||||
|
|
||||||
$this->validateTimesheet($timesheet);
|
if ($validate) {
|
||||||
|
$this->validateTimesheet($timesheet);
|
||||||
|
}
|
||||||
|
|
||||||
$this->dispatcher->dispatch(new TimesheetStopPreEvent($timesheet));
|
$this->dispatcher->dispatch(new TimesheetStopPreEvent($timesheet));
|
||||||
$this->repository->save($timesheet);
|
$this->repository->save($timesheet);
|
||||||
@@ -328,4 +331,17 @@ final class TimesheetService
|
|||||||
{
|
{
|
||||||
return $this->trackingModeService->getActiveMode();
|
return $this->trackingModeService->getActiveMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function stopAll(): int
|
||||||
|
{
|
||||||
|
$activeEntries = $this->repository->getActiveEntries();
|
||||||
|
$counter = 0;
|
||||||
|
|
||||||
|
foreach ($activeEntries as $timesheet) {
|
||||||
|
$this->stopTimesheet($timesheet, false);
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ final class LocaleFormatExtensions extends AbstractExtension
|
|||||||
private $formats;
|
private $formats;
|
||||||
private $security;
|
private $security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
private $fdowSunday = null;
|
||||||
/**
|
/**
|
||||||
* @var LocaleFormats|null
|
* @var LocaleFormats|null
|
||||||
*/
|
*/
|
||||||
@@ -77,14 +81,7 @@ final class LocaleFormatExtensions extends AbstractExtension
|
|||||||
public function getTests()
|
public function getTests()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigTest('weekend', function ($dateTime) {
|
new TwigTest('weekend', [$this, 'isWeekend']),
|
||||||
if (!$dateTime instanceof \DateTime) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$day = (int) $dateTime->format('w');
|
|
||||||
|
|
||||||
return ($day === 0 || $day === 6);
|
|
||||||
}),
|
|
||||||
new TwigTest('today', function ($dateTime) {
|
new TwigTest('today', function ($dateTime) {
|
||||||
if (!$dateTime instanceof \DateTime) {
|
if (!$dateTime instanceof \DateTime) {
|
||||||
return false;
|
return false;
|
||||||
@@ -149,6 +146,35 @@ final class LocaleFormatExtensions extends AbstractExtension
|
|||||||
return $this->locale;
|
return $this->locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateTime $dateTime
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isWeekend($dateTime): bool
|
||||||
|
{
|
||||||
|
if (!$dateTime instanceof \DateTime) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$day = (int) $dateTime->format('w');
|
||||||
|
|
||||||
|
if ($this->fdowSunday === null) {
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user !== null) {
|
||||||
|
$this->fdowSunday = $user->isFirstDayOfWeekSunday();
|
||||||
|
} else {
|
||||||
|
$this->fdowSunday = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->fdowSunday) {
|
||||||
|
return ($day === 5 || $day === 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($day === 0 || $day === 6);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param DateTime|string $date
|
* @param DateTime|string $date
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
58
tests/Command/TimesheetStopAllCommandTest.php
Normal file
58
tests/Command/TimesheetStopAllCommandTest.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Command;
|
||||||
|
|
||||||
|
use App\Command\TimesheetStopAllCommand;
|
||||||
|
use App\Tests\Mocks\TimesheetServiceFactory;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Command\TimesheetStopAllCommand
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class TimesheetStopAllCommandTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Application
|
||||||
|
*/
|
||||||
|
protected $application;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$factory = new TimesheetServiceFactory($this);
|
||||||
|
$service = $factory->create();
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
|
$kernel = self::bootKernel();
|
||||||
|
$this->application = new Application($kernel);
|
||||||
|
$this->application->add(new TimesheetStopAllCommand($service));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCommandName()
|
||||||
|
{
|
||||||
|
$command = $this->application->find('kimai:timesheet:stop-all');
|
||||||
|
self::assertInstanceOf(TimesheetStopAllCommand::class, $command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRun()
|
||||||
|
{
|
||||||
|
$command = $this->application->find('kimai:timesheet:stop-all');
|
||||||
|
$commandTester = new CommandTester($command);
|
||||||
|
$commandTester->execute(['command' => $command->getName()]);
|
||||||
|
|
||||||
|
$result = $commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertStringContainsString('[OK] Stopped 0 timesheet records.', $result);
|
||||||
|
|
||||||
|
self::assertEquals(0, $commandTester->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ class InvoiceModelTest extends TestCase
|
|||||||
self::assertNull($sut->getDueDate());
|
self::assertNull($sut->getDueDate());
|
||||||
self::assertInstanceOf(InvoiceModel::class, $sut->setTemplate($template));
|
self::assertInstanceOf(InvoiceModel::class, $sut->setTemplate($template));
|
||||||
self::assertSame($template, $sut->getTemplate());
|
self::assertSame($template, $sut->getTemplate());
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
self::assertInstanceOf(\DateTime::class, $sut->getDueDate());
|
self::assertInstanceOf(\DateTime::class, $sut->getDueDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ abstract class AbstractMockFactory
|
|||||||
return $this->testCase;
|
return $this->testCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createMock(string $className)
|
||||||
|
{
|
||||||
|
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock();
|
||||||
|
}
|
||||||
|
|
||||||
protected function getMockBuilder(string $className): MockBuilder
|
protected function getMockBuilder(string $className): MockBuilder
|
||||||
{
|
{
|
||||||
return new MockBuilder($this->testCase, $className);
|
return new MockBuilder($this->testCase, $className);
|
||||||
|
|||||||
34
tests/Mocks/TimesheetServiceFactory.php
Normal file
34
tests/Mocks/TimesheetServiceFactory.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Repository\TimesheetRepository;
|
||||||
|
use App\Timesheet\TimesheetService;
|
||||||
|
use App\Timesheet\TrackingModeService;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
|
class TimesheetServiceFactory extends AbstractMockFactory
|
||||||
|
{
|
||||||
|
public function create(): TimesheetService
|
||||||
|
{
|
||||||
|
$configuration = $this->createMock(SystemConfiguration::class);
|
||||||
|
$repository = $this->createMock(TimesheetRepository::class);
|
||||||
|
$repository->method('getActiveEntries')->willReturn([]);
|
||||||
|
$service = new TrackingModeService($configuration, []);
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$security = $this->createMock(AuthorizationCheckerInterface::class);
|
||||||
|
$validator = $this->createMock(ValidatorInterface::class);
|
||||||
|
|
||||||
|
return new TimesheetService($configuration, $repository, $service, $dispatcher, $security, $validator);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -341,10 +341,8 @@ class BaseQueryTest extends TestCase
|
|||||||
$dateRange->setEnd($end);
|
$dateRange->setEnd($end);
|
||||||
|
|
||||||
self::assertSame($begin, $sut->getDateRange()->getBegin());
|
self::assertSame($begin, $sut->getDateRange()->getBegin());
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
self::assertSame($begin, $sut->getBegin());
|
self::assertSame($begin, $sut->getBegin());
|
||||||
self::assertSame($end, $sut->getDateRange()->getEnd());
|
self::assertSame($end, $sut->getDateRange()->getEnd());
|
||||||
/* @phpstan-ignore-next-line */
|
|
||||||
self::assertSame($end, $sut->getEnd());
|
self::assertSame($end, $sut->getEnd());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
|||||||
|
|
||||||
$result = $repository->stopRecording($timesheet);
|
$result = $repository->stopRecording($timesheet);
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
|
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace App\Tests\Twig;
|
|||||||
use App\Configuration\LanguageFormattings;
|
use App\Configuration\LanguageFormattings;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserPreference;
|
||||||
use App\Twig\LocaleFormatExtensions;
|
use App\Twig\LocaleFormatExtensions;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Intl\Util\IntlTestHelper;
|
use Symfony\Component\Intl\Util\IntlTestHelper;
|
||||||
@@ -35,9 +36,10 @@ class LocaleFormatExtensionsTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @param string|array $locale
|
* @param string|array $locale
|
||||||
* @param array|string $dateSettings
|
* @param array|string $dateSettings
|
||||||
|
* @param bool $fdowSunday
|
||||||
* @return LocaleFormatExtensions
|
* @return LocaleFormatExtensions
|
||||||
*/
|
*/
|
||||||
protected function getSut($locale, $dateSettings)
|
protected function getSut($locale, $dateSettings, $fdowSunday = false)
|
||||||
{
|
{
|
||||||
$language = $locale;
|
$language = $locale;
|
||||||
if (\is_array($locale)) {
|
if (\is_array($locale)) {
|
||||||
@@ -45,8 +47,10 @@ class LocaleFormatExtensionsTest extends TestCase
|
|||||||
$dateSettings = $locale;
|
$dateSettings = $locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->setPreferenceValue(UserPreference::FIRST_WEEKDAY, ($fdowSunday ? 'sunday' : 'monday'));
|
||||||
$security = $this->createMock(Security::class);
|
$security = $this->createMock(Security::class);
|
||||||
$security->expects($this->any())->method('getUser')->willReturn(new User());
|
$security->expects($this->any())->method('getUser')->willReturn($user);
|
||||||
|
|
||||||
$sut = new LocaleFormatExtensions(new LanguageFormattings($dateSettings), $security);
|
$sut = new LocaleFormatExtensions(new LanguageFormattings($dateSettings), $security);
|
||||||
$sut->setLocale($language);
|
$sut->setLocale($language);
|
||||||
@@ -519,9 +523,8 @@ class LocaleFormatExtensionsTest extends TestCase
|
|||||||
$this->assertEquals('0.00', $sut->durationDecimal(null));
|
$this->assertEquals('0.00', $sut->durationDecimal(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTest(string $name): TwigTest
|
private function getTest(LocaleFormatExtensions $sut, string $name): TwigTest
|
||||||
{
|
{
|
||||||
$sut = $this->getSut('en', $this->localeEn);
|
|
||||||
foreach ($sut->getTests() as $test) {
|
foreach ($sut->getTests() as $test) {
|
||||||
if ($test->getName() === $name) {
|
if ($test->getName() === $name) {
|
||||||
return $test;
|
return $test;
|
||||||
@@ -533,7 +536,8 @@ class LocaleFormatExtensionsTest extends TestCase
|
|||||||
|
|
||||||
public function testIsToday()
|
public function testIsToday()
|
||||||
{
|
{
|
||||||
$test = $this->getTest('today');
|
$sut = $this->getSut('en', $this->localeEn);
|
||||||
|
$test = $this->getTest($sut, 'today');
|
||||||
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime()));
|
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime()));
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('-1 day')));
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('-1 day')));
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('+1 day')));
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('+1 day')));
|
||||||
@@ -543,11 +547,27 @@ class LocaleFormatExtensionsTest extends TestCase
|
|||||||
|
|
||||||
public function testIsWeekend()
|
public function testIsWeekend()
|
||||||
{
|
{
|
||||||
$test = $this->getTest('weekend');
|
$sut = $this->getSut('en', $this->localeEn, false);
|
||||||
|
$test = $this->getTest($sut, 'weekend');
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first monday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first tuesday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first wednesday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first thursday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first friday this month')));
|
||||||
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first saturday this month')));
|
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first saturday this month')));
|
||||||
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first sunday this month')));
|
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first sunday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \stdClass()));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), null));
|
||||||
|
|
||||||
|
$sut = $this->getSut('en', $this->localeEn, true);
|
||||||
|
$test = $this->getTest($sut, 'weekend');
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first monday this month')));
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first monday this month')));
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first friday this month')));
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first tuesday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first wednesday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first thursday this month')));
|
||||||
|
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first friday this month')));
|
||||||
|
self::assertTrue(\call_user_func($test->getCallable(), new \DateTime('first saturday this month')));
|
||||||
|
self::assertFalse(\call_user_func($test->getCallable(), new \DateTime('first sunday this month')));
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), new \stdClass()));
|
self::assertFalse(\call_user_func($test->getCallable(), new \stdClass()));
|
||||||
self::assertFalse(\call_user_func($test->getCallable(), null));
|
self::assertFalse(\call_user_func($test->getCallable(), null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ includes:
|
|||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||||
ignoreErrors:
|
|
||||||
- '#Call to static method PHPUnit\\Framework\\Assert::assertSame\(\) with App\\Entity\\[a-zA-Z0-9]+ and null will always evaluate to false.#'
|
|
||||||
excludePaths:
|
excludePaths:
|
||||||
- %rootDir%/../../../tests/Ldap/LdapDriverTest.php
|
- %rootDir%/../../../tests/Ldap/LdapDriverTest.php
|
||||||
inferPrivatePropertyTypeFromConstructor: true
|
inferPrivatePropertyTypeFromConstructor: true
|
||||||
|
|||||||
@@ -2000,9 +2000,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001214:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001214:
|
||||||
version "1.0.30001276"
|
version "1.0.30001335"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001276.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz"
|
||||||
integrity sha512-psUNoaG1ilknZPxi8HuhQWobuhLqtYSRUxplfVkEJdgZNB9TETVYGSBtv4YyfAdGvE6gn2eb0ztiXqHoWJcGnw==
|
integrity sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==
|
||||||
|
|
||||||
caseless@~0.12.0:
|
caseless@~0.12.0:
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user