Release 2.45 (#5721)
Co-authored-by: Henning Klein <info@henningklein.de>
This commit is contained in:
@@ -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)]
|
||||
@@ -39,8 +39,8 @@ trait BudgetTrait
|
||||
private int $timeBudget = 0;
|
||||
/**
|
||||
* The type of budget:
|
||||
* - null = default / full time
|
||||
* - month = monthly budget
|
||||
* - null = default / full time
|
||||
* - month = monthly budget
|
||||
*/
|
||||
#[ORM\Column(name: 'budget_type', type: Types::STRING, length: 10, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
|
||||
@@ -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,10 +25,12 @@ final class CalendarToolbarForm extends AbstractType
|
||||
'view_timezone' => $options['timezone'],
|
||||
]);
|
||||
$builder->add('view', CalendarViewType::class, []);
|
||||
$builder->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
'attr' => ['onchange' => 'this.form.submit()']
|
||||
]);
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user