Release 2.1.0 (#4321)
* fix deprecations * remove unused config * replace invalid annotation type with attribute * use AsDoctrineListener to fix deprecation * new ModifiedSubscriber to support custom logic and fix deprecation * removed inheritdoc comment * new ModifiedSubscriber to support custom logic and fix deprecation * cleanup event dispatcher interface * re-order annotation params * one more doctrine based deprecation * fix query to count active timesheets * link to "all times" to identify active timesheets * link icon instead of text * fix "skin" translation in wizard * use duration filter to show duration * added login link command and controller * bump tabler theme to 1.0 * added wizard to force password reset by user * allow to configure that new accounts need to reset their password * prevent uploading twig templates by default * bump composer packages * enable sandbox and basic security measures for custom twig templates for invoice and export * bump to symfony 6.3.5 * allow to export single user reports to excel * removed broken method to reload twig cache * added api parameter to fetch user collection fully serialized * allow to replace or append description via timesheet batch update * show api username above form
This commit is contained in:
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,45 +1,34 @@
|
|||||||
# some hosters require a htaccess to change the PHP version
|
# some hosters require a htaccess to change the PHP version
|
||||||
.htaccess
|
/public/.htaccess
|
||||||
.env-*
|
/.env-*
|
||||||
.idea/
|
/.idea/
|
||||||
.DS_Store
|
/.DS_Store
|
||||||
rector.php
|
/phpstan.sh
|
||||||
phpstan.sh
|
|
||||||
|
|
||||||
# custom apache rules e.g. to deactivate ioncube loader
|
# custom apache rules e.g. to deactivate ioncube loader
|
||||||
public/.user.ini
|
/public/.user.ini
|
||||||
|
|
||||||
# for symfony local webserver
|
# for symfony local webserver
|
||||||
php.ini
|
/php.ini
|
||||||
.php-version
|
/.php-version
|
||||||
|
|
||||||
# YARN 2
|
# YARN 2
|
||||||
.yarnrc.yml
|
/.yarnrc.yml
|
||||||
.yarn
|
/.yarn
|
||||||
|
|
||||||
# for keeping empty directories
|
# for keeping empty directories
|
||||||
!.gitkeep
|
/config/packages/local.yaml
|
||||||
|
|
||||||
/bin/*
|
|
||||||
!/bin/console
|
|
||||||
*local.yaml
|
|
||||||
/config/bundles-local.php
|
/config/bundles-local.php
|
||||||
|
|
||||||
/templates/invoice/renderer/.~lock*
|
|
||||||
/translations/branding.*.xlf
|
|
||||||
|
|
||||||
/var/data/*
|
/var/data/*
|
||||||
/var/coverage/
|
|
||||||
/var/cache/*
|
/var/cache/*
|
||||||
|
/var/invoices*
|
||||||
/var/invoices/*
|
|
||||||
/var/export/*
|
/var/export/*
|
||||||
/var/log/*
|
/var/log/*
|
||||||
/var/sessions/*
|
/var/sessions/*
|
||||||
/var/packages/*
|
/var/packages/*
|
||||||
/var/plugins/*
|
/var/plugins/*
|
||||||
/var/plugins_old/
|
/var/plugins_old/
|
||||||
*.disabled
|
/var/plugins/*/*.disabled
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"friendsofsymfony/rest-bundle": "^3.0",
|
"friendsofsymfony/rest-bundle": "^3.0",
|
||||||
"gedmo/doctrine-extensions": "^3.6",
|
"gedmo/doctrine-extensions": "^3.6",
|
||||||
"jms/serializer-bundle": "^5.0",
|
"jms/serializer-bundle": "^5.0",
|
||||||
"kevinpapst/tabler-bundle": "^0.22",
|
"kevinpapst/tabler-bundle": "^1.0",
|
||||||
"league/csv": "^9.4",
|
"league/csv": "^9.4",
|
||||||
"mpdf/mpdf": "^8.0",
|
"mpdf/mpdf": "^8.0",
|
||||||
"nelmio/api-doc-bundle": "^4.0",
|
"nelmio/api-doc-bundle": "^4.0",
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
"symfony/polyfill-mbstring": "*",
|
"symfony/polyfill-mbstring": "*",
|
||||||
"symfony/polyfill-intl": "*",
|
"symfony/polyfill-intl": "*",
|
||||||
"symfony/polyfill-iconv": "*",
|
"symfony/polyfill-iconv": "*",
|
||||||
|
|||||||
715
composer.lock
generated
715
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ doctrine:
|
|||||||
default_table_options:
|
default_table_options:
|
||||||
charset: utf8mb4
|
charset: utf8mb4
|
||||||
collate: utf8mb4_unicode_ci
|
collate: utf8mb4_unicode_ci
|
||||||
|
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
|
||||||
|
|
||||||
types:
|
types:
|
||||||
datetime: App\Doctrine\UTCDateTimeType
|
datetime: App\Doctrine\UTCDateTimeType
|
||||||
@@ -26,6 +27,7 @@ doctrine:
|
|||||||
default_entity_manager: default
|
default_entity_manager: default
|
||||||
entity_managers:
|
entity_managers:
|
||||||
default:
|
default:
|
||||||
|
report_fields_where_declared: true
|
||||||
connection: default
|
connection: default
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ framework:
|
|||||||
cookie_httponly: true
|
cookie_httponly: true
|
||||||
storage_factory_id: session.storage.factory.native
|
storage_factory_id: session.storage.factory.native
|
||||||
|
|
||||||
#esi: ~
|
|
||||||
#fragments: ~
|
|
||||||
php_errors:
|
php_errors:
|
||||||
log: true
|
log: true
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,13 @@ kimai:
|
|||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# INVOICES
|
# INVOICES
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# invoice:
|
# invoice:
|
||||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||||
# documents:
|
# documents:
|
||||||
# - 'var/invoices/'
|
# - 'var/invoices/'
|
||||||
# - 'templates/invoice/renderer/'
|
# - 'templates/invoice/renderer/'
|
||||||
|
# # allow to upload twig templates as invoice documents
|
||||||
|
# upload_twig: true
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ security:
|
|||||||
max_attempts: 5
|
max_attempts: 5
|
||||||
interval: '5 minutes'
|
interval: '5 minutes'
|
||||||
|
|
||||||
|
login_link:
|
||||||
|
check_route: link_login_check
|
||||||
|
signature_properties: ['id']
|
||||||
|
lifetime: 300
|
||||||
|
max_uses: 1
|
||||||
|
|
||||||
access_decision_manager:
|
access_decision_manager:
|
||||||
strategy: unanimous
|
strategy: unanimous
|
||||||
allow_if_all_abstain: false
|
allow_if_all_abstain: false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
controllers:
|
controllers:
|
||||||
resource: ../src/Controller/
|
resource: ../src/Controller/
|
||||||
type: annotation
|
type: attribute
|
||||||
prefix: /{_locale}
|
prefix: /{_locale}
|
||||||
requirements:
|
requirements:
|
||||||
_locale: '%app_locales%'
|
_locale: '%app_locales%'
|
||||||
@@ -19,17 +19,17 @@ api.swagger:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
resource: ../src/API/
|
resource: ../src/API/
|
||||||
type: annotation
|
type: attribute
|
||||||
prefix: /api
|
prefix: /api
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
resource: ../src/Controller/Auth/
|
resource: ../src/Controller/Auth/
|
||||||
type: annotation
|
type: attribute
|
||||||
prefix: /auth
|
prefix: /auth
|
||||||
|
|
||||||
security:
|
security:
|
||||||
resource: ../src/Controller/Security/
|
resource: ../src/Controller/Security/
|
||||||
type: annotation
|
type: attribute
|
||||||
prefix: /{_locale}
|
prefix: /{_locale}
|
||||||
requirements:
|
requirements:
|
||||||
_locale: '%app_locales%'
|
_locale: '%app_locales%'
|
||||||
@@ -38,7 +38,7 @@ security:
|
|||||||
|
|
||||||
kernel:
|
kernel:
|
||||||
resource: ../src/Kernel.php
|
resource: ../src/Kernel.php
|
||||||
type: annotation
|
type: attribute
|
||||||
|
|
||||||
home:
|
home:
|
||||||
path: /
|
path: /
|
||||||
|
|||||||
@@ -91,16 +91,10 @@ services:
|
|||||||
App\Doctrine\TimesheetSubscriber:
|
App\Doctrine\TimesheetSubscriber:
|
||||||
class: App\Doctrine\TimesheetSubscriber
|
class: App\Doctrine\TimesheetSubscriber
|
||||||
arguments: [!tagged timesheet.calculator]
|
arguments: [!tagged timesheet.calculator]
|
||||||
tags:
|
|
||||||
- { name: doctrine.event_subscriber, priority: 50 }
|
|
||||||
|
|
||||||
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
|
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
|
||||||
Gedmo\Timestampable\TimestampableListener:
|
App\Doctrine\ModifiedSubscriber:
|
||||||
class: Gedmo\Timestampable\TimestampableListener
|
class: App\Doctrine\ModifiedSubscriber
|
||||||
tags:
|
|
||||||
- { name: doctrine.event_subscriber, priority: 60 }
|
|
||||||
calls:
|
|
||||||
- [ setAnnotationReader, [ "@annotation_reader" ] ]
|
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# TIMESHEET RECORD CALCULATOR
|
# TIMESHEET RECORD CALCULATOR
|
||||||
|
|||||||
19
phpstan.neon
19
phpstan.neon
@@ -928,7 +928,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'user' on mixed\\.$#"
|
message: "#^Cannot access offset 'user' on mixed\\.$#"
|
||||||
count: 2
|
count: 1
|
||||||
path: src/Controller/CalendarController.php
|
path: src/Controller/CalendarController.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -2578,12 +2578,12 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'customer' on mixed\\.$#"
|
message: "#^Cannot access offset 'customer' on mixed\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
|
path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
||||||
count: 6
|
count: 2
|
||||||
path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
|
path: src/Form/MultiUpdate/TimesheetMultiUpdate.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -3603,12 +3603,12 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'activity' on mixed\\.$#"
|
message: "#^Cannot access offset 'activity' on mixed\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Form/Type/QuickEntryWeekType.php
|
path: src/Form/Type/QuickEntryWeekType.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Form/Type/QuickEntryWeekType.php
|
path: src/Form/Type/QuickEntryWeekType.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -4293,7 +4293,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'lastRecord' on mixed\\.$#"
|
message: "#^Cannot access offset 'lastRecord' on mixed\\.$#"
|
||||||
count: 2
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -4438,7 +4438,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Reporting/ProjectDetails/ProjectDetailsForm.php
|
path: src/Reporting/ProjectDetails/ProjectDetailsForm.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -5716,11 +5716,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Utils/MenuItemModel.php
|
path: src/Utils/MenuItemModel.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:getRouteArgs\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Utils/MenuItemModel.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:setChildRoutes\\(\\) has parameter \\$routes with no value type specified in iterable type array\\.$#"
|
message: "#^Method App\\\\Utils\\\\MenuItemModel\\:\\:setChildRoutes\\(\\) has parameter \\$routes with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
|||||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[Route(path: '/actions')]
|
#[Route(path: '/actions')]
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Repository\Query\BaseQuery;
|
||||||
use App\Timesheet\DateTimeFactory;
|
use App\Timesheet\DateTimeFactory;
|
||||||
use App\Utils\Pagination;
|
use App\Utils\Pagination;
|
||||||
use FOS\RestBundle\View\View;
|
use FOS\RestBundle\View\View;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormTypeInterface;
|
use Symfony\Component\Form\FormTypeInterface;
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ abstract class BaseApiController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @template TFormType of FormTypeInterface<TData>
|
* @template TFormType of FormTypeInterface<TData>
|
||||||
* @template TData of mixed
|
* @template TData of BaseQuery
|
||||||
* @param class-string<TFormType> $type
|
* @param class-string<TFormType> $type
|
||||||
* @param TData|null $data
|
* @param TData $data
|
||||||
* @param array<mixed> $options
|
* @param array<mixed> $options
|
||||||
* @return FormInterface<TData|null>
|
* @return FormInterface<BaseQuery>
|
||||||
*/
|
*/
|
||||||
protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
protected function createSearchForm(string $type, BaseQuery $data, array $options = []): FormInterface
|
||||||
{
|
{
|
||||||
return $this->container
|
return $this->container
|
||||||
->get('form.factory')
|
->get('form.factory')
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -40,7 +41,6 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\Component\Validator\Constraints;
|
use Symfony\Component\Validator\Constraints;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
|
|
||||||
#[Route(path: '/timesheets')]
|
#[Route(path: '/timesheets')]
|
||||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||||
@@ -93,7 +93,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||||
#[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)')]
|
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||||
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
|
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
|
||||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||||
@@ -257,7 +257,8 @@ final class TimesheetController extends BaseApiController
|
|||||||
$view = new View($results, 200);
|
$view = new View($results, 200);
|
||||||
$this->addPagination($view, $data);
|
$this->addPagination($view, $data);
|
||||||
|
|
||||||
if (null !== $paramFetcher->get('full')) {
|
$full = $paramFetcher->get('full');
|
||||||
|
if ($full === '1' || $full === 'true') {
|
||||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||||
} else {
|
} else {
|
||||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use FOS\RestBundle\View\View;
|
|||||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
@@ -39,6 +39,7 @@ final class UserController extends BaseApiController
|
|||||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||||
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
||||||
|
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ViewHandlerInterface $viewHandler,
|
private ViewHandlerInterface $viewHandler,
|
||||||
@@ -60,6 +61,7 @@ final class UserController extends BaseApiController
|
|||||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
|
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
|
||||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||||
|
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||||
{
|
{
|
||||||
$query = new UserQuery();
|
$query = new UserQuery();
|
||||||
@@ -88,7 +90,13 @@ final class UserController extends BaseApiController
|
|||||||
$query->setIsApiCall(true);
|
$query->setIsApiCall(true);
|
||||||
$data = $this->repository->getUsersForQuery($query);
|
$data = $this->repository->getUsersForQuery($query);
|
||||||
$view = new View($data, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
|
||||||
|
$full = $paramFetcher->get('full');
|
||||||
|
if ($full === '1' || $full === 'true') {
|
||||||
|
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||||
|
} else {
|
||||||
|
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use App\Event\ActivityUpdatePreEvent;
|
|||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use App\Event\CalendarSourceEvent;
|
|||||||
use App\Event\RecentActivityEvent;
|
use App\Event\RecentActivityEvent;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use App\Utils\Color;
|
use App\Utils\Color;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
final class CalendarService
|
final class CalendarService
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||||
{
|
{
|
||||||
$username = $user->getUserIdentifier();
|
$username = $user->getUserIdentifier();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Repository\Query\TimesheetQuery;
|
|||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Timesheet\DateTimeFactory;
|
use App\Timesheet\DateTimeFactory;
|
||||||
use App\Utils\SearchTerm;
|
use App\Utils\SearchTerm;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\Table;
|
use Symfony\Component\Console\Helper\Table;
|
||||||
@@ -32,7 +33,6 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
|
|
||||||
#[AsCommand(name: 'kimai:invoice:create')]
|
#[AsCommand(name: 'kimai:invoice:create')]
|
||||||
final class InvoiceCreateCommand extends Command
|
final class InvoiceCreateCommand extends Command
|
||||||
|
|||||||
89
src/Command/UserLoginLinkCommand.php
Normal file
89
src/Command/UserLoginLinkCommand.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'kimai:user:login-link', description: 'Create a URL that can be used to login as that user', hidden: true)]
|
||||||
|
/**
|
||||||
|
* @CloudRequired
|
||||||
|
*/
|
||||||
|
final class UserLoginLinkCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LoginLinkHandlerInterface $loginLink,
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
private RequestStack $requestStack
|
||||||
|
)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
|
||||||
|
$this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
if ($email === null || $email === '') {
|
||||||
|
$io->error('Need email to create login URL');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
$io->error('Need username to create login URL');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->isEnabled()) {
|
||||||
|
$io->error('User is not enabled');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->isInternalUser()) {
|
||||||
|
$io->error('User does not use internal login');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = new Request();
|
||||||
|
$request->setLocale($user->getLocale());
|
||||||
|
$this->requestStack->push($request);
|
||||||
|
|
||||||
|
$loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
|
||||||
|
$loginLink = $loginLinkDetails->getUrl();
|
||||||
|
|
||||||
|
if ($input->getOption('password-reset') === true) {
|
||||||
|
$user->setPasswordRequestedAt(new \DateTime());
|
||||||
|
$user->setRequiresPasswordReset(true);
|
||||||
|
$this->userRepository->saveUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln($loginLink);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.35';
|
public const VERSION = '2.1.0';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20035;
|
public const VERSION_ID = 20100;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +31,7 @@ class Constants
|
|||||||
*/
|
*/
|
||||||
public const GITHUB = 'https://github.com/kimai/kimai/';
|
public const GITHUB = 'https://github.com/kimai/kimai/';
|
||||||
/**
|
/**
|
||||||
* The Github repository name
|
* The GitHub repository name
|
||||||
*/
|
*/
|
||||||
public const GITHUB_REPO = 'kimai/kimai';
|
public const GITHUB_REPO = 'kimai/kimai';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use App\Timesheet\DateTimeFactory;
|
|||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormTypeInterface;
|
use Symfony\Component\Form\FormTypeInterface;
|
||||||
@@ -76,13 +75,13 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @template TFormType of FormTypeInterface<TData>
|
* @template TFormType of FormTypeInterface<TData>
|
||||||
* @template TData of mixed
|
* @template TData of array|object
|
||||||
* @param class-string<TFormType> $type
|
* @param class-string<TFormType> $type
|
||||||
* @param TData|null $data
|
* @param TData $data
|
||||||
* @param array<mixed> $options
|
* @param array<mixed> $options
|
||||||
* @return FormInterface<TData|null>
|
* @return FormInterface<TData>
|
||||||
*/
|
*/
|
||||||
protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface
|
||||||
{
|
{
|
||||||
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
|
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ use App\Repository\TeamRepository;
|
|||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ use App\Reporting\YearByUser\YearByUser;
|
|||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use App\WorkingTime\Model\BoxConfiguration;
|
use App\WorkingTime\Model\BoxConfiguration;
|
||||||
use App\WorkingTime\WorkingTimeService;
|
use App\WorkingTime\WorkingTimeService;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Users can control their working time statistics
|
* Users can control their working time statistics
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ use App\Repository\Query\ProjectQuery;
|
|||||||
use App\Repository\TeamRepository;
|
use App\Repository\TeamRepository;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Repository\BookmarkRepository;
|
|||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use App\Widget\WidgetInterface;
|
use App\Widget\WidgetInterface;
|
||||||
use App\Widget\WidgetService;
|
use App\Widget\WidgetService;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Configuration\LocaleService;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\ConfigureMainMenuEvent;
|
use App\Event\ConfigureMainMenuEvent;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use App\Repository\Query\InvoiceQuery;
|
|||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
@@ -48,7 +49,6 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -468,36 +468,6 @@ final class InvoiceController extends AbstractController
|
|||||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
throw $this->createNotFoundException('Unknown document: ' . $document);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/document_reload/{document}', name: 'admin_invoice_document_reload', methods: ['GET', 'POST'])]
|
|
||||||
#[IsGranted('upload_invoice_template')]
|
|
||||||
public function reloadDocument(string $document, Environment $twig): Response
|
|
||||||
{
|
|
||||||
$event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
|
|
||||||
$this->dispatcher->dispatch($event);
|
|
||||||
|
|
||||||
$reloaded = false;
|
|
||||||
|
|
||||||
foreach ($event->getInvoiceDocuments() as $doc) {
|
|
||||||
if ($document === $doc->getId() && $doc->isTwig()) {
|
|
||||||
$reloaded = true;
|
|
||||||
try {
|
|
||||||
$twig->enableAutoReload();
|
|
||||||
$twig->load('@invoice/' . basename($doc->getFilename()));
|
|
||||||
$twig->disableAutoReload();
|
|
||||||
$this->flashSuccess('Reloaded template');
|
|
||||||
} catch (Exception $ex) {
|
|
||||||
$this->flashException($ex, 'Failed to reload template: ' . $ex->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$reloaded) {
|
|
||||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('upload_invoice_template')]
|
#[IsGranted('upload_invoice_template')]
|
||||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use App\Security\RolePermissionManager;
|
|||||||
use App\Security\RoleService;
|
use App\Security\RoleService;
|
||||||
use App\User\PermissionService;
|
use App\User\PermissionService;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ use Endroid\QrCode\Encoding\Encoding;
|
|||||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||||
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
||||||
use Endroid\QrCode\Writer\PngWriter;
|
use Endroid\QrCode\Writer\PngWriter;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ use App\Repository\TeamRepository;
|
|||||||
use App\Utils\Context;
|
use App\Utils\Context;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ReportUsersMonthController extends AbstractController
|
|||||||
{
|
{
|
||||||
$data = $this->getData($request, $statisticService, $userRepository);
|
$data = $this->getData($request, $statisticService, $userRepository);
|
||||||
|
|
||||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||||
|
|
||||||
$reader = new Html();
|
$reader = new Html();
|
||||||
$spreadsheet = $reader->loadFromString($content);
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ReportUsersWeekController extends AbstractController
|
|||||||
{
|
{
|
||||||
$data = $this->getData($request, $statisticService, $userRepository);
|
$data = $this->getData($request, $statisticService, $userRepository);
|
||||||
|
|
||||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||||
|
|
||||||
$reader = new Html();
|
$reader = new Html();
|
||||||
$spreadsheet = $reader->loadFromString($content);
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use App\Reporting\YearlyUserList\YearlyUserListForm;
|
|||||||
use App\Repository\Query\UserQuery;
|
use App\Repository\Query\UserQuery;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Timesheet\TimesheetStatisticService;
|
use App\Timesheet\TimesheetStatisticService;
|
||||||
use Exception;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -30,11 +29,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|||||||
#[IsGranted('report:other')]
|
#[IsGranted('report:other')]
|
||||||
final class ReportUsersYearController extends AbstractController
|
final class ReportUsersYearController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
#[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
|
#[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
|
||||||
public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||||
{
|
{
|
||||||
@@ -44,17 +38,12 @@ final class ReportUsersYearController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
#[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
|
#[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
|
||||||
public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||||
{
|
{
|
||||||
$data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
|
$data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
|
||||||
|
|
||||||
$content = $this->container->get('twig')->render('reporting/report_user_list_monthly_export.html.twig', $data);
|
$content = $this->renderView('reporting/report_user_list_monthly_export.html.twig', $data);
|
||||||
|
|
||||||
$reader = new Html();
|
$reader = new Html();
|
||||||
$spreadsheet = $reader->loadFromString($content);
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|||||||
@@ -10,10 +10,13 @@
|
|||||||
namespace App\Controller\Reporting;
|
namespace App\Controller\Reporting;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
|
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||||
use App\Model\DailyStatistic;
|
use App\Model\DailyStatistic;
|
||||||
use App\Reporting\MonthByUser\MonthByUser;
|
use App\Reporting\MonthByUser\MonthByUser;
|
||||||
use App\Reporting\MonthByUser\MonthByUserForm;
|
use App\Reporting\MonthByUser\MonthByUserForm;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -35,6 +38,21 @@ final class UserMonthController extends AbstractUserReportController
|
|||||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
|
||||||
|
public function export(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->getData($request);
|
||||||
|
|
||||||
|
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||||
|
|
||||||
|
$reader = new Html();
|
||||||
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|
||||||
|
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-monthly');
|
||||||
|
|
||||||
|
return $writer->getFileResponse($spreadsheet);
|
||||||
|
}
|
||||||
|
|
||||||
private function getData(Request $request): array
|
private function getData(Request $request): array
|
||||||
{
|
{
|
||||||
$currentUser = $this->getUser();
|
$currentUser = $this->getUser();
|
||||||
@@ -95,6 +113,7 @@ final class UserMonthController extends AbstractUserReportController
|
|||||||
'current' => $start,
|
'current' => $start,
|
||||||
'next' => $nextMonth,
|
'next' => $nextMonth,
|
||||||
'previous' => $previousMonth,
|
'previous' => $previousMonth,
|
||||||
|
'export_route' => 'report_user_month_export',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,13 @@
|
|||||||
|
|
||||||
namespace App\Controller\Reporting;
|
namespace App\Controller\Reporting;
|
||||||
|
|
||||||
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
|
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||||
use App\Model\DailyStatistic;
|
use App\Model\DailyStatistic;
|
||||||
use App\Reporting\WeekByUser\WeekByUser;
|
use App\Reporting\WeekByUser\WeekByUser;
|
||||||
use App\Reporting\WeekByUser\WeekByUserForm;
|
use App\Reporting\WeekByUser\WeekByUserForm;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -34,6 +37,21 @@ final class UserWeekController extends AbstractUserReportController
|
|||||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
|
||||||
|
public function export(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->getData($request);
|
||||||
|
|
||||||
|
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||||
|
|
||||||
|
$reader = new Html();
|
||||||
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|
||||||
|
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-weekly');
|
||||||
|
|
||||||
|
return $writer->getFileResponse($spreadsheet);
|
||||||
|
}
|
||||||
|
|
||||||
private function getData(Request $request): array
|
private function getData(Request $request): array
|
||||||
{
|
{
|
||||||
$currentUser = $this->getUser();
|
$currentUser = $this->getUser();
|
||||||
@@ -88,6 +106,7 @@ final class UserWeekController extends AbstractUserReportController
|
|||||||
'current' => $start,
|
'current' => $start,
|
||||||
'next' => $next,
|
'next' => $next,
|
||||||
'previous' => $previous,
|
'previous' => $previous,
|
||||||
|
'export_route' => 'report_user_week_export',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace App\Controller\Reporting;
|
|||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
|
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||||
use App\Model\DateStatisticInterface;
|
use App\Model\DateStatisticInterface;
|
||||||
use App\Model\MonthlyStatistic;
|
use App\Model\MonthlyStatistic;
|
||||||
use App\Reporting\YearByUser\YearByUser;
|
use App\Reporting\YearByUser\YearByUser;
|
||||||
@@ -18,6 +20,7 @@ use App\Reporting\YearByUser\YearByUserForm;
|
|||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -39,6 +42,21 @@ final class UserYearController extends AbstractUserReportController
|
|||||||
return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
|
return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
|
||||||
|
public function export(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||||
|
{
|
||||||
|
$data = $this->getData($request, $systemConfiguration);
|
||||||
|
|
||||||
|
$content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);
|
||||||
|
|
||||||
|
$reader = new Html();
|
||||||
|
$spreadsheet = $reader->loadFromString($content);
|
||||||
|
|
||||||
|
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-yearly');
|
||||||
|
|
||||||
|
return $writer->getFileResponse($spreadsheet);
|
||||||
|
}
|
||||||
|
|
||||||
private function getData(Request $request, SystemConfiguration $systemConfiguration): array
|
private function getData(Request $request, SystemConfiguration $systemConfiguration): array
|
||||||
{
|
{
|
||||||
$currentUser = $this->getUser();
|
$currentUser = $this->getUser();
|
||||||
@@ -104,6 +122,7 @@ final class UserYearController extends AbstractUserReportController
|
|||||||
'current' => $start,
|
'current' => $start,
|
||||||
'next' => $next,
|
'next' => $next,
|
||||||
'previous' => $previous,
|
'previous' => $previous,
|
||||||
|
'export_route' => 'report_user_year_export',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
src/Controller/Security/LoginLinkController.php
Normal file
27
src/Controller/Security/LoginLinkController.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?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\Controller\Security;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
#[Route(path: '/auth/link')]
|
||||||
|
/**
|
||||||
|
* @CloudRequired
|
||||||
|
*/
|
||||||
|
final class LoginLinkController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(path: '/check', name: 'link_login_check', methods: ['GET'])]
|
||||||
|
public function check(): Response
|
||||||
|
{
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ use App\Form\PasswordResetForm;
|
|||||||
use App\User\LoginManager;
|
use App\User\LoginManager;
|
||||||
use App\User\UserService;
|
use App\User\UserService;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -26,7 +27,6 @@ use Symfony\Component\Mime\Address;
|
|||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[Route(path: '/resetting')]
|
#[Route(path: '/resetting')]
|
||||||
@@ -108,7 +108,7 @@ final class PasswordResetController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('security/password-reset/check_email.html.twig', [
|
return $this->render('security/password-reset/check_email.html.twig', [
|
||||||
'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
|
'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use App\Event\EmailSelfRegistrationEvent;
|
|||||||
use App\Form\SelfRegistrationForm;
|
use App\Form\SelfRegistrationForm;
|
||||||
use App\User\LoginManager;
|
use App\User\LoginManager;
|
||||||
use App\User\UserService;
|
use App\User\UserService;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -27,7 +28,6 @@ use Symfony\Component\Mime\Email;
|
|||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[Route(path: '/register')]
|
#[Route(path: '/register')]
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ use App\Utils\PageSetup;
|
|||||||
use App\Validator\Constraints\ColorChoices;
|
use App\Validator\Constraints\ColorChoices;
|
||||||
use App\Validator\Constraints\DateTimeFormat;
|
use App\Validator\Constraints\DateTimeFormat;
|
||||||
use App\Validator\Constraints\TimeFormat;
|
use App\Validator\Constraints\TimeFormat;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use App\Timesheet\TimesheetService;
|
|||||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -334,6 +334,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
$execute = false;
|
$execute = false;
|
||||||
/** @var Timesheet $timesheet */
|
/** @var Timesheet $timesheet */
|
||||||
foreach ($timesheets as $timesheet) {
|
foreach ($timesheets as $timesheet) {
|
||||||
|
if ($dto->isReplaceDescription()) {
|
||||||
|
$timesheet->setDescription($dto->getDescription());
|
||||||
|
$execute = true;
|
||||||
|
} elseif($dto->getDescription() !== null && $dto->getDescription() !== '') {
|
||||||
|
$timesheet->setDescription($timesheet->getDescription() . PHP_EOL . $dto->getDescription());
|
||||||
|
$execute = true;
|
||||||
|
}
|
||||||
if ($dto->isReplaceTags()) {
|
if ($dto->isReplaceTags()) {
|
||||||
foreach ($timesheet->getTags() as $tag) {
|
foreach ($timesheet->getTags() as $tag) {
|
||||||
$timesheet->removeTag($tag);
|
$timesheet->removeTag($tag);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use App\Repository\UserRepository;
|
|||||||
use App\User\UserService;
|
use App\User\UserService;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\UserPreference;
|
|||||||
use App\Form\Type\LanguageType;
|
use App\Form\Type\LanguageType;
|
||||||
use App\Form\Type\SkinType;
|
use App\Form\Type\SkinType;
|
||||||
use App\Form\Type\TimezoneType;
|
use App\Form\Type\TimezoneType;
|
||||||
|
use App\Form\UserPasswordType;
|
||||||
use App\User\UserService;
|
use App\User\UserService;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -58,6 +59,11 @@ final class WizardController extends AbstractController
|
|||||||
->setMethod('POST')
|
->setMethod('POST')
|
||||||
->getForm();
|
->getForm();
|
||||||
|
|
||||||
|
$next = 'done';
|
||||||
|
if ($user->requiresPasswordReset()) {
|
||||||
|
$next = 'password';
|
||||||
|
}
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
@@ -72,13 +78,44 @@ final class WizardController extends AbstractController
|
|||||||
if ($data['reload'] === '1') {
|
if ($data['reload'] === '1') {
|
||||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
||||||
} else {
|
} else {
|
||||||
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
|
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('wizard/profile.html.twig', [
|
return $this->render('wizard/profile.html.twig', [
|
||||||
'percent' => \intval(100 / \count(User::WIZARDS) * 1),
|
'percent' => \intval(100 / \count(User::WIZARDS) * 1),
|
||||||
'previous' => 'intro',
|
'previous' => 'intro',
|
||||||
|
'next' => $next,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wizard === 'password' || $user->requiresPasswordReset()) {
|
||||||
|
$form = $this->createForm(UserPasswordType::class, $user, [
|
||||||
|
'action' => $this->generateUrl('wizard', ['wizard' => 'password']),
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$user->setRequiresPasswordReset(false);
|
||||||
|
$userService->updateUser($user);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('wizard', ['wizard' => 'done']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$previous = 'profile';
|
||||||
|
$percent = \intval(100 / \count(User::WIZARDS) * 1);
|
||||||
|
|
||||||
|
if ($user->requiresPasswordReset()) {
|
||||||
|
$previous = null;
|
||||||
|
$percent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('wizard/password.html.twig', [
|
||||||
|
'percent' => $percent,
|
||||||
|
'previous' => $previous,
|
||||||
'next' => 'done',
|
'next' => 'done',
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use App\Repository\Query\CustomerQuery;
|
|||||||
use App\Utils\NumberGenerator;
|
use App\Utils\NumberGenerator;
|
||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
final class CustomerService
|
final class CustomerService
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->defaultValue('{Y}/{cy,3}')
|
->defaultValue('{Y}/{cy,3}')
|
||||||
->end()
|
->end()
|
||||||
->booleanNode('upload_twig')
|
->booleanNode('upload_twig')
|
||||||
->defaultTrue()
|
->defaultFalse()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|||||||
50
src/Doctrine/ModifiedSubscriber.php
Normal file
50
src/Doctrine/ModifiedSubscriber.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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\Doctrine;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically set the modifiedAt field for all Timesheet entries.
|
||||||
|
*/
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
|
||||||
|
final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
|
||||||
|
{
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$uow = $args->getObjectManager()->getUnitOfWork();
|
||||||
|
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!($entity instanceof Timesheet)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$entity->setModifiedAt($now);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!($entity instanceof Timesheet)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$entity->setModifiedAt($now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ namespace App\Doctrine;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Timesheet\CalculatorInterface;
|
use App\Timesheet\CalculatorInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
use Doctrine\Common\EventSubscriber;
|
use Doctrine\Common\EventSubscriber;
|
||||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
use Doctrine\ORM\Events;
|
use Doctrine\ORM\Events;
|
||||||
@@ -18,6 +19,7 @@ use Doctrine\ORM\Events;
|
|||||||
/**
|
/**
|
||||||
* A listener to make sure all Timesheet entries will be calculated properly (e.g. duration and rates).
|
* A listener to make sure all Timesheet entries will be calculated properly (e.g. duration and rates).
|
||||||
*/
|
*/
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush, priority: 50)]
|
||||||
final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterface
|
final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
|||||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
||||||
@@ -87,7 +87,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
|||||||
*
|
*
|
||||||
* @var Collection<ActivityMeta>
|
* @var Collection<ActivityMeta>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Activity'])]
|
#[Serializer\Groups(['Activity'])]
|
||||||
#[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
|
#[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
|
||||||
@@ -102,7 +102,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
|||||||
#[ORM\JoinTable(name: 'kimai2_activities_teams')]
|
#[ORM\JoinTable(name: 'kimai2_activities_teams')]
|
||||||
#[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')]
|
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'activities', cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Activity'])]
|
#[Serializer\Groups(['Activity'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ActivityMeta implements MetaTableTypeInterface
|
|||||||
use MetaTableTypeTrait;
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
|
#[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Activity $activity = null;
|
private ?Activity $activity = null;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ActivityRate implements RateInterface
|
|||||||
use Rate;
|
use Rate;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Activity $activity = null;
|
private ?Activity $activity = null;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Bookmark
|
|||||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
|
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ trait CommentTableTypeTrait
|
|||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?string $message = null;
|
private ?string $message = null;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?User $createdBy = null;
|
private ?User $createdBy = null;
|
||||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||||||
*
|
*
|
||||||
* @var Collection<CustomerMeta>
|
* @var Collection<CustomerMeta>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Customer'])]
|
#[Serializer\Groups(['Customer'])]
|
||||||
#[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
|
#[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
|
||||||
@@ -171,7 +171,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||||||
#[ORM\JoinTable(name: 'kimai2_customers_teams')]
|
#[ORM\JoinTable(name: 'kimai2_customers_teams')]
|
||||||
#[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')]
|
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'customers', cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Customer'])]
|
#[Serializer\Groups(['Customer'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||||
@@ -180,7 +180,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||||||
* Default invoice template for this customer
|
* Default invoice template for this customer
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
|
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||||
private ?InvoiceTemplate $invoiceTemplate = null;
|
private ?InvoiceTemplate $invoiceTemplate = null;
|
||||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||||
private ?string $invoiceText = null;
|
private ?string $invoiceText = null;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class CustomerComment implements CommentInterface
|
|||||||
use CommentTableTypeTrait;
|
use CommentTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private Customer $customer;
|
private Customer $customer;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class CustomerMeta implements MetaTableTypeInterface
|
|||||||
use MetaTableTypeTrait;
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
|
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Customer $customer = null;
|
private ?Customer $customer = null;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CustomerRate implements RateInterface
|
|||||||
use Rate;
|
use Rate;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Customer $customer = null;
|
private ?Customer $customer = null;
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ class Invoice implements EntityWithMetaFields
|
|||||||
#[Exporter\Expose(label: 'comment')]
|
#[Exporter\Expose(label: 'comment')]
|
||||||
private ?string $comment = null;
|
private ?string $comment = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Customer $customer = null;
|
private ?Customer $customer = null;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||||
@@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields
|
|||||||
*
|
*
|
||||||
* @var Collection<InvoiceMeta>
|
* @var Collection<InvoiceMeta>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Invoice'])]
|
#[Serializer\Groups(['Invoice'])]
|
||||||
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class InvoiceMeta implements MetaTableTypeInterface
|
|||||||
use MetaTableTypeTrait;
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
|
#[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Invoice $invoice = null;
|
private ?Invoice $invoice = null;
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||||||
* Customer for this project
|
* Customer for this project
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||||
@@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||||||
*
|
*
|
||||||
* @var Collection<ProjectMeta>
|
* @var Collection<ProjectMeta>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Project'])]
|
#[Serializer\Groups(['Project'])]
|
||||||
#[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
|
#[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
|
||||||
@@ -152,7 +152,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||||||
#[ORM\JoinTable(name: 'kimai2_projects_teams')]
|
#[ORM\JoinTable(name: 'kimai2_projects_teams')]
|
||||||
#[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')]
|
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'projects', cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Project'])]
|
#[Serializer\Groups(['Project'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ProjectComment implements CommentInterface
|
|||||||
use CommentTableTypeTrait;
|
use CommentTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private Project $project;
|
private Project $project;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ProjectMeta implements MetaTableTypeInterface
|
|||||||
use MetaTableTypeTrait;
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
|
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Project $project = null;
|
private ?Project $project = null;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ProjectRate implements RateInterface
|
|||||||
use Rate;
|
use Rate;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Project $project = null;
|
private ?Project $project = null;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ trait Rate
|
|||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/User')]
|
#[OA\Property(ref: '#/components/schemas/User')]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class RolePermission
|
|||||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Role::class)]
|
#[ORM\ManyToOne(targetEntity: Role::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Role $role = null;
|
private ?Role $role = null;
|
||||||
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
|
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Tag
|
|||||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||||
#[Assert\NotBlank]
|
#[Assert\NotBlank]
|
||||||
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
|
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
|
||||||
#[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
|
#[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class Team
|
|||||||
*
|
*
|
||||||
* @var Collection<TeamMember>
|
* @var Collection<TeamMember>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'team', targetEntity: TeamMember::class, cascade: ['persist', 'remove'], fetch: 'LAZY', orphanRemoval: true)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||||
#[Assert\Count(min: 1)]
|
#[Assert\Count(min: 1)]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
@@ -59,7 +59,7 @@ class Team
|
|||||||
*
|
*
|
||||||
* @var Collection<Customer>
|
* @var Collection<Customer>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Team_Entity'])]
|
#[Serializer\Groups(['Team_Entity'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
|
||||||
@@ -69,7 +69,7 @@ class Team
|
|||||||
*
|
*
|
||||||
* @var Collection<Project>
|
* @var Collection<Project>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
|
||||||
@@ -79,7 +79,7 @@ class Team
|
|||||||
*
|
*
|
||||||
* @var Collection<Activity>
|
* @var Collection<Activity>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
|
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class TeamMember
|
|||||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')]
|
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])]
|
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/User')]
|
#[OA\Property(ref: '#/components/schemas/User')]
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')]
|
#[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
|
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use DateTimeZone;
|
|||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Gedmo\Mapping\Annotation as Gedmo;
|
|
||||||
use JMS\Serializer\Annotation as Serializer;
|
use JMS\Serializer\Annotation as Serializer;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
@@ -128,21 +127,21 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
private ?int $duration = 0;
|
private ?int $duration = 0;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/User')]
|
#[OA\Property(ref: '#/components/schemas/User')]
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
|
#[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
|
||||||
private ?Activity $activity = null;
|
private ?Activity $activity = null;
|
||||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||||
@@ -189,12 +188,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?string $category = self::WORK;
|
private ?string $category = self::WORK;
|
||||||
/**
|
|
||||||
* @internal used for limiting queries, eg. via API sync
|
|
||||||
*/
|
|
||||||
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
|
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
|
||||||
#[Gedmo\Timestampable]
|
private \DateTimeInterface $modifiedAt;
|
||||||
private ?\DateTime $modifiedAt = null;
|
|
||||||
/**
|
/**
|
||||||
* Tags
|
* Tags
|
||||||
*
|
*
|
||||||
@@ -211,7 +206,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
*
|
*
|
||||||
* @var Collection<TimesheetMeta>
|
* @var Collection<TimesheetMeta>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'timesheet', targetEntity: TimesheetMeta::class, cascade: ['persist'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Timesheet'])]
|
#[Serializer\Groups(['Timesheet'])]
|
||||||
#[Serializer\Type(name: 'array<App\Entity\TimesheetMeta>')]
|
#[Serializer\Type(name: 'array<App\Entity\TimesheetMeta>')]
|
||||||
@@ -223,6 +218,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
{
|
{
|
||||||
$this->tags = new ArrayCollection();
|
$this->tags = new ArrayCollection();
|
||||||
$this->meta = new ArrayCollection();
|
$this->meta = new ArrayCollection();
|
||||||
|
$this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -590,11 +586,16 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModifiedAt(): ?DateTime
|
public function getModifiedAt(): \DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->modifiedAt;
|
return $this->modifiedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setModifiedAt(\DateTimeInterface $dateTime): void
|
||||||
|
{
|
||||||
|
$this->modifiedAt = $dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|MetaTableTypeInterface[]
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
*/
|
*/
|
||||||
@@ -687,7 +688,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// field will not be set, if it contains a value
|
// field will not be set, if it contains a value
|
||||||
$this->modifiedAt = null;
|
$this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||||
$this->exported = false;
|
$this->exported = false;
|
||||||
|
|
||||||
$currentMeta = $this->meta;
|
$currentMeta = $this->meta;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TimesheetMeta implements MetaTableTypeInterface
|
|||||||
use MetaTableTypeTrait;
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')]
|
#[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?Timesheet $timesheet = null;
|
private ?Timesheet $timesheet = null;
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[Exporter\Expose(name: 'username', label: 'username', exp: 'object.getUserIdentifier()')]
|
#[Exporter\Expose(name: 'username', label: 'username', exp: 'object.getUserIdentifier()')]
|
||||||
#[Exporter\Expose(name: 'timezone', label: 'timezone', exp: 'object.getTimezone()')]
|
#[Exporter\Expose(name: 'timezone', label: 'timezone', exp: 'object.getTimezone()')]
|
||||||
#[Exporter\Expose(name: 'language', label: 'language', exp: 'object.getLanguage()')]
|
#[Exporter\Expose(name: 'language', label: 'language', exp: 'object.getLanguage()')]
|
||||||
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', exp: 'object.getLastLogin()', type: 'datetime')]
|
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', type: 'datetime', exp: 'object.getLastLogin()')]
|
||||||
#[Exporter\Expose(name: 'roles', label: 'roles', exp: 'object.getRoles()', type: 'array')]
|
#[Exporter\Expose(name: 'roles', label: 'roles', type: 'array', exp: 'object.getRoles()')]
|
||||||
#[Exporter\Expose(name: 'active', label: 'active', exp: 'object.isEnabled()', type: 'boolean')]
|
#[Exporter\Expose(name: 'active', label: 'active', type: 'boolean', exp: 'object.isEnabled()')]
|
||||||
#[Constraints\User(groups: ['UserCreate', 'Registration', 'Default', 'Profile'])]
|
#[Constraints\User(groups: ['UserCreate', 'Registration', 'Default', 'Profile'])]
|
||||||
class User implements UserInterface, EquatableInterface, ThemeUserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
|
class User implements UserInterface, EquatableInterface, ThemeUserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
|
||||||
{
|
{
|
||||||
@@ -127,15 +127,15 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
*
|
*
|
||||||
* @var Collection<UserPreference>|null
|
* @var Collection<UserPreference>|null
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
|
||||||
private ?Collection $preferences = null;
|
private ?Collection $preferences = null;
|
||||||
/**
|
/**
|
||||||
* List of all team memberships.
|
* List of all team memberships.
|
||||||
*
|
*
|
||||||
* @var Collection<TeamMember>
|
* @var Collection<TeamMember>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'user', targetEntity: TeamMember::class, cascade: ['persist'], fetch: 'LAZY', orphanRemoval: true)]
|
||||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['User_Entity'])]
|
#[Serializer\Groups(['User_Entity'])]
|
||||||
@@ -217,7 +217,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
|
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||||
private bool $systemAccount = false;
|
private bool $systemAccount = false;
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['User_Entity'])]
|
#[Serializer\Groups(['User_Entity'])]
|
||||||
#[OA\Property(ref: '#/components/schemas/User')]
|
#[OA\Property(ref: '#/components/schemas/User')]
|
||||||
@@ -297,6 +297,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Serializer\VirtualProperty]
|
||||||
|
#[Serializer\SerializedName('apiToken')]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
public function hasApiToken(): bool
|
||||||
|
{
|
||||||
|
return $this->apiToken !== null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getPlainApiToken(): ?string
|
public function getPlainApiToken(): ?string
|
||||||
{
|
{
|
||||||
return $this->plainApiToken;
|
return $this->plainApiToken;
|
||||||
@@ -836,9 +844,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this->email !== null;
|
return $this->email !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getPassword(): ?string
|
public function getPassword(): ?string
|
||||||
{
|
{
|
||||||
return $this->password;
|
return $this->password;
|
||||||
@@ -864,9 +869,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this->confirmationToken;
|
return $this->confirmationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getRoles(): array
|
public function getRoles(): array
|
||||||
{
|
{
|
||||||
$roles = $this->roles;
|
$roles = $this->roles;
|
||||||
@@ -963,7 +965,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPasswordRequestedAt(\DateTime $date = null): User
|
public function setPasswordRequestedAt(?\DateTime $date = null): User
|
||||||
{
|
{
|
||||||
$this->passwordRequestedAt = $date;
|
$this->passwordRequestedAt = $date;
|
||||||
|
|
||||||
@@ -1113,6 +1115,20 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this->getDisplayName();
|
return $this->getDisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function requiresPasswordReset(): bool
|
||||||
|
{
|
||||||
|
if (!$this->isInternalUser() || !$this->isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getPreferenceValue('__pw_reset__') === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRequiresPasswordReset(bool $require = true): void
|
||||||
|
{
|
||||||
|
$this->setPreferenceValue('__pw_reset__', ($require ? '1' : '0'));
|
||||||
|
}
|
||||||
|
|
||||||
public function hasSeenWizard(string $wizard): bool
|
public function hasSeenWizard(string $wizard): bool
|
||||||
{
|
{
|
||||||
$wizards = $this->getPreferenceValue('__wizards__');
|
$wizards = $this->getPreferenceValue('__wizards__');
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
|
|||||||
|
|
||||||
$event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]);
|
$event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]);
|
||||||
|
|
||||||
if ($document->isTwig()) {
|
|
||||||
$event->addAction('reload', ['url' => $this->path('admin_invoice_document_reload', ['document' => $document->getId()])]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$inUse) {
|
if (!$inUse) {
|
||||||
$event->addDelete($this->path('invoice_document_delete', ['id' => $document->getId(), 'token' => $token]), false);
|
$event->addDelete($this->path('invoice_document_delete', ['id' => $document->getId(), 'token' => $token]), false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use App\Event\ConfigureMainMenuEvent;
|
|||||||
use App\Utils\MenuItemModel;
|
use App\Utils\MenuItemModel;
|
||||||
use KevinPapst\TablerBundle\Event\MenuEvent;
|
use KevinPapst\TablerBundle\Event\MenuEvent;
|
||||||
use KevinPapst\TablerBundle\Model\MenuItemInterface;
|
use KevinPapst\TablerBundle\Model\MenuItemInterface;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use App\Form\Type\SkinType;
|
|||||||
use App\Form\Type\TimezoneType;
|
use App\Form\Type\TimezoneType;
|
||||||
use App\Form\Type\UserLanguageType;
|
use App\Form\Type\UserLanguageType;
|
||||||
use App\Form\Type\YesNoType;
|
use App\Form\Type\YesNoType;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\EventSubscriber;
|
|||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\PrepareUserEvent;
|
use App\Event\PrepareUserEvent;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|||||||
@@ -49,29 +49,32 @@ class WizardSubscriber implements EventSubscriberInterface
|
|||||||
$uri = $event->getRequest()->getRequestUri();
|
$uri = $event->getRequest()->getRequestUri();
|
||||||
|
|
||||||
// never require 2FA on API calls
|
// never require 2FA on API calls
|
||||||
if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false) {
|
if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false || stripos($uri, '/wizard/') !== false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $token->getUser();
|
$user = $token->getUser();
|
||||||
|
|
||||||
if ($user instanceof User) {
|
if (!($user instanceof User)) {
|
||||||
if (stripos($uri, '/wizard/') !== false) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (User::WIZARDS as $wizard) {
|
||||||
|
if (!$user->hasSeenWizard($wizard)) {
|
||||||
|
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
|
||||||
|
$event->setResponse($response);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
|
if ($user->requiresPasswordReset()) {
|
||||||
return;
|
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
|
||||||
}
|
$event->setResponse($response);
|
||||||
|
|
||||||
foreach (User::WIZARDS as $wizard) {
|
|
||||||
if (!$user->hasSeenWizard($wizard)) {
|
|
||||||
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
|
|
||||||
$event->setResponse($response);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|||||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ use App\Event\UserPreferenceDisplayEvent;
|
|||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
use Twig\Extension\SandboxExtension;
|
||||||
|
|
||||||
class HtmlRenderer
|
class HtmlRenderer
|
||||||
{
|
{
|
||||||
@@ -93,6 +95,11 @@ class HtmlRenderer
|
|||||||
|
|
||||||
$summary = $this->calculateSummary($timesheets);
|
$summary = $this->calculateSummary($timesheets);
|
||||||
|
|
||||||
|
// enable basic security measures
|
||||||
|
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||||
|
$sandbox->enableSandbox();
|
||||||
|
$this->twig->addExtension($sandbox);
|
||||||
|
|
||||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||||
'entries' => $timesheets,
|
'entries' => $timesheets,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ use App\Pdf\PdfContext;
|
|||||||
use App\Pdf\PdfRendererTrait;
|
use App\Pdf\PdfRendererTrait;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
use Twig\Extension\SandboxExtension;
|
||||||
|
|
||||||
class PDFRenderer implements DispositionInlineInterface
|
class PDFRenderer implements DispositionInlineInterface
|
||||||
{
|
{
|
||||||
@@ -76,6 +78,12 @@ class PDFRenderer implements DispositionInlineInterface
|
|||||||
$context->setOption('filename', $filename->getFilename());
|
$context->setOption('filename', $filename->getFilename());
|
||||||
|
|
||||||
$summary = $this->calculateSummary($timesheets);
|
$summary = $this->calculateSummary($timesheets);
|
||||||
|
|
||||||
|
// enable basic security measures
|
||||||
|
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||||
|
$sandbox->enableSandbox();
|
||||||
|
$this->twig->addExtension($sandbox);
|
||||||
|
|
||||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||||
'entries' => $timesheets,
|
'entries' => $timesheets,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\Export\Renderer;
|
|||||||
|
|
||||||
use App\Activity\ActivityStatisticService;
|
use App\Activity\ActivityStatisticService;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
final class HtmlRendererFactory
|
final class HtmlRendererFactory
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use App\Event\ExportItemsQueryEvent;
|
|||||||
use App\Export\Renderer\HtmlRendererFactory;
|
use App\Export\Renderer\HtmlRendererFactory;
|
||||||
use App\Export\Renderer\PdfRendererFactory;
|
use App\Export\Renderer\PdfRendererFactory;
|
||||||
use App\Repository\Query\ExportQuery;
|
use App\Repository\Query\ExportQuery;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
final class ServiceExport
|
final class ServiceExport
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace App\Export\Spreadsheet\Extractor;
|
|||||||
use App\Entity\EntityWithMetaFields;
|
use App\Entity\EntityWithMetaFields;
|
||||||
use App\Event\MetaDisplayEventInterface;
|
use App\Event\MetaDisplayEventInterface;
|
||||||
use App\Export\Spreadsheet\ColumnDefinition;
|
use App\Export\Spreadsheet\ColumnDefinition;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace App\Export\Spreadsheet\Extractor;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\UserPreferenceDisplayEvent;
|
use App\Event\UserPreferenceDisplayEvent;
|
||||||
use App\Export\Spreadsheet\ColumnDefinition;
|
use App\Export\Spreadsheet\ColumnDefinition;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Form\MultiUpdate;
|
|||||||
|
|
||||||
use App\Form\Type\ActivityType;
|
use App\Form\Type\ActivityType;
|
||||||
use App\Form\Type\CustomerType;
|
use App\Form\Type\CustomerType;
|
||||||
|
use App\Form\Type\DescriptionType;
|
||||||
use App\Form\Type\FixedRateType;
|
use App\Form\Type\FixedRateType;
|
||||||
use App\Form\Type\HourlyRateType;
|
use App\Form\Type\HourlyRateType;
|
||||||
use App\Form\Type\MetaFieldsCollectionType;
|
use App\Form\Type\MetaFieldsCollectionType;
|
||||||
@@ -141,9 +142,12 @@ final class TimesheetMultiUpdate extends AbstractType
|
|||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
|
'label_attr' => [
|
||||||
|
'class' => 'radio-inline',
|
||||||
|
],
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'replaceTags' => true,
|
'append' => false,
|
||||||
'appendTags' => false,
|
'replace' => true,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -151,6 +155,23 @@ final class TimesheetMultiUpdate extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$builder->add('replaceDescription', ChoiceType::class, [
|
||||||
|
'label' => false,
|
||||||
|
'required' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'label_attr' => [
|
||||||
|
'class' => 'radio-inline',
|
||||||
|
],
|
||||||
|
'choices' => [
|
||||||
|
'append' => false,
|
||||||
|
'replace' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('description', DescriptionType::class, [
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
if ($options['include_user']) {
|
if ($options['include_user']) {
|
||||||
$builder->add('user', UserType::class, [
|
$builder->add('user', UserType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
|
|||||||
* @var array<string>
|
* @var array<string>
|
||||||
*/
|
*/
|
||||||
private array $updateMeta = [];
|
private array $updateMeta = [];
|
||||||
|
private bool $replaceDescription = false;
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -98,6 +100,16 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
|
|||||||
$this->tags = $tags;
|
$this->tags = $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): void
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
}
|
||||||
|
|
||||||
public function getUser(): ?User
|
public function getUser(): ?User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
@@ -148,6 +160,16 @@ final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements Entit
|
|||||||
$this->replaceTags = $replaceTags;
|
$this->replaceTags = $replaceTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isReplaceDescription(): bool
|
||||||
|
{
|
||||||
|
return $this->replaceDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReplaceDescription(bool $replaceDescription): void
|
||||||
|
{
|
||||||
|
$this->replaceDescription = $replaceDescription;
|
||||||
|
}
|
||||||
|
|
||||||
public function getFixedRate(): ?float
|
public function getFixedRate(): ?float
|
||||||
{
|
{
|
||||||
return $this->fixedRate;
|
return $this->fixedRate;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\Form\Type;
|
|||||||
|
|
||||||
use App\Event\ConfigureMainMenuEvent;
|
use App\Event\ConfigureMainMenuEvent;
|
||||||
use App\Utils\MenuItemModel;
|
use App\Utils\MenuItemModel;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class SkinType extends AbstractType
|
|||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
|
'label' => 'skin',
|
||||||
'search' => false,
|
'search' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'choices' => self::THEMES,
|
'choices' => self::THEMES,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Form;
|
|||||||
|
|
||||||
use App\Form\Type\TeamType;
|
use App\Form\Type\TeamType;
|
||||||
use App\Form\Type\UserRoleType;
|
use App\Form\Type\UserRoleType;
|
||||||
|
use App\Form\Type\YesNoType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@@ -63,6 +64,12 @@ class UserCreateType extends UserEditType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$builder->add('requiresPasswordReset', YesNoType::class, [
|
||||||
|
'label' => 'force_password_change',
|
||||||
|
'help' => 'force_password_change_help',
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ use App\Repository\InvoiceDocumentRepository;
|
|||||||
use App\Repository\InvoiceRepository;
|
use App\Repository\InvoiceRepository;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use App\Utils\FileHelper;
|
use App\Utils\FileHelper;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to manage invoice dependencies.
|
* Service to manage invoice dependencies.
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class Kernel extends BaseKernel
|
|||||||
$container->registerExtension(new AppExtension());
|
$container->registerExtension(new AppExtension());
|
||||||
|
|
||||||
$container->setParameter('container.autowiring.strict_mode', true);
|
$container->setParameter('container.autowiring.strict_mode', true);
|
||||||
$container->setParameter('container.dumper.inline_class_loader', true);
|
$container->setParameter('.container.dumper.inline_class_loader', true);
|
||||||
$confDir = $this->getProjectDir() . '/config';
|
$confDir = $this->getProjectDir() . '/config';
|
||||||
|
|
||||||
// using this one instead of $loader->load($confDir . '/packages/*' . self::CONFIG_EXTS, 'glob');
|
// using this one instead of $loader->load($confDir . '/packages/*' . self::CONFIG_EXTS, 'glob');
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use App\Repository\ProjectRepository;
|
|||||||
use App\Utils\Context;
|
use App\Utils\Context;
|
||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ use App\Repository\UserRepository;
|
|||||||
use App\Timesheet\DateTimeFactory;
|
use App\Timesheet\DateTimeFactory;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @final
|
* @final
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\Reporting;
|
|||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\ReportingEvent;
|
use App\Event\ReportingEvent;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
|
||||||
final class ReportingService
|
final class ReportingService
|
||||||
|
|||||||
@@ -355,14 +355,17 @@ class TimesheetRepository extends EntityRepository
|
|||||||
{
|
{
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
$qb->select('COUNT(t)')
|
$qb->select($qb->expr()->count('t'))
|
||||||
->from(Timesheet::class, 't')
|
->from(Timesheet::class, 't')
|
||||||
->andWhere($qb->expr()->isNull('t.end'))
|
->andWhere($qb->expr()->isNull('t.end'))
|
||||||
->orderBy('t.begin', 'DESC');
|
;
|
||||||
|
|
||||||
if (null !== $user) {
|
if (null !== $user) {
|
||||||
$qb->andWhere('t.user = :user');
|
$qb
|
||||||
$qb->setParameter('user', $user);
|
->andWhere('t.user = :user')
|
||||||
|
->groupBy('t.user')
|
||||||
|
->setParameter('user', $user)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Security;
|
namespace App\Security;
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
|
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
|
||||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface;
|
use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
@@ -22,9 +21,6 @@ final class TwoFactorCondition implements TwoFactorConditionInterface
|
|||||||
|
|
||||||
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
|
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
|
||||||
$user = $context->getUser();
|
|
||||||
|
|
||||||
// never require 2FA on API calls
|
// never require 2FA on API calls
|
||||||
if (str_starts_with($context->getRequest()->getRequestUri(), '/api/')) {
|
if (str_starts_with($context->getRequest()->getRequestUri(), '/api/')) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use App\Timesheet\TrackingMode\TrackingModeInterface;
|
|||||||
use App\Validator\ValidationException;
|
use App\Validator\ValidationException;
|
||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
use Symfony\Component\Validator\ConstraintViolation;
|
use Symfony\Component\Validator\ConstraintViolation;
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ final class Extensions extends AbstractExtension
|
|||||||
{
|
{
|
||||||
public const REPORT_DATE = 'Y-m-d';
|
public const REPORT_DATE = 'Y-m-d';
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getFilters(): array
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -37,9 +34,6 @@ final class Extensions extends AbstractExtension
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getFunctions(): array
|
public function getFunctions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getFilters(): array
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -74,9 +71,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getFunctions(): array
|
public function getFunctions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user