Release 2.45 (#5721)
Co-authored-by: Henning Klein <info@henningklein.de>
This commit is contained in:
6
.github/workflows/lockfiles.yaml
vendored
6
.github/workflows/lockfiles.yaml
vendored
@@ -4,6 +4,10 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
lockfiles:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -13,5 +17,5 @@ jobs:
|
||||
uses: xalvarez/prevent-file-change-action@v3
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: .*\.lock$
|
||||
pattern: .*\.lock$|^\.github\/.*$
|
||||
trustedAuthors: kevinpapst, dependabot
|
||||
|
||||
@@ -143,7 +143,7 @@ $fixer
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
'yoda_style' => false,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'visibility_required' => ['elements' => [
|
||||
'modifier_keywords' => ['elements' => [
|
||||
'const',
|
||||
'method',
|
||||
'property',
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAutocompleteTags extends KimaiAutocomplete {
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
let results = [];
|
||||
for (let item of data) {
|
||||
results.push({text: item.name, value: item.name, color: item['color-safe']});
|
||||
results.push({text: item.name, value: item.name, color: item.color});
|
||||
}
|
||||
callback(results);
|
||||
}, () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ nelmio_api_doc:
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Not_Expanded] }
|
||||
- { alias: TimesheetExpanded, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Expanded] }
|
||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Not_Expanded] }
|
||||
- { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Subresource, Expanded] }
|
||||
- { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Expanded] }
|
||||
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
|
||||
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
|
||||
- { alias: User, type: App\Entity\User, groups: [Default] }
|
||||
|
||||
48
migrations/Version20251214160001.php
Normal file
48
migrations/Version20251214160001.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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.45
|
||||
*/
|
||||
final class Version20251214160001 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add index for a query called on every timesheet page';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$table = $schema->getTable('kimai2_tags');
|
||||
|
||||
if (!$table->hasIndex('IDX_27CAF54C7AB0E859')) {
|
||||
// used to count the tags for the dropdown (filter and timesheet edit)
|
||||
$table->addIndex(['visible'], 'IDX_27CAF54C7AB0E859');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$table = $schema->getTable('kimai2_tags');
|
||||
|
||||
if ($table->hasIndex('IDX_27CAF54C7AB0E859')) {
|
||||
$table->dropIndex('IDX_27CAF54C7AB0E859');
|
||||
}
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
15
phpstan.neon
15
phpstan.neon
@@ -649,16 +649,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
||||
count: 2
|
||||
@@ -779,11 +769,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 1
|
||||
|
||||
2
public/build/app.1899dc33.js
Normal file
2
public/build/app.1899dc33.js
Normal file
File diff suppressed because one or more lines are too long
@@ -86,7 +86,7 @@ final class ActivityController extends BaseApiController
|
||||
}
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
if (is_numeric($visible)) {
|
||||
$query->setVisibility((int) $visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
abstract class BaseApiController extends AbstractController
|
||||
{
|
||||
@@ -85,8 +84,11 @@ abstract class BaseApiController extends AbstractController
|
||||
$size = $all['size'];
|
||||
if (is_numeric($size)) {
|
||||
$size = (int) $size;
|
||||
if ($size < 1 || $size > self::MAX_PAGE_SIZE) {
|
||||
throw new BadRequestHttpException('Size must be between 1 and ' . self::MAX_PAGE_SIZE);
|
||||
if ($size < 1) {
|
||||
$size = BaseQuery::DEFAULT_PAGESIZE;
|
||||
}
|
||||
if ($size > self::MAX_PAGE_SIZE) {
|
||||
$size = self::MAX_PAGE_SIZE;
|
||||
}
|
||||
$query->setPageSize($size);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ final class CustomerController extends BaseApiController
|
||||
}
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
if (is_numeric($visible)) {
|
||||
$query->setVisibility((int) $visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ final class ProjectController extends BaseApiController
|
||||
}
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
if (is_numeric($visible)) {
|
||||
$query->setVisibility((int) $visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\User\TeamService;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -41,7 +42,8 @@ final class TeamController extends BaseApiController
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly TeamRepository $repository
|
||||
private readonly TeamRepository $repository,
|
||||
private readonly TeamService $teamService
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -88,7 +90,7 @@ final class TeamController extends BaseApiController
|
||||
#[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_team', requirements: ['id' => '\d+'])]
|
||||
public function deleteAction(Team $team): Response
|
||||
{
|
||||
$this->repository->deleteTeam($team);
|
||||
$this->teamService->deleteTeam($team);
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
@@ -104,13 +106,13 @@ final class TeamController extends BaseApiController
|
||||
#[Route(methods: ['POST'], path: '', name: 'post_team')]
|
||||
public function postAction(Request $request): Response
|
||||
{
|
||||
$team = new Team('');
|
||||
$team = $this->teamService->createNewTeam('');
|
||||
|
||||
$form = $this->createForm(TeamApiEditForm::class, $team);
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
|
||||
$view = new View($team, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -139,6 +141,8 @@ final class TeamController extends BaseApiController
|
||||
$team->removeMember($member);
|
||||
$this->repository->removeTeamMember($member);
|
||||
}
|
||||
// this fails, if we use the teamservice, because the validator
|
||||
// complains about teams without members or teamleads
|
||||
$this->repository->saveTeam($team);
|
||||
}
|
||||
|
||||
@@ -154,7 +158,7 @@ final class TeamController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -178,7 +182,7 @@ final class TeamController extends BaseApiController
|
||||
|
||||
$team->addUser($member);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -206,7 +210,7 @@ final class TeamController extends BaseApiController
|
||||
|
||||
$team->removeUser($member);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
@@ -66,7 +66,7 @@ final class UserController extends BaseApiController
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
if (is_numeric($visible)) {
|
||||
$query->setVisibility((int) $visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.44.0';
|
||||
public const VERSION = '2.45.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24400;
|
||||
public const VERSION_ID = 24500;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,6 @@ use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ActivityDetailControllerEvent;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
@@ -32,6 +31,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\User\TeamService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
@@ -40,6 +40,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -303,19 +304,24 @@ final class ActivityController extends AbstractController
|
||||
#[Route(path: '/{id}/create_team', name: 'activity_team_create', methods: ['GET'])]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'activity')]
|
||||
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
|
||||
public function createDefaultTeamAction(Activity $activity, TeamService $teamService): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||
$name = $activity->getName();
|
||||
if ($name === null) {
|
||||
throw new BadRequestHttpException('Cannot create default team for activity with empty name: ' . $activity->getId());
|
||||
}
|
||||
|
||||
$defaultTeam = $teamService->findTeamByName($name);
|
||||
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($activity->getName());
|
||||
$defaultTeam = $teamService->createNewTeam($name);
|
||||
}
|
||||
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addActivity($activity);
|
||||
|
||||
try {
|
||||
$teamRepository->saveTeam($defaultTeam);
|
||||
$teamService->saveTeam($defaultTeam);
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Customer\CustomerStatisticService;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\CustomerDetailControllerEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
@@ -35,6 +34,7 @@ use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\Query\VisibilityInterface;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\User\TeamService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -42,6 +42,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
@@ -239,19 +240,24 @@ final class CustomerController extends AbstractController
|
||||
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'customer')]
|
||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository): Response
|
||||
public function createDefaultTeamAction(Customer $customer, TeamService $teamService): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
$name = $customer->getName();
|
||||
if ($name === null) {
|
||||
throw new BadRequestHttpException('Cannot create default team for customer with empty name: ' . $customer->getId());
|
||||
}
|
||||
|
||||
$defaultTeam = $teamService->findTeamByName($name);
|
||||
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($customer->getName());
|
||||
$defaultTeam = $teamService->createNewTeam($name);
|
||||
}
|
||||
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addCustomer($customer);
|
||||
|
||||
try {
|
||||
$teamRepository->saveTeam($defaultTeam);
|
||||
$teamService->saveTeam($defaultTeam);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectDetailControllerEvent;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
@@ -38,6 +37,7 @@ use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\Query\VisibilityInterface;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\User\TeamService;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -46,6 +46,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
@@ -267,19 +268,24 @@ final class ProjectController extends AbstractController
|
||||
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
|
||||
#[IsGranted('create_team')]
|
||||
#[IsGranted('permissions', 'project')]
|
||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository): Response
|
||||
public function createDefaultTeamAction(Project $project, TeamService $teamService): Response
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
$name = $project->getName();
|
||||
if ($name === null) {
|
||||
throw new BadRequestHttpException('Cannot create default team for project with empty name: ' . $project->getId());
|
||||
}
|
||||
|
||||
$defaultTeam = $teamService->findTeamByName($name);
|
||||
|
||||
if (null === $defaultTeam) {
|
||||
$defaultTeam = new Team($project->getName());
|
||||
$defaultTeam = $teamService->createNewTeam($name);
|
||||
}
|
||||
|
||||
$defaultTeam->addTeamlead($this->getUser());
|
||||
$defaultTeam->addProject($project);
|
||||
|
||||
try {
|
||||
$teamRepository->saveTeam($defaultTeam);
|
||||
$teamService->saveTeam($defaultTeam);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\User\TeamService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
@@ -30,7 +31,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('view_team')]
|
||||
final class TeamController extends AbstractController
|
||||
{
|
||||
public function __construct(private TeamRepository $repository)
|
||||
public function __construct(
|
||||
private readonly TeamRepository $repository,
|
||||
private readonly TeamService $teamService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,7 +89,9 @@ final class TeamController extends AbstractController
|
||||
#[IsGranted('create_team')]
|
||||
public function createTeam(Request $request): Response
|
||||
{
|
||||
return $this->renderEditScreen(new Team(''), $request, true);
|
||||
$team = $this->teamService->createNewTeam('');
|
||||
|
||||
return $this->renderEditScreen($team, $request, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])]
|
||||
@@ -128,7 +134,7 @@ final class TeamController extends AbstractController
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
@@ -167,7 +173,7 @@ final class TeamController extends AbstractController
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->teamService->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($create) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[Serializer\Groups(['Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
||||
private ?Project $project = null;
|
||||
/**
|
||||
@@ -96,7 +96,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
trait BudgetTrait
|
||||
{
|
||||
/**
|
||||
* The total monetary budget, will be zero if not configured.
|
||||
* The total monetary budget (default: 0).
|
||||
*/
|
||||
#[ORM\Column(name: 'budget', type: Types::FLOAT, nullable: false)]
|
||||
#[Assert\Range(min: 0.00, max: 900000000000.00)]
|
||||
@@ -28,7 +28,7 @@ trait BudgetTrait
|
||||
#[Exporter\Expose(label: 'budget', type: 'float')]
|
||||
private float $budget = 0.00;
|
||||
/**
|
||||
* The time budget in seconds, will be zero if not configured.
|
||||
* The time budget in seconds (default: 0).
|
||||
*/
|
||||
#[ORM\Column(name: 'time_budget', type: Types::INTEGER, nullable: false)]
|
||||
#[Assert\Range(min: 0, max: 2145600000)]
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Entity;
|
||||
|
||||
use App\Constants;
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Utils\Color;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -22,8 +23,7 @@ trait ColorTrait
|
||||
* The assigned color in HTML hex format, e.g. #dd1d00
|
||||
*/
|
||||
#[ORM\Column(name: 'color', type: Types::STRING, length: 7, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Exclude]
|
||||
#[Exporter\Expose(label: 'color')]
|
||||
#[Constraints\HexColor]
|
||||
private ?string $color = null;
|
||||
@@ -46,4 +46,17 @@ trait ColorTrait
|
||||
{
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
abstract public function getName(): ?string;
|
||||
|
||||
/**
|
||||
* Internal value: this color will never be empty and is generated by the tag name if not set explicit.
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('color')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
public function getColorSafe(): string
|
||||
{
|
||||
return $this->getColor() ?? (new Color())->getRandom($this->getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,19 +122,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
#[ORM\Column(name: 'phone', type: Types::STRING, length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'phone')]
|
||||
private ?string $phone = null;
|
||||
#[ORM\Column(name: 'fax', type: Types::STRING, length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'fax')]
|
||||
private ?string $fax = null;
|
||||
#[ORM\Column(name: 'mobile', type: Types::STRING, length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'mobile')]
|
||||
private ?string $mobile = null;
|
||||
/**
|
||||
@@ -149,7 +149,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
#[ORM\Column(name: 'homepage', type: Types::STRING, length: 100, nullable: true)]
|
||||
#[Assert\Length(max: 100)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'homepage')]
|
||||
private ?string $homepage = null;
|
||||
/**
|
||||
@@ -160,7 +160,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
#[Assert\Timezone]
|
||||
#[Assert\Length(max: 64)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'timezone')]
|
||||
private ?string $timezone = null;
|
||||
/**
|
||||
@@ -170,7 +170,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
|
||||
@@ -60,7 +60,7 @@ class Invoice implements EntityWithMetaFields
|
||||
private ?string $invoiceNumber = null;
|
||||
#[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
@@ -79,8 +79,6 @@ class Invoice implements EntityWithMetaFields
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?\DateTime $createdAt = null;
|
||||
#[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)]
|
||||
private ?string $timezone = null;
|
||||
@@ -107,7 +105,7 @@ class Invoice implements EntityWithMetaFields
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0, max: 999)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'due_days', type: 'integer')]
|
||||
private int $dueDays = 30;
|
||||
#[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)]
|
||||
@@ -140,7 +138,7 @@ class Invoice implements EntityWithMetaFields
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
@@ -176,6 +174,9 @@ class Invoice implements EntityWithMetaFields
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('createdAt')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(name: 'createdAt', label: 'date', type: 'datetime')]
|
||||
public function getCreatedAt(): ?\DateTime
|
||||
{
|
||||
@@ -202,6 +203,9 @@ class Invoice implements EntityWithMetaFields
|
||||
return $dueDate;
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty()]
|
||||
#[Serializer\SerializedName('overdue')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
public function isOverdue(): bool
|
||||
{
|
||||
if (null === $this->getDueDate()) {
|
||||
|
||||
@@ -56,7 +56,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[Serializer\Groups(['Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/Customer')]
|
||||
private ?Customer $customer = null;
|
||||
/**
|
||||
@@ -85,7 +85,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
*/
|
||||
#[ORM\Column(name: 'order_date', type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project_Entity'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
|
||||
#[Serializer\Accessor(getter: 'getOrderDate')]
|
||||
private ?\DateTime $orderDate = null;
|
||||
@@ -141,7 +141,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\TagRepository;
|
||||
use App\Utils\Color;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
@@ -21,9 +20,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: TagRepository::class)]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[ORM\Index(columns: ['visible'])]
|
||||
#[UniqueEntity('name')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('ColorSafe', exp: 'object.getColorSafe()', options: [new Serializer\SerializedName('color-safe'), new Serializer\Type(name: 'string'), new Serializer\Groups(['Default'])])]
|
||||
class Tag
|
||||
{
|
||||
/**
|
||||
@@ -88,9 +87,4 @@ class Tag
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
public function getColorSafe(): string
|
||||
{
|
||||
return $this->getColor() ?? (new Color())->getRandom($this->getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,23 +54,23 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
use ModifiedTrait;
|
||||
|
||||
/**
|
||||
* Category: Normal work-time (default category)
|
||||
* @deprecated since 2.45
|
||||
*/
|
||||
public const WORK = 'work';
|
||||
/**
|
||||
* Category: Holiday
|
||||
* @deprecated since 2.45
|
||||
*/
|
||||
public const HOLIDAY = 'holiday';
|
||||
/**
|
||||
* Category: Sickness
|
||||
* @deprecated since 2.45
|
||||
*/
|
||||
public const SICKNESS = 'sickness';
|
||||
/**
|
||||
* Category: Parental leave
|
||||
* @deprecated since 2.45
|
||||
*/
|
||||
public const PARENTAL = 'parental';
|
||||
/**
|
||||
* Category: Overtime reduction
|
||||
* @deprecated since 2.45
|
||||
*/
|
||||
public const OVERTIME = 'overtime';
|
||||
|
||||
@@ -132,27 +132,29 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $duration = 0;
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[ORM\Column(name: 'break', type: Types::INTEGER, nullable: true)]
|
||||
private ?int $break = 0;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[Serializer\Groups(['Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
private ?User $user = null;
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[Serializer\Groups(['Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
|
||||
private ?Activity $activity = null;
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[Serializer\Groups(['Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
||||
private ?Project $project = null;
|
||||
#[ORM\Column(name: 'description', type: Types::TEXT, nullable: true)]
|
||||
@@ -196,7 +198,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
private ?string $billableMode = self::BILLABLE_DEFAULT;
|
||||
#[ORM\Column(name: 'category', type: Types::STRING, length: 10, nullable: false, options: ['default' => 'work'])]
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
private ?string $category = 'work';
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
@@ -503,11 +505,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
public function setCategory(string $category): Timesheet
|
||||
{
|
||||
$allowed = [self::WORK, self::HOLIDAY, self::SICKNESS, self::PARENTAL, self::OVERTIME];
|
||||
|
||||
if (!\in_array($category, $allowed)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid timesheet category "%s" given, expected one of: %s', $category, implode(', ', $allowed)));
|
||||
}
|
||||
@trigger_error('Timesheet::setCategory() is deprecated.', E_USER_DEPRECATED);
|
||||
|
||||
$this->category = $category;
|
||||
|
||||
|
||||
@@ -169,6 +169,8 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Assert\Length(min: 2, max: 180)]
|
||||
#[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $email = null;
|
||||
#[ORM\Column(name: 'account', type: Types::STRING, length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
@@ -219,6 +221,8 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[ORM\Column(name: 'totp_enabled', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
|
||||
private bool $totpEnabled = false;
|
||||
#[ORM\Column(name: 'system_account', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $systemAccount = false;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
@@ -352,6 +356,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
|
||||
$all = [];
|
||||
foreach ($this->preferences as $preference) {
|
||||
if ($preference->getName() === null || $preference->getName()[0] === '_') {
|
||||
continue;
|
||||
}
|
||||
if ($preference->isEnabled() && !\in_array($preference->getName(), $skip)) {
|
||||
$all[] = $preference;
|
||||
}
|
||||
@@ -370,7 +377,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
|
||||
/**
|
||||
* @param iterable<UserPreference> $preferences
|
||||
* @return User
|
||||
*/
|
||||
public function setPreferences(iterable $preferences): User
|
||||
{
|
||||
@@ -384,7 +390,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param bool|int|string|float|null $value
|
||||
*/
|
||||
public function setPreferenceValue(string $name, $value = null): void
|
||||
@@ -419,7 +424,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('locale')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getLocale(): string
|
||||
{
|
||||
@@ -434,7 +439,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('timezone')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getTimezone(): string
|
||||
{
|
||||
@@ -442,11 +447,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
}
|
||||
|
||||
/**
|
||||
* The locale used for translations
|
||||
* The locale used for translating the UI
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('language')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getLanguage(): string
|
||||
{
|
||||
|
||||
28
src/Event/AbstractTeamEvent.php
Normal file
28
src/Event/AbstractTeamEvent.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base event class triggered for Team manipulations.
|
||||
*/
|
||||
abstract class AbstractTeamEvent extends Event
|
||||
{
|
||||
public function __construct(private readonly Team $team)
|
||||
{
|
||||
}
|
||||
|
||||
public function getTeam(): Team
|
||||
{
|
||||
return $this->team;
|
||||
}
|
||||
}
|
||||
17
src/Event/TeamCreateEvent.php
Normal file
17
src/Event/TeamCreateEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new team instances, which might or might not be saved.
|
||||
*/
|
||||
final class TeamCreateEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/TeamCreatePostEvent.php
Normal file
17
src/Event/TeamCreatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Webhook\Attribute\AsWebhook;
|
||||
|
||||
#[AsWebhook(name: 'team.created', description: 'Triggered after a team was created', payload: 'object.getTeam()')]
|
||||
final class TeamCreatePostEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/TeamCreatePreEvent.php
Normal file
17
src/Event/TeamCreatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for team instances, which are just about to being saved.
|
||||
*/
|
||||
final class TeamCreatePreEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/TeamDeleteEvent.php
Normal file
17
src/Event/TeamDeleteEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Webhook\Attribute\AsWebhook;
|
||||
|
||||
#[AsWebhook(name: 'team.deleted', description: 'Triggered after a team was deleted', payload: 'object.getTeam()')]
|
||||
final class TeamDeleteEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/TeamUpdatePostEvent.php
Normal file
17
src/Event/TeamUpdatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Webhook\Attribute\AsWebhook;
|
||||
|
||||
#[AsWebhook(name: 'team.updated', description: 'Triggered after a team was updated', payload: 'object.getTeam()')]
|
||||
final class TeamUpdatePostEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/TeamUpdatePreEvent.php
Normal file
17
src/Event/TeamUpdatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for team instances, which are just about to being updated.
|
||||
*/
|
||||
final class TeamUpdatePreEvent extends AbstractTeamEvent
|
||||
{
|
||||
}
|
||||
@@ -29,6 +29,8 @@ class ThemeEvent extends Event
|
||||
private string $content = '';
|
||||
|
||||
/**
|
||||
* User is nullable, because theme events are triggered on anonymous pages, like "Login" or "Kiosk"
|
||||
*
|
||||
* @param array<string, mixed|array<mixed>> $payload
|
||||
*/
|
||||
public function __construct(private readonly ?User $user = null, protected array $payload = [])
|
||||
|
||||
@@ -17,6 +17,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Adds the links in the user profile dropdown in the template on each page.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UserDetailsSubscriber implements EventSubscriberInterface
|
||||
|
||||
@@ -105,6 +105,10 @@ class TimesheetEditForm extends AbstractType
|
||||
$this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
|
||||
}
|
||||
|
||||
if ($this->systemConfiguration->isBreakTimeEnabled()) {
|
||||
$builder->add('break', DurationType::class, ['label' => 'break', 'required' => false, 'icon' => 'break']);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($options['user']); // @phpstan-ignore-line
|
||||
@@ -328,10 +332,6 @@ class TimesheetEditForm extends AbstractType
|
||||
|
||||
$builder->add('duration', DurationType::class, $durationOptions);
|
||||
|
||||
if ($this->systemConfiguration->isBreakTimeEnabled()) {
|
||||
$builder->add('break', DurationType::class, ['label' => 'break', 'required' => false, 'icon' => 'break']);
|
||||
}
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::POST_SET_DATA,
|
||||
function (FormEvent $event): void {
|
||||
|
||||
@@ -25,11 +25,13 @@ final class CalendarToolbarForm extends AbstractType
|
||||
'view_timezone' => $options['timezone'],
|
||||
]);
|
||||
$builder->add('view', CalendarViewType::class, []);
|
||||
if ($options['change_user']) {
|
||||
$builder->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
'attr' => ['onchange' => 'this.form.submit()']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ final class TagsType extends AbstractType
|
||||
public function getParent(): string
|
||||
{
|
||||
if ($this->count === null) {
|
||||
$this->count = $this->repository->count([]);
|
||||
$this->count = $this->repository->count(['visible' => true]);
|
||||
}
|
||||
|
||||
if ($this->count > self::MAX_AMOUNT_SELECT) {
|
||||
|
||||
@@ -75,6 +75,8 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
'entry.duration_format' => $formatter->getFormattedDuration($item->getDuration()),
|
||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
||||
'entry.duration_minutes' => (int) ($item->getDuration() / 60),
|
||||
// prepare optional field with empty string
|
||||
'entry.activity' => '',
|
||||
];
|
||||
|
||||
if ($begin !== null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Form\Type\MonthPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -41,6 +42,8 @@ final class ProjectDateRangeForm extends AbstractType
|
||||
'label' => 'includeNoWork',
|
||||
]);
|
||||
|
||||
$builder->add('view', HiddenType::class);
|
||||
|
||||
$builder->add('budgetType', ChoiceType::class, [
|
||||
'placeholder' => null,
|
||||
'required' => false,
|
||||
|
||||
@@ -18,6 +18,7 @@ final class ProjectDateRangeQuery
|
||||
private ?Customer $customer = null;
|
||||
private bool $includeNoWork = false;
|
||||
private ?string $budgetType = null;
|
||||
private ?string $view = '0';
|
||||
|
||||
public function __construct(\DateTime $month, private User $user)
|
||||
{
|
||||
@@ -83,4 +84,14 @@ final class ProjectDateRangeQuery
|
||||
{
|
||||
$this->budgetType = $budgetType;
|
||||
}
|
||||
|
||||
public function getView(): ?string
|
||||
{
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
public function setView(?string $view): void
|
||||
{
|
||||
$this->view = $view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,18 @@
|
||||
|
||||
namespace App\User;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\TeamCreateEvent;
|
||||
use App\Event\TeamCreatePostEvent;
|
||||
use App\Event\TeamCreatePreEvent;
|
||||
use App\Event\TeamDeleteEvent;
|
||||
use App\Event\TeamUpdatePostEvent;
|
||||
use App\Event\TeamUpdatePreEvent;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class TeamService
|
||||
{
|
||||
@@ -18,10 +29,19 @@ final class TeamService
|
||||
*/
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(private TeamRepository $repository)
|
||||
public function __construct(
|
||||
private readonly TeamRepository $repository,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function findTeamByName(string $name): ?Team
|
||||
{
|
||||
return $this->repository->findOneBy(['name' => $name]);
|
||||
}
|
||||
|
||||
public function countTeams(): int
|
||||
{
|
||||
if (!\array_key_exists('count', $this->cache)) {
|
||||
@@ -31,8 +51,67 @@ final class TeamService
|
||||
return $this->cache['count'];
|
||||
}
|
||||
|
||||
public function createNewTeam(string $name): Team
|
||||
{
|
||||
$team = new Team($name);
|
||||
$this->dispatcher->dispatch(new TeamCreateEvent($team));
|
||||
|
||||
return $team;
|
||||
}
|
||||
|
||||
public function saveTeam(Team $team): Team
|
||||
{
|
||||
if(null === $team->getId()) {
|
||||
// invalidate cache only on new teams
|
||||
return $this->saveNewTeam($team);
|
||||
}
|
||||
|
||||
return $this->updateTeam($team);
|
||||
}
|
||||
|
||||
private function saveNewTeam(Team $team): Team
|
||||
{
|
||||
if (null !== $team->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create team, already persisted');
|
||||
}
|
||||
|
||||
$this->validateTeam($team);
|
||||
|
||||
$this->dispatcher->dispatch(new TeamCreatePreEvent($team));
|
||||
$this->repository->saveTeam($team);
|
||||
$this->dispatcher->dispatch(new TeamCreatePostEvent($team));
|
||||
|
||||
return $team;
|
||||
}
|
||||
|
||||
public function hasTeams(): bool
|
||||
{
|
||||
return $this->countTeams() > 0;
|
||||
}
|
||||
|
||||
private function validateTeam(Team $team): void
|
||||
{
|
||||
$errors = $this->validator->validate($team);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
throw new ValidationFailedException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateTeam(Team $team): Team
|
||||
{
|
||||
$this->validateTeam($team);
|
||||
|
||||
$this->dispatcher->dispatch(new TeamUpdatePreEvent($team));
|
||||
$this->repository->saveTeam($team);
|
||||
$this->dispatcher->dispatch(new TeamUpdatePostEvent($team));
|
||||
|
||||
return $team;
|
||||
}
|
||||
|
||||
public function deleteTeam(Team $delete): void
|
||||
{
|
||||
$this->dispatcher->dispatch(new TeamDeleteEvent($delete));
|
||||
$this->repository->deleteTeam($delete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ final class ParsedownExtension extends Parsedown
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function blockTable($Line, array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter
|
||||
protected function blockTable($Line, ?array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter
|
||||
{
|
||||
$Block = parent::blockTable($Line, $Block);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Validator;
|
||||
|
||||
final class ValidationException extends \RuntimeException
|
||||
{
|
||||
public function __construct(string $message = null)
|
||||
public function __construct(?string $message = null)
|
||||
{
|
||||
if ($message === null) {
|
||||
$message = 'Validation Failed';
|
||||
|
||||
@@ -181,7 +181,7 @@ final class WorkingTimeService
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
// deprecated 3.0 remove $user, fetch from $month->getUser() instead
|
||||
// FIXME deprecated 3.0 remove $user, fetch from $month->getUser() instead
|
||||
public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approvedBy): void
|
||||
{
|
||||
foreach ($month->getDays() as $day) {
|
||||
|
||||
@@ -311,9 +311,9 @@ mpdf-->
|
||||
{% if id in ['duration', 'duration_decimal', 'duration_seconds'] %}
|
||||
<td class="totals duration">{{ duration|duration(id == 'duration_decimal') }}</td>
|
||||
{% elseif id == 'internal_rate' %}
|
||||
<td class="totals cost">3{% if currency is not null %}{{ rateInternal|money(currency) }}{% endif %}</td>
|
||||
<td class="totals cost">{% if currency is not null %}{{ rateInternal|money(currency) }}{% endif %}</td>
|
||||
{% elseif id == 'rate' %}
|
||||
<td class="totals cost">4{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
||||
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
'actions': {'class': 'actions alwaysVisible'},
|
||||
}) %}
|
||||
{% set tableName = 'project_daterange_reporting' %}
|
||||
{% set queryValue = app.request.query.get('view') %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
@@ -51,6 +52,9 @@
|
||||
{{ form_widget(form.includeNoWork) }}
|
||||
</li>
|
||||
</ul>
|
||||
<button onclick="{{ form.view.vars.id }}.value = {{ queryValue == '1' ? 0 : 1 }}" type="submit" class="btn btn-icon {% if queryValue != '1' %}active{% endif %}">
|
||||
{{ icon('collapse', true) }}
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -70,13 +74,39 @@
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, null, {'boxClass': ''}) }}
|
||||
|
||||
{% if queryValue != '1' %}
|
||||
{% for id, mapping in entries|sort((a, b) => a.customer.name <=> b.customer.name) %}
|
||||
{% set currency = mapping.customer.currency %}
|
||||
{% set totalDuration = 0 %}
|
||||
{% set totalRevenue = 0 %}
|
||||
{% set totalCosts = 0 %}
|
||||
|
||||
{% for entry in mapping.projects %}
|
||||
{% set totalDuration = totalDuration + entry.statistic.duration %}
|
||||
{% set totalRevenue = totalRevenue + entry.statistic.rateBillable %}
|
||||
{% set totalCosts = totalCosts + entry.statistic.internalRate %}
|
||||
{% endfor %}
|
||||
{% set totalProfit = totalRevenue - totalCosts %}
|
||||
|
||||
<tr class="summary">
|
||||
<td colspan="{{ columns|length }}">{{ widgets.label_customer(mapping.customer) }}</td>
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
{% if name == 'name' %}
|
||||
{{ widgets.label_customer(mapping.customer) }}
|
||||
{% elseif name == 'duration' %}
|
||||
{{ totalDuration|duration }}
|
||||
{% elseif name == 'revenue' %}
|
||||
{{ totalRevenue|money(currency) }}
|
||||
{% elseif name == 'costs' %}
|
||||
{{ totalCosts|money(currency) }}
|
||||
{% elseif name == 'profit' %}
|
||||
{{ totalProfit|money(currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for entry in mapping.projects|sort((a, b) => a.entity.name <=> b.entity.name) %}
|
||||
{% set project = entry.entity %}
|
||||
{% set currency = project.customer.currency %}
|
||||
<tr {{ widgets.project_row_attr(project, queryEnd) }}>
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
@@ -106,6 +136,96 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for id, mapping in entries|sort((a, b) => a.customer.name <=> b.customer.name) %}
|
||||
{% set currency = mapping.customer.currency %}
|
||||
{% set totalDuration = 0 %}
|
||||
{% set totalRevenue = 0 %}
|
||||
{% set totalCosts = 0 %}
|
||||
|
||||
{% for entry in mapping.projects %}
|
||||
{% set totalDuration = totalDuration + entry.statistic.duration %}
|
||||
{% set totalRevenue = totalRevenue + entry.statistic.rateBillable %}
|
||||
{% set totalCosts = totalCosts + entry.statistic.internalRate %}
|
||||
{% endfor %}
|
||||
{% set totalProfit = totalRevenue - totalCosts %}
|
||||
|
||||
<tr>
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
{% if name == 'name' %}
|
||||
{{ widgets.label_customer(mapping.customer) }}
|
||||
{% elseif name == 'duration' %}
|
||||
{{ totalDuration|duration }}
|
||||
{% elseif name == 'revenue' %}
|
||||
{{ totalRevenue|money(currency) }}
|
||||
{% elseif name == 'costs' %}
|
||||
{{ totalCosts|money(currency) }}
|
||||
{% elseif name == 'profit' %}
|
||||
{{ totalProfit|money(currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% set grandTotalDuration = 0 %}
|
||||
{% set grandTotalRevenue = 0 %}
|
||||
{% set grandTotalCosts = 0 %}
|
||||
{% set grandTotalProfit = 0 %}
|
||||
{% set currencyTotal = null %}
|
||||
|
||||
{% for id, mapping in entries %}
|
||||
{% set currency = mapping.customer.currency %}
|
||||
{% if currencyTotal is null %}
|
||||
{% set currencyTotal = currency %}
|
||||
{% endif %}
|
||||
{% if currency != currencyTotal %}
|
||||
{% set currencyTotal = false %}
|
||||
{% endif %}
|
||||
{% set totalDuration = 0 %}
|
||||
{% set totalRevenue = 0 %}
|
||||
{% set totalCosts = 0 %}
|
||||
|
||||
{# accumulate project data per customer #}
|
||||
{% for entry in mapping.projects %}
|
||||
{% set totalDuration = totalDuration + entry.statistic.duration %}
|
||||
{% set totalRevenue = totalRevenue + entry.statistic.rateBillable %}
|
||||
{% set totalCosts = totalCosts + entry.statistic.internalRate %}
|
||||
{% endfor %}
|
||||
|
||||
{% set totalProfit = totalRevenue - totalCosts %}
|
||||
|
||||
{# accumulate grand totals #}
|
||||
{% set grandTotalDuration = grandTotalDuration + totalDuration %}
|
||||
{% set grandTotalRevenue = grandTotalRevenue + totalRevenue %}
|
||||
{% set grandTotalCosts = grandTotalCosts + totalCosts %}
|
||||
{% set grandTotalProfit = grandTotalProfit + totalProfit %}
|
||||
{% endfor %}
|
||||
|
||||
{% if currencyTotal is same as (false) %}
|
||||
{% set currencyTotal = null %}
|
||||
{% endif %}
|
||||
|
||||
<tr class="summary">
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
{% if name == 'name' %}
|
||||
<strong>{{ 'sum.total'|trans }}</strong>
|
||||
{% elseif name == 'duration' %}
|
||||
<strong>{{ grandTotalDuration|duration }}</strong>
|
||||
{% elseif name == 'revenue' %}
|
||||
<strong>{{ grandTotalRevenue|money(currencyTotal) }}</strong>
|
||||
{% elseif name == 'costs' %}
|
||||
<strong>{{ grandTotalCosts|money(currencyTotal) }}</strong>
|
||||
{% elseif name == 'profit' %}
|
||||
<strong>{{ grandTotalProfit|money(currencyTotal) }}</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -304,12 +304,13 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'user' => ['result' => 'object', 'type' => '@User'],
|
||||
'dueDays' => 'int',
|
||||
'invoiceNumber' => 'string',
|
||||
'metaFields' => 'array',
|
||||
'paymentDate' => '@datetime',
|
||||
'status' => 'string',
|
||||
'tax' => 'float',
|
||||
'total' => 'float',
|
||||
'vat' => 'float',
|
||||
'overdue' => 'bool',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
];
|
||||
|
||||
case 'PageActionItem':
|
||||
@@ -326,8 +327,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'color' => 'string',
|
||||
'visible' => 'bool',
|
||||
];
|
||||
|
||||
@@ -354,14 +354,19 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'username' => 'string',
|
||||
'email' => 'string',
|
||||
'enabled' => 'bool',
|
||||
'apiToken' => 'bool',
|
||||
'color' => '@string',
|
||||
'systemAccount' => 'bool',
|
||||
'color' => 'string',
|
||||
'avatar' => '@string',
|
||||
'alias' => '@string',
|
||||
'accountNumber' => '@string',
|
||||
'initials' => '@string',
|
||||
'title' => '@string',
|
||||
'language' => 'string',
|
||||
'locale' => 'string',
|
||||
'timezone' => 'string',
|
||||
];
|
||||
|
||||
// if a user is loaded explicitly
|
||||
@@ -369,20 +374,23 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'username' => 'string',
|
||||
'email' => 'string',
|
||||
'enabled' => 'bool',
|
||||
'apiToken' => 'bool',
|
||||
'alias' => '@string',
|
||||
'title' => '@string',
|
||||
'supervisor' => ['result' => 'object', 'type' => '@UserEntity'],
|
||||
'systemAccount' => 'bool',
|
||||
'color' => 'string',
|
||||
'avatar' => '@string',
|
||||
'color' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'roles' => ['result' => 'array', 'type' => 'string'],
|
||||
'alias' => '@string',
|
||||
'accountNumber' => '@string',
|
||||
'initials' => 'string',
|
||||
'title' => '@string',
|
||||
'language' => 'string',
|
||||
'locale' => 'string',
|
||||
'timezone' => 'string',
|
||||
'accountNumber' => '@string',
|
||||
// TODO more info in entity than in collection
|
||||
'supervisor' => ['result' => 'object', 'type' => '@UserEntity'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'roles' => ['result' => 'array', 'type' => 'string'],
|
||||
'memberships' => ['result' => 'array', 'type' => 'TeamMembership'],
|
||||
'preferences' => ['result' => 'array', 'type' => 'UserPreference'],
|
||||
];
|
||||
@@ -394,7 +402,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
];
|
||||
|
||||
// explicitly requested team
|
||||
@@ -402,8 +410,9 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'members' => ['result' => 'array', 'type' => 'TeamMember'],
|
||||
// TODO more info in entity than in collection
|
||||
'customers' => ['result' => 'array', 'type' => '@Customer'],
|
||||
'projects' => ['result' => 'array', 'type' => '@Project'],
|
||||
'activities' => ['result' => 'array', 'type' => '@Activity'],
|
||||
@@ -430,12 +439,18 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'currency' => 'string', // since 2.40.0
|
||||
'country' => 'string', // since 2.40.0
|
||||
'company' => '@string', // since 2.40.0
|
||||
'currency' => 'string',
|
||||
'country' => 'string',
|
||||
'company' => '@string',
|
||||
'homepage' => '@string',
|
||||
'timezone' => 'string',
|
||||
'fax' => '@string',
|
||||
'mobile' => '@string',
|
||||
'phone' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'], // since 2.45
|
||||
];
|
||||
|
||||
// if a list of customers is loaded
|
||||
@@ -445,19 +460,19 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'boolean',
|
||||
'billable' => 'bool',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'currency' => 'string', // since 1.10
|
||||
'currency' => 'string',
|
||||
'country' => 'string',
|
||||
'company' => '@string',
|
||||
'homepage' => '@string',
|
||||
'timezone' => 'string',
|
||||
'fax' => '@string',
|
||||
'mobile' => '@string',
|
||||
'phone' => '@string',
|
||||
'timezone' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
];
|
||||
|
||||
// if a customer is loaded explicitly
|
||||
@@ -467,32 +482,34 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'homepage' => '@string',
|
||||
'color' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'company' => '@string',
|
||||
'contact' => '@string',
|
||||
'address' => '@string',
|
||||
'country' => 'string',
|
||||
'currency' => 'string',
|
||||
'phone' => '@string',
|
||||
'country' => 'string',
|
||||
'company' => '@string',
|
||||
'homepage' => '@string',
|
||||
'timezone' => 'string',
|
||||
'fax' => '@string',
|
||||
'mobile' => '@string',
|
||||
'phone' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
// TODO more info in entity than in collection
|
||||
'contact' => '@string',
|
||||
'email' => '@string',
|
||||
'timezone' => 'string',
|
||||
'vatId' => '@string',
|
||||
'addressLine1' => '@string',
|
||||
'addressLine2' => '@string',
|
||||
'addressLine3' => '@string',
|
||||
'city' => '@string',
|
||||
'postCode' => '@string',
|
||||
'buyerReference' => '@string',
|
||||
// only available in the entity itself
|
||||
'address' => '@string', // deprecated, do not expose in collection
|
||||
'budget' => 'float',
|
||||
'timeBudget' => 'int',
|
||||
'vatId' => '@string', // since 1.10
|
||||
'budgetType' => '@string', // since 1.15
|
||||
'addressLine1' => '@string', // since 2.32
|
||||
'addressLine2' => '@string', // since 2.32
|
||||
'addressLine3' => '@string', // since 2.32
|
||||
'city' => '@string', // since 2.32
|
||||
'postCode' => '@string', // since 2.32
|
||||
'buyerReference' => '@string', // since 2.41
|
||||
'budgetType' => '@string',
|
||||
];
|
||||
|
||||
// if a project is embedded
|
||||
@@ -502,14 +519,16 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
'orderDate' => '@date',
|
||||
'globalActivities' => 'bool',
|
||||
'comment' => '@string',
|
||||
'start' => '@datetime',
|
||||
'end' => '@datetime',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
];
|
||||
|
||||
// if a project is embedded in an expanded collection (here timesheet)
|
||||
@@ -519,14 +538,16 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'customer' => ['result' => 'object', 'type' => 'Customer'],
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
'orderDate' => '@date',
|
||||
'globalActivities' => 'bool',
|
||||
'comment' => '@string',
|
||||
'start' => '@datetime',
|
||||
'end' => '@datetime',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
];
|
||||
|
||||
// if a collection of projects is loaded
|
||||
@@ -536,17 +557,18 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
'color' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'parentTitle' => 'string',
|
||||
'orderDate' => '@date',
|
||||
'globalActivities' => 'bool',
|
||||
'comment' => '@string',
|
||||
'start' => '@datetime',
|
||||
'end' => '@datetime',
|
||||
'globalActivities' => 'bool',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'comment' => '@string',
|
||||
'parentTitle' => 'string',
|
||||
];
|
||||
|
||||
// if a project is explicitly loaded
|
||||
@@ -556,21 +578,22 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'parentTitle' => 'string',
|
||||
'start' => '@date',
|
||||
'end' => '@date',
|
||||
'globalActivities' => 'bool',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'comment' => '@string',
|
||||
'budget' => 'float',
|
||||
'timeBudget' => 'int',
|
||||
'orderNumber' => '@string',
|
||||
'orderDate' => '@date',
|
||||
'budgetType' => '@string', // since 1.15
|
||||
'globalActivities' => 'bool',
|
||||
'comment' => '@string',
|
||||
'start' => '@date',
|
||||
'end' => '@date',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'parentTitle' => 'string',
|
||||
// only available in the entity itself
|
||||
'budget' => 'float',
|
||||
'timeBudget' => 'int',
|
||||
'budgetType' => '@string',
|
||||
];
|
||||
|
||||
// embedded activities
|
||||
@@ -582,7 +605,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
@@ -594,7 +618,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => ['result' => 'object', 'type' => '@ProjectExpanded'],
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
@@ -607,10 +632,10 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'parentTitle' => '@string',
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
];
|
||||
|
||||
@@ -623,14 +648,15 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'parentTitle' => '@string',
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
// only available in the entity itself
|
||||
'budget' => 'float',
|
||||
'timeBudget' => 'int',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'budgetType' => '@string', // since 1.15
|
||||
'budgetType' => '@string',
|
||||
];
|
||||
|
||||
case 'TimesheetEntity':
|
||||
@@ -651,7 +677,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'fixedRate' => '@float',
|
||||
'hourlyRate' => '@float',
|
||||
// TODO new fields: category
|
||||
'break' => 'int',
|
||||
];
|
||||
|
||||
case 'TimesheetExpanded':
|
||||
@@ -672,7 +698,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'fixedRate' => '@float',
|
||||
'hourlyRate' => '@float',
|
||||
// TODO new fields: category
|
||||
'break' => 'int',
|
||||
];
|
||||
|
||||
case 'TimesheetCollection':
|
||||
@@ -691,6 +717,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'internalRate' => 'float',
|
||||
'exported' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'break' => 'int',
|
||||
];
|
||||
|
||||
case 'TimesheetCollectionFull':
|
||||
@@ -709,6 +736,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'internalRate' => 'float',
|
||||
'exported' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'break' => 'int',
|
||||
];
|
||||
|
||||
default:
|
||||
|
||||
@@ -258,7 +258,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['budgetType']);
|
||||
self::assertNull($result['number']);
|
||||
self::assertEquals('Test comment', $result['comment']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#5319e7', $result['color']);
|
||||
self::assertTrue($result['visible']);
|
||||
self::assertTrue($result['billable']);
|
||||
}
|
||||
@@ -297,7 +297,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
|
||||
self::assertEquals('Test', $result['parentTitle']);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null]], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
@@ -262,7 +262,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['homepage']);
|
||||
self::assertEquals('Europe/Berlin', $result['timezone']);
|
||||
self::assertNull($result['buyerReference']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#5319e7', $result['color']);
|
||||
self::assertTrue($result['visible']);
|
||||
self::assertTrue($result['billable']);
|
||||
}
|
||||
@@ -316,7 +316,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null]], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
@@ -329,7 +329,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['orderNumber']);
|
||||
self::assertNull($result['number']);
|
||||
self::assertNull($result['comment']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#2ECC40', $result['color']);
|
||||
self::assertTrue($result['globalActivities']);
|
||||
self::assertTrue($result['billable']);
|
||||
self::assertTrue($result['visible']);
|
||||
@@ -374,7 +374,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
|
||||
self::assertEquals('Test', $result['parentTitle']);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null]], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
@@ -34,6 +34,7 @@ class ActivityTest extends AbstractEntityTestCase
|
||||
self::assertTrue($sut->isBillable());
|
||||
self::assertTrue($sut->isGlobal());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertIsString($sut->getColorSafe());
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
@@ -71,6 +72,9 @@ class ActivityTest extends AbstractEntityTestCase
|
||||
$sut->setColor('#fffccc');
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
self::assertTrue($sut->hasColor());
|
||||
self::assertNotEmpty($sut->getColorSafe());
|
||||
$sut->setName('alsjdkhfalsf');
|
||||
self::assertEquals('#fffccc', $sut->getColorSafe());
|
||||
|
||||
$sut->setColor(Constants::DEFAULT_COLOR);
|
||||
self::assertNull($sut->getColor());
|
||||
|
||||
@@ -53,6 +53,7 @@ class CustomerTest extends AbstractEntityTestCase
|
||||
self::assertNull($sut->getTimezone());
|
||||
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertEquals('#e135f4', $sut->getColorSafe());
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
|
||||
@@ -38,6 +38,7 @@ class ProjectTest extends AbstractEntityTestCase
|
||||
self::assertTrue($sut->isBillable());
|
||||
self::assertTrue($sut->isGlobalActivities());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertIsString($sut->getColorSafe());
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
@@ -60,9 +61,21 @@ class ProjectTest extends AbstractEntityTestCase
|
||||
self::assertInstanceOf(Project::class, $sut->setCustomer($customer));
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
|
||||
self::assertFalse($sut->hasColor());
|
||||
$sut->setColor('#fffccc');
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
self::assertIsString($sut->getColorSafe());
|
||||
self::assertTrue($sut->hasColor());
|
||||
|
||||
$sut->setColor(Constants::DEFAULT_COLOR);
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertFalse($sut->hasColor());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setName('123456789'));
|
||||
self::assertEquals('123456789', (string) $sut);
|
||||
|
||||
self::assertEquals('#FF9800', $sut->getColorSafe());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setOrderNumber('123456789'));
|
||||
self::assertEquals('123456789', $sut->getOrderNumber());
|
||||
|
||||
@@ -88,15 +101,6 @@ class ProjectTest extends AbstractEntityTestCase
|
||||
$sut->setInvoiceText('very long invoice text comment 12324');
|
||||
self::assertEquals('very long invoice text comment 12324', $sut->getInvoiceText());
|
||||
|
||||
self::assertFalse($sut->hasColor());
|
||||
$sut->setColor('#fffccc');
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
self::assertTrue($sut->hasColor());
|
||||
|
||||
$sut->setColor(Constants::DEFAULT_COLOR);
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertFalse($sut->hasColor());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setVisible(false));
|
||||
self::assertFalse($sut->isVisible());
|
||||
|
||||
|
||||
@@ -28,9 +28,11 @@ class TagTest extends TestCase
|
||||
{
|
||||
$sut = new Tag();
|
||||
|
||||
self::assertIsString($sut->getColorSafe());
|
||||
self::assertInstanceOf(Tag::class, $sut->setName('foo'));
|
||||
self::assertEquals('foo', $sut->getName());
|
||||
self::assertEquals('foo', (string) $sut);
|
||||
self::assertEquals('#e135f4', $sut->getColorSafe());
|
||||
|
||||
$sut->setName(null);
|
||||
self::assertNull($sut->getName());
|
||||
|
||||
@@ -43,6 +43,7 @@ class TeamTest extends TestCase
|
||||
{
|
||||
$sut = new Team('foo');
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertEquals('#e135f4', $sut->getColorSafe());
|
||||
self::assertFalse($sut->hasColor());
|
||||
|
||||
$sut->setColor(Constants::DEFAULT_COLOR);
|
||||
|
||||
@@ -155,26 +155,6 @@ class TimesheetTest extends TestCase
|
||||
self::assertTrue($sut->isBillable());
|
||||
}
|
||||
|
||||
public function testCategory(): void
|
||||
{
|
||||
$sut = new Timesheet();
|
||||
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::HOLIDAY));
|
||||
self::assertEquals('holiday', $sut->getCategory());
|
||||
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::WORK));
|
||||
self::assertEquals('work', $sut->getCategory());
|
||||
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::SICKNESS));
|
||||
self::assertEquals('sickness', $sut->getCategory());
|
||||
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::PARENTAL));
|
||||
self::assertEquals('parental', $sut->getCategory());
|
||||
self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::OVERTIME));
|
||||
self::assertEquals('overtime', $sut->getCategory());
|
||||
|
||||
self::expectException(\InvalidArgumentException::class);
|
||||
self::expectExceptionMessage('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
|
||||
|
||||
$sut->setCategory('foo');
|
||||
}
|
||||
|
||||
public function testClone(): void
|
||||
{
|
||||
$sut = new Timesheet();
|
||||
|
||||
@@ -34,6 +34,7 @@ class UserTest extends TestCase
|
||||
self::assertInstanceOf(EquatableInterface::class, $user);
|
||||
self::assertInstanceOf(UserInterface::class, $user);
|
||||
self::assertInstanceOf(ArrayCollection::class, $user->getPreferences());
|
||||
self::assertEquals([], $user->getVisiblePreferences());
|
||||
self::assertNull($user->getTitle());
|
||||
self::assertNull($user->getAvatar());
|
||||
self::assertNull($user->getAlias());
|
||||
@@ -186,9 +187,13 @@ class UserTest extends TestCase
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertFalse($sut->hasColor());
|
||||
|
||||
$sut->setUsername('foo test 123');
|
||||
self::assertEquals('#a972c9', $sut->getColorSafe());
|
||||
|
||||
$sut->setColor('#000000');
|
||||
self::assertEquals('#000000', $sut->getColor());
|
||||
self::assertTrue($sut->hasColor());
|
||||
self::assertEquals('#000000', $sut->getColorSafe());
|
||||
}
|
||||
|
||||
public function testWizards(): void
|
||||
@@ -268,6 +273,19 @@ class UserTest extends TestCase
|
||||
$prefs = $user->getPreferences();
|
||||
self::assertCount(3, $prefs);
|
||||
|
||||
$preference1 = new UserPreference('_aaaaa', 'bbbbb');
|
||||
$user->addPreference($preference1);
|
||||
|
||||
$preference2 = new UserPreference('AAAAAA', 'CCCCCC');
|
||||
$user->addPreference($preference2);
|
||||
|
||||
$prefs = $user->getPreferences();
|
||||
self::assertCount(5, $prefs);
|
||||
|
||||
$visiblePrefs = $user->getVisiblePreferences();
|
||||
self::assertCount(3, $visiblePrefs); // export_decimal and _aaaaa is skipped
|
||||
self::assertEquals([$user->getPreference('test'), $user->getPreference('test2'), $preference2], $visiblePrefs);
|
||||
|
||||
self::assertInstanceOf(UserPreference::class, $prefs[0]);
|
||||
self::assertEquals('test', $prefs[0]->getName());
|
||||
|
||||
|
||||
29
tests/Event/AbstractTeamEventTestCase.php
Normal file
29
tests/Event/AbstractTeamEventTestCase.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
abstract class AbstractTeamEventTestCase extends TestCase
|
||||
{
|
||||
abstract protected function createTeamEvent(Team $team): AbstractTeamEvent;
|
||||
|
||||
public function testGetterAndSetter(): void
|
||||
{
|
||||
$team = new Team('foo');
|
||||
$sut = $this->createTeamEvent($team);
|
||||
|
||||
self::assertInstanceOf(Event::class, $sut);
|
||||
self::assertSame($team, $sut->getTeam());
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamCreateEventTest.php
Normal file
25
tests/Event/TeamCreateEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamCreateEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamCreateEvent::class)]
|
||||
class TeamCreateEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamCreateEvent($team);
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamCreatePostEventTest.php
Normal file
25
tests/Event/TeamCreatePostEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamCreatePostEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamCreatePostEvent::class)]
|
||||
class TeamCreatePostEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamCreatePostEvent($team);
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamCreatePreEventTest.php
Normal file
25
tests/Event/TeamCreatePreEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamCreatePreEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamCreatePreEvent::class)]
|
||||
class TeamCreatePreEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamCreatePreEvent($team);
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamDeleteEventTest.php
Normal file
25
tests/Event/TeamDeleteEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamDeleteEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamDeleteEvent::class)]
|
||||
class TeamDeleteEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamDeleteEvent($team);
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamUpdatePostEventTest.php
Normal file
25
tests/Event/TeamUpdatePostEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamUpdatePostEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamUpdatePostEvent::class)]
|
||||
class TeamUpdatePostEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamUpdatePostEvent($team);
|
||||
}
|
||||
}
|
||||
25
tests/Event/TeamUpdatePreEventTest.php
Normal file
25
tests/Event/TeamUpdatePreEventTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Event\AbstractTeamEvent;
|
||||
use App\Event\TeamUpdatePreEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(AbstractTeamEventTestCase::class)]
|
||||
#[CoversClass(TeamUpdatePreEvent::class)]
|
||||
class TeamUpdatePreEventTest extends AbstractTeamEventTestCase
|
||||
{
|
||||
protected function createTeamEvent(Team $team): AbstractTeamEvent
|
||||
{
|
||||
return new TeamUpdatePreEvent($team);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user